-
-
[原创]《逆向工程核心原理》书中代码注入实验在64位Windows11上的复现与思考
-
发表于: 5天前 606
-
《逆向工程核心原理》书中代码注入实验在64位Windows11上的复现与思考
书中的实验是在32位Windows7上演示的,我在64位Windows11中复现了书中的实验。在复现的过程中遇到了不少问题,故写一篇博客记录下来。由于笔者水平有限,文中难免有错误,请广大读者斧正。
代码注入原理
和使用远程线程进行DLL注入很像,但是代码注入更有隐蔽性和灵活性。主要的思路如下:
- 编写注入代码
- 构造注入代码的参数
- 在目标进程中分配两块内存空间(VirtualAllocEx)
- 拷贝注入代码与参数至目标进行中(WriteProcessMemory)
- 调用CreateRemoteThread执行注入的代码(注入程序需要具有特定权限,可以使用OpenProcessToken等函数进行提权)
原作者示例代码与分析
下面的代码是原作者的代码:
// CodeInjection.cpp
// reversecore@gmail.com
// e33K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4u0W2N6X3g2J5M7$3g2U0L8%4u0W2i4K6u0W2j5$3!0E0
#include "stdio.h"
#include "windows.h"
typedef struct _THREAD_PARAM
{
FARPROC pFunc[2]; // LoadLibraryA(), GetProcAddress()
char szBuf[4][128]; // "user32.dll", "MessageBoxA", "4f1K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4u0W2N6X3g2J5M7$3g2U0L8%4u0W2i4K6u0W2j5$3!0E0", "ReverseCore"
} THREAD_PARAM, *PTHREAD_PARAM;
typedef HMODULE(WINAPI *PFLOADLIBRARYA)(LPCSTR lpLibFileName);
typedef FARPROC(WINAPI *PFGETPROCADDRESS)(HMODULE hModule, LPCSTR lpProcName);
typedef int(WINAPI *PFMESSAGEBOXA)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
DWORD WINAPI ThreadProc(LPVOID lParam)
{
PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam;
HMODULE hMod = NULL;
FARPROC pFunc = NULL;
// LoadLibrary()
hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]); // "user32.dll"
if (!hMod)
return 1;
// GetProcAddress()
pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]); // "MessageBoxA"
if (!pFunc)
return 1;
// MessageBoxA()
((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);
return 0;
}
BOOL InjectCode(DWORD dwPID)
{
HMODULE hMod = NULL;
THREAD_PARAM param = {
0,
};
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
LPVOID pRemoteBuf[2] = {
0,
};
DWORD dwSize = 0;
hMod = GetModuleHandleA("kernel32.dll");
// set THREAD_PARAM
param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");
strcpy_s(param.szBuf[0], "user32.dll");
strcpy_s(param.szBuf[1], "MessageBoxA");
strcpy_s(param.szBuf[2], "4b1K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4u0W2N6X3g2J5M7$3g2U0L8%4u0W2i4K6u0W2j5$3!0E0");
strcpy_s(param.szBuf[3], "ReverseCore");
// Open Process
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, // dwDesiredAccess
FALSE, // bInheritHandle
dwPID))) // dwProcessId
{
printf("OpenProcess() fail : err_code = %d\n", GetLastError());
return FALSE;
}
// Allocation for THREAD_PARAM
dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
if (!(pRemoteBuf[0] = VirtualAllocEx(hProcess, // hProcess
NULL, // lpAddress
dwSize, // dwSize
MEM_COMMIT, // flAllocationType
PAGE_READWRITE))) // flProtect
{
printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
return FALSE;
}
if (!WriteProcessMemory(hProcess, // hProcess
pRemoteBuf[0], // lpBaseAddress
(LPVOID)¶m, // lpBuffer
dwSize, // nSize
NULL)) // [out] lpNumberOfBytesWritten
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}
// Allocation for ThreadProc()
dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
if (!(pRemoteBuf[1] = VirtualAllocEx(hProcess, // hProcess
NULL, // lpAddress
dwSize, // dwSize
MEM_COMMIT, // flAllocationType
PAGE_EXECUTE_READWRITE))) // flProtect
{
printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
return FALSE;
}
if (!WriteProcessMemory(hProcess, // hProcess
pRemoteBuf[1], // lpBaseAddress
(LPVOID)ThreadProc, // lpBuffer
dwSize, // nSize
NULL)) // [out] lpNumberOfBytesWritten
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}
if (!(hThread = CreateRemoteThread(hProcess, // hProcess
NULL, // lpThreadAttributes
0, // dwStackSize
(LPTHREAD_START_ROUTINE)pRemoteBuf[1], // dwStackSize
pRemoteBuf[0], // lpParameter
0, // dwCreationFlags
NULL))) // lpThreadId
{
printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());
return FALSE;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege)
{
TOKEN_PRIVILEGES tp;
HANDLE hToken;
LUID luid;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
printf("OpenProcessToken error: %u\n", GetLastError());
return FALSE;
}
if (!LookupPrivilegeValue(NULL, // lookup privilege on local system
lpszPrivilege, // privilege to lookup
&luid)) // receives LUID of privilege
{
printf("LookupPrivilegeValue error: %u\n", GetLastError());
return FALSE;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
if (bEnablePrivilege)
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
else
tp.Privileges[0].Attributes = 0;
// Enable the privilege or disable all privileges.
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL))
{
printf("AdjustTokenPrivileges error: %u\n", GetLastError());
return FALSE;
}
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)
{
printf("The token does not have the specified privilege. \n");
return FALSE;
}
return TRUE;
}
int main(int argc, char *argv[])
{
DWORD dwPID = 0;
if (argc != 2)
{
printf("\n USAGE : %s <pid>\n", argv[0]);
return 1;
}
// change privilege
if (!SetPrivilege(SE_DEBUG_NAME, TRUE))
return 1;
// code injection
dwPID = (DWORD)atol(argv[1]);
InjectCode(dwPID);
return 0;
}
代码不多,先是定义了注入代码需要用到的参数结构体与一些函数指针。然后对注入程序进行提权,执行注入函数。这里需要分析一下如何在远程进程中调用DLL中的函数。
原作者的代码基于一个重要的事实:Windows上的程序一般都会载入kernel32.dll并且加载基址相同。这使得我们可以在自己的进程中使用GetProcAddress函数获取kernel32.dll中导出函数的地址(绝对地址),把这个地址放在远程进程中,也是正确的函数起始地址。这样我们就可以在自己的进程中编写使用kernel32.dll中的函数的代码,并将这些代码拷贝到目标进程中执行。
注意,在原作者的代码中,对于ThreadProc函数大小的计算是dwSize = InjectCode - ThreadProc,这很重要,原作者假定InjectCode函数的起始地址在ThreadProc函数之后,事实上在原作者的环境中也确实如此(32位Windows7)。但是在64位的Windows11中也这样吗?请接着往下看笔者的代码。
笔者的代码与分析
下面是我的代码,基于原作者的代码修改而来,主要的修改为:
- 硬编码调用UNICODE版本的函数
- 在计算ThreadProc大小的语句中,笔者的计算方式和原作者是相反的,即:dwSize = ThreadProc - InjectCode
ThreadProc大小的计算方式之所以与原作者相反,是因为笔者的调试注入程序时发现:**ThreadProc的起始地址是大于InjectCode的。**也许读者以为函数的起始地址排列方式是与它们在源文件中的定义顺序一致的,但是在64位Windows11中并非如此,后面会详细分析这一问题。先看笔者的源代码:
#include "Windows.h"
#include "iostream"
// 定义线程过程ThreadProc的参数结构体
struct THREAD_PARAM
{
FARPROC pFunc[2]; // LoadLibrary, GetProcAddress
TCHAR szDllName[32]; // L"user32"
char szProcName[32]; // "MessageBoxW"
TCHAR szMsgArgs[2][32];
};
typedef HMODULE(WINAPI *PFLOADLIBRARYW)(LPCWSTR lpLibFileName); // LoadLibraryW
typedef FARPROC(WINAPI *PFGETPROCADDRESS)(HMODULE hModule, LPCSTR lpProcName); // GetProcAddress
typedef int(WINAPI *PFMESSAGEBOXW)(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);
// 需要注入的代码
DWORD WINAPI ThreadProc(LPVOID lParam)
{
THREAD_PARAM *pParam = (THREAD_PARAM *)lParam;
HMODULE hMod = NULL;
FARPROC pFunc = NULL;
// LoadLibrary()
hMod = ((PFLOADLIBRARYW)pParam->pFunc[0])(pParam->szDllName); // "user32.dll"
if (!hMod)
return 1;
// GetProcAddress()
pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szProcName); // "MessageBoxW"
if (!pFunc)
return 1;
// MessageBox()
((PFMESSAGEBOXW)pFunc)(NULL, pParam->szMsgArgs[0], pParam->szMsgArgs[1], MB_OK);
return 0;
}
DWORD WINAPI ThreadProcEnd()
{
return 0;
}
BOOL InjectCode(DWORD dwPID)
{
//DbgFunc();
HMODULE hMod = NULL;
THREAD_PARAM param = {0};
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
LPVOID pRemoteBuf[2] = {0};
DWORD dwSize = 0;
hMod = GetModuleHandleW(L"kernel32.dll");
// set THREAD_PARAM
param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryW");
param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");
wcscpy_s(param.szDllName, L"user32.dll");
strcpy_s(param.szProcName, "MessageBoxW");
wcscpy_s(param.szMsgArgs[0], L"f00K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4u0W2N6X3g2J5M7$3g2U0L8%4u0W2i4K6u0W2j5$3!0E0");
wcscpy_s(param.szMsgArgs[1], L"ReverseCore");
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
printf("OpenProcess() fail : err_code = %d\n", GetLastError());
return FALSE;
}
// 在需要注入的进程中分配内存,这块内存就是ThreadFunc的参数所在之处
dwSize = sizeof(THREAD_PARAM);
if (!(pRemoteBuf[0] = VirtualAllocEx(hProcess, // hProcess
NULL, // lpAddress
dwSize, // dwSize
MEM_COMMIT, // flAllocationType
PAGE_READWRITE))) // flProtect
{
printf("VirtualAllocEx() param fail : err_code = %d\n", GetLastError());
return FALSE;
}
// 将param写入远程进程的地址空间中
if (!WriteProcessMemory(hProcess, // hProcess
pRemoteBuf[0], // lpBaseAddress
(LPVOID)¶m, // lpBuffer
dwSize, // nSize
NULL)) // [out] lpNumberOfBytesWritten
{
printf("WriteProcessMemory() param fail : err_code = %d\n", GetLastError());
return FALSE;
}
// 分配ThreadProc的空间 ======== 这里有点坑
dwSize = (DWORD)ThreadProc - (DWORD)InjectCode;
// dwSize = (DWORD)ThreadProcEnd - (DWORD)ThreadProc;
std::cout << dwSize << std::endl;
if (!(pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE)))
{
printf("VirtualAllocEx() ThreadProc fail : err_code = %d\n", GetLastError());
return FALSE;
}
if (!WriteProcessMemory(hProcess, // hProcess
pRemoteBuf[1], // lpBaseAddress
(LPVOID)ThreadProc, // lpBuffer
dwSize, // nSize
NULL)) // [out] lpNumberOfBytesWritten
{
printf("WriteProcessMemory() ThreadProc fail : err_code = %d\n", GetLastError());
return FALSE;
}
std::cout << "lParam: " << pRemoteBuf[0] << std::endl;
std::cout << "ThreadProc: " << pRemoteBuf[1] << std::endl;
// 开始注入代码
if (!(hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)pRemoteBuf[1], pRemoteBuf[0], 0, NULL)))
{
printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());
return FALSE;
}
std::cout << "Injected !!" << std::endl;
WaitForSingleObject(hThread, INFINITE);
std::cout << "Done!!" << std::endl;
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
// 打开或者关闭当前进程的特定权限
// 1. 字符串形式的特权名称
// 2. 开启或者关闭
BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege)
{
TOKEN_PRIVILEGES tp;
HANDLE hToken;
LUID luid;
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
std::cout << "OpenProcessToken error: " << GetLastError() << std::endl;
return FALSE;
}
// 查找特权LUID:将lpszPrivilege转换位luid
if (!LookupPrivilegeValue(NULL, lpszPrivilege, &luid))
{
std::cout << "LookupPrivilegeValue error: " << GetLastError() << std::endl;
return FALSE;
}
tp.PrivilegeCount = 1; // 只修改一个特权
tp.Privileges[0].Luid = luid; // 设置需要修改的特权LUID
if (bEnablePrivilege) // 判断是开启还是关闭特权
{
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
}
else
{
tp.Privileges[0].Attributes = 0;
}
// 调整令牌的特权
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES),
(PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL))
{
std::cout << "AjustTokenPrivileges error: " << GetLastError() << std::endl;
return FALSE;
}
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)
{
std::cout << "The token does not have the specified privilege. \n";
return FALSE;
}
return TRUE;
}
int main(int argc, char const *argv[])
{
DWORD dwPID = 0;
if (argc != 2)
{
std::cout << "\n USAGE : " << argv[0] << "<pid>" << std::endl;
return 1;
}
// change privilege
if (!SetPrivilege(SE_DEBUG_NAME, TRUE))
return 1;
// code injection
dwPID = (DWORD)atol(argv[1]);
InjectCode(dwPID);
return 0;
}
注意,在编译的时候,请使用Release编译选项,如果使用Debug编译选项,得到的注入代码在目标进程中执行的时候会报内存违规访问错误,这个问题后面也会详细分析。
在64位Windows11中的复现
注入程序需要以管理员身份运行
这里展示注入成功的截图:
首先查看notepad的PID:

运行注入程序(注意此处使用的是Release版本的):

注入成功:

复现中遇到的问题
上面提到两个问题:
- ThreadProc大小计算方式问题:笔者的计算方式和原作者的计算方式相反
- 编译选项问题:只有使用Release编译选项编译的注入程序才可以注入成功,使用Debug版本的则不行
第一个问题
这个问题的发现源于代码中WriteProcessMemory返回的错误代码:299,这表示只写入了一部分数据,为什么只写入一部分呢?我怀疑是写入的东西太多了,于是在代码中添加了输出dwSize的代码,发现是一个FFFF开头的数,由于DWORD是unsigned long的类型别名(FFFF开头的dwSize被解释为无符号数的时候是一个很大的数字),说明dwSize是一个负数,所以我调整了计算方式,使计算方式和原作者的相反。
这样确实得到了正确的大小,为什么呢?难道函数的起始地址和函数在源文件中的声明顺序不同?下图是计算dwSize的反汇编代码:

对应源代码,我们发现,rax是ThreadProc的首地址,rcx是InjecCode的首地址,明显的,rax > rcx。在源文件中ThreadProc的定义是在InjectCode之上的。
这是在64位Windows11上的分析结果,如果是原作者的程序呢?

经过验证,原作者的及算方式也是正确的。
在不同的平台上(包括使用不同的编译选项),函数的起始地址并没有绝对的顺序。但具体的规则是什么,我也不清楚。希望大佬解答一下。
在我的平台上,我怀疑编译器对ThreadProc函数进行了内联?貌似只有这样解释得通。本人很菜,如果有知道的希望在评论区留言,谢谢~
第二个问题
对于这个问题,我首先想到的是在Debug编译选项下编译器向可执行程序中添加了一些调试信息。下面的代码是我用于生成注入程序的CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
project(CodeInjection LANGUAGES CXX)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
file(GLOB SOURCES "src/*.cpp")
# 可执行文件
add_executable(${PROJECT_NAME} ${SOURCES})
# 目标链接库
target_link_libraries(${PROJECT_NAME} PRIVATE
kernel32.lib
user32.lib
advapi32.lib
)
# Windows通用定义
target_compile_definitions(${PROJECT_NAME} PRIVATE
UNICODE
_UNICODE
_WIN32_WINNT=0x0A00
)
# 通用编译选项
target_compile_options(${PROJECT_NAME} PRIVATE
/W4
/EHsc
)
# Debug配置特定的编译选项
target_compile_options(${PROJECT_NAME} PRIVATE
"$<$<CONFIG:Debug>:/MDd>"
"$<$<CONFIG:Debug>:/Od>"
"$<$<CONFIG:Debug>:/Zi>"
"$<$<CONFIG:Debug>:/D_DEBUG>"
)
# Release配置特定的编译选项
target_compile_options(${PROJECT_NAME} PRIVATE
"$<$<CONFIG:Release>:/MD>"
"$<$<CONFIG:Release>:/O2>"
"$<$<CONFIG:Release>:/DNDEBUG>"
)
# 链接选项
target_link_options(${PROJECT_NAME} PRIVATE
/SUBSYSTEM:CONSOLE
"$<$<CONFIG:Debug>:/DEBUG>"
"$<$<CONFIG:Release>:/OPT:REF>"
"$<$<CONFIG:Release>:/OPT:ICF>"
)
# 设置输出目录(直接放在build下)
set_target_properties(${PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/Debug"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/Release"
WIN32_EXECUTABLE TRUE
)
使用Debug版的注入程序,CreateRemoteThread是可以正确返回的,但是注入的代码在执行的时候会报一个**内存违规访问的错误。**我使用windbg调试,忘记截图了,错误代码是:Access Violation - Code c0000005。发生错误的地址位于注入代码的那段地址空间中。
总结
纸上得来终觉浅,绝知此事要躬行。