#pragma once #ifndef FUNCTIONS_H #define FUNCTIONS_H #define WIN32_LEAN_AND_MEAN #include #include #include struct Call { int returnValue; std::string returnString; DWORD exitCode; }; namespace functions { enum class Type { T_VOID = 0x0, T_STRING = 0x1, T_CHAR = 0x2, T_BOOL = 0x3, T_INT = 0x4, T_DOUBLE = 0x5, T_FLOAT = 0x6 }; struct Arg { Type type; LPVOID value; }; LPVOID reserveString(HANDLE hProcess, const char* value, SIZE_T size); char readChar(HANDLE hProcess, DWORD64 address); template Call call(HANDLE pHandle, std::vector args, Type returnType, DWORD64 address, char** errorMessage) { std::vector argShellcode; std::reverse(args.begin(), args.end()); for (auto &arg : args) { // 0x68: PUSH imm16/imm32 // 0x6A: PUSH imm8 if (arg.type == Type::T_INT || arg.type == Type::T_FLOAT) { argShellcode.push_back(0x68); int value = *static_cast(arg.value); // Little endian representation for (int i = 0; i < 4; i++) { int shifted = (value >> (i * 8)) & 0xFF; argShellcode.push_back(shifted); } continue; } if (arg.type == Type::T_STRING) { argShellcode.push_back(0x68); std::string value = *static_cast(arg.value); LPVOID address = functions::reserveString(pHandle, value.c_str(), value.length()); // Little endian representation for (int i = 0; i < 4; i++) { int shifted = ((int)address >> (i * 8)) & 0xFF; argShellcode.push_back(shifted); } continue; } argShellcode.push_back(0x6A); unsigned char value = *static_cast(arg.value); argShellcode.push_back(value); } // 83: ADD r/m16/32 imm8 std::vector callShellcode = { // call 0x00000000 0xE8, 0x00, 0x00, 0x00, 0x00, // add esp, [arg count * 4] 0x83, 0xC4, (unsigned char)(args.size() * 0x4), }; LPVOID returnValuePointer = 0; if (returnType != Type::T_VOID) { // We will reserve memory for where we want to store the result, // and move the return value to this address. returnValuePointer = VirtualAllocEx(pHandle, NULL, sizeof(returnDataType), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (returnType == Type::T_FLOAT) { // fstp DWORD PTR [0x12345678] // when `call` is executed, if return type is float, it's stored // in fpu registers (st0 through st7). we can use the `fst` // instruction to move st0 to a place in memory // D9 FSTP m32real ST Store Floating Point Value and Pop // D9 = for m32 callShellcode.push_back(0xD9); callShellcode.push_back(0x1C); callShellcode.push_back(0x25); } else if (returnType == Type::T_DOUBLE) { // fstp QWORD PTR [0x12345678] // DD FSTP m64real ST Store Floating Point Value and Pop // DD = for m64 callShellcode.push_back(0xDD); callShellcode.push_back(0x1C); callShellcode.push_back(0x25); } else { // mov [0x1234], eax // A3: MOVE moffs16/32 eAX // Call routines places return value inside EAX callShellcode.push_back(0xA3); } for (int i = 0; i < 4; i++) { int shifted = ((DWORD)returnValuePointer >> (i * 8)) & 0xFF; callShellcode.push_back(shifted); } } // C3: return callShellcode.push_back(0xC3); // concatenate the arg shellcode with the calling shellcode std::vector shellcode; shellcode.reserve(argShellcode.size() + callShellcode.size()); shellcode.insert(shellcode.end(), argShellcode.begin(), argShellcode.end()); shellcode.insert(shellcode.end(), callShellcode.begin(), callShellcode.end()); // 5 = 0xE8 (call) + 4 empty bytes for where the address will go int addessShellcodeOffset = argShellcode.size() + 5; // Allocate space for the shellcode SIZE_T size = shellcode.size() * sizeof(unsigned char); LPVOID pShellcode = VirtualAllocEx(pHandle, NULL, size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); // `call` opcode takes relative address, so calculate the relative address // taking into account where the shellcode will be written in memory DWORD64 relative = address - (uintptr_t)pShellcode - addessShellcodeOffset; // Write the relative address to the shellcode for (int i = 0; i < 4; i++) { int shifted = (relative >> (i * 8)) & 0xFF; // argShellcode.size() will offset to the `0xE8` opcode, add 1 to offset that instruction shellcode.at(argShellcode.size() + 1 + i) = shifted; } // Write the shellcode WriteProcessMemory(pHandle, pShellcode, shellcode.data(), size, NULL); // Execute the shellcode HANDLE thread = CreateRemoteThread(pHandle, NULL, NULL, (LPTHREAD_START_ROUTINE)pShellcode, NULL, NULL, NULL); Call data = { 0, "", (DWORD) -1 }; if (thread == NULL) { *errorMessage = "unable to create remote thread."; return data; } WaitForSingleObject(thread, INFINITE); GetExitCodeThread(thread, &data.exitCode); if (returnType != Type::T_VOID && returnType != Type::T_STRING) { ReadProcessMemory(pHandle, (LPVOID)returnValuePointer, &data.returnValue, sizeof(int), NULL); VirtualFreeEx(pHandle, returnValuePointer, 0, MEM_RELEASE); } if (returnType == Type::T_STRING) { // String is stored in memory somewhere // When returning a string, the address of the string is placed in EAX. // So we read the current returnValuePointer address to get the actual address of the string ReadProcessMemory(pHandle, (LPVOID)returnValuePointer, &returnValuePointer, sizeof(int), NULL); std::vector chars; int offset = 0x0; while (true) { char c = functions::readChar(pHandle, (DWORD64)returnValuePointer + offset); chars.push_back(c); // break at 1 million chars if (offset == (sizeof(char) * 1000000)) { chars.clear(); break; } // break at terminator (end of string) if (c == '\0') { break; } // go to next char offset += sizeof(char); } std::string str(chars.begin(), chars.end()); // TODO: pass str as LPVOID and cast back to string data.returnString = str; } VirtualFreeEx(pHandle, pShellcode, 0, MEM_RELEASE); return data; } } #endif #pragma once