注入代码的方式比较
注入shellcode
优点:
1. 简单,只需要EXE的一部分。代码可以用C\C++或汇编写
缺点:
1. 要写位置无关代码,这意味着不能直接使用全局变量、其他编译单元的函数(包括CRT的memcpy
)、API等。如果要使用则要由源进程分配空间、计算API在目标进程的地址,并传到目标进程的shellcode。或者shellcode自己计算LoadLibrary
和GetProcAddress
的地址也行
2. 没有符号文件,难以调试
注入DLL
建议没什么特殊需要都优先选择注入DLL
优点:
1. 代码没什么限制
2. 调试方便,VS可以直接在源代码下断点,附加到目标进程,注入DLL后可以正常调试
缺点:
1. 程序要带上一个DLL。其实我觉得不算缺点,大不了把DLL放到EXE资源里,要注入时释放到一个临时目录
2. 容易被检测到注入,不是干坏事的话也可以忽略这点
注入EXE
优点:
1. 只需要EXE,少一个DLL文件。有些游戏补丁和修改器是这么干的
2. 跟shellcode比起来,因为自带重定位表、IAT等东西,可以不写位置无关代码,只要修复了重定位和IAT就可以正常运行大部分代码。而且操作系统在载入源进程时已经做了IAT修复,假设kernel32.dll
模块在每个进程加载地址一样,就可以在目标进程修复IAT之前直接调用部分API
缺点:
1. 没有符号文件,难以调试
2. 不能依赖于全局变量的初始状态,这意味着不能使用静态链接的CRT,因为CRT的很多函数依赖于全局变量的初始值(比如malloc
)
注入EXE的方法
前置知识,这里面提到的不再细讲:
1. 注入DLL,这里用到远线程注入
2. 加载PE文件,用到PE文件的结构和修复重定位、IAT
完整源码:InjectExe
1. 把整个EXE和需要的变量写入目标进程
typedef int(* RemoteCallbackType)();
// 需要传到目标进程的变量
struct InjectionContext
{
LPVOID imageBase; // 目标进程中EXE的地址
uintptr_t offset; // 目标进程中EXE的地址 - 源进程中EXE的地址,用来做重定位
RemoteCallbackType callback; // 注入完毕后在目标进程调用的回调
};
// 注入EXE到process,然后在目标进程调用callback
// callback必须返回0,否则视为注入失败
// 如果注入成功则返回EXE在目标进程的地址,否则返回NULL
LPVOID InjectExe(HANDLE process, RemoteCallbackType callback)
{
auto dosHeader = (PIMAGE_DOS_HEADER)GetModuleHandle(NULL);
auto ntHeader = PIMAGE_NT_HEADERS((uintptr_t)dosHeader + dosHeader->e_lfanew);
auto imageBase = (LPVOID)dosHeader;
SIZE_T imageSize = ntHeader->OptionalHeader.SizeOfImage;
LPVOID remoteImageBase = NULL;
LPVOID remoteCtx = NULL;
HANDLE remoteThread = NULL;
try
{
// 优先在当前的imageBase分配地址,这样就不用重定位,分配失败则选择其他地址
remoteImageBase = VirtualAllocEx(process, imageBase, imageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (remoteImageBase == NULL)
{
remoteImageBase = VirtualAllocEx(process, NULL, imageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);