一个简单的桩实现类:
#define JMPCODE_LENGTH 5 //x86 平坦内存模式下,绝对跳转指令长度 #define JMPCMD_LENGTH 1 //机械码0xe9长度 #define JMPCMD 0xe9 //对应汇编的jmp指令 // 一个简化的打桩类的实现 class XSimpleStub { public: explicit XSimpleStub(void* pOrigFunc, void* pNewFunc, bool need_lock_other_thread = false); ~XSimpleStub(); private: // 源函数地址 void * str_func_addr; // 是否打桩成功 bool is_stub_succ; // 是否打桩成功 bool need_lock_other_thread_; // 源指令数据的备份 unsigned char str_instruct_back[JMPCODE_LENGTH]; };
函数就只有两个函数体,分别如下
#include <tlhelp32.h> BOOL LockOtherThread() { DWORD dwCurrPid = GetCurrentProcessId(); DWORD dwCurrTid = GetCurrentThreadId(); HANDLE hThread = NULL; HANDLE hThreadSnap = NULL; THREADENTRY32 te32 = { 0 }; te32.dwSize = sizeof(THREADENTRY32); // 遍历线程 if (Thread32First(hThreadSnap, &te32)) { do { if (te32.th32OwnerProcessID == dwCurrPid) { if (te32.th32ThreadID != dwCurrTid){ // 获取句柄 hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te32.th32ThreadID); if (NULL != hThread){ SuspendThread(hThread); } CloseHandle(hThread); } } } while (Thread32Next(hThreadSnap, &te32)); } CloseHandle(hThreadSnap); return TRUE; } BOOL UnlockOtherThread() { DWORD dwCurrPid = GetCurrentProcessId(); DWORD dwCurrTid = GetCurrentThreadId(); HANDLE hThread = NULL; HANDLE hThreadSnap = NULL; THREADENTRY32 te32 = { 0 }; te32.dwSize = sizeof(THREADENTRY32); // 遍历线程 if (Thread32First(hThreadSnap, &te32)) { do { if (te32.th32OwnerProcessID == dwCurrPid) { if (te32.th32ThreadID != dwCurrTid){ // 获取句柄 hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te32.th32ThreadID); if (NULL != hThread){ ResumeThread(hThread); } CloseHandle(hThread); } } } while (Thread32Next(hThreadSnap, &te32)); } CloseHandle(hThreadSnap); return TRUE; } static void __inner_memcpy(unsigned char* pDest, unsigned char* pSrc, unsigned int count) { while(count > 0) { *pDest++ = *pSrc++; count --; } } XSimpleStub::XSimpleStub(void* pOrigFunc, void* pNewFunc, bool need_lock_other_thread): str_func_addr(pOrigFunc), is_stub_succ(false), need_lock_other_thread_(need_lock_other_thread) { // 源地址、目标地址需要进行一次判定 if (nullptr != pOrigFunc && nullptr != pNewFunc) { DWORD ProtectVar; // 保护属性变量 MEMORY_BASIC_INFORMATION MemInfo; //内存分页属性信息 // 取得对应内存的原始属性 if (0 != VirtualQuery(pOrigFunc, &MemInfo, sizeof(MEMORY_BASIC_INFORMATION))) { // 如果需要锁住所有其他线程,则先执行锁定动作 if (need_lock_other_thread) { LockOtherThread(); } // 修改页面为可写 if(VirtualProtect(MemInfo.BaseAddress, MemInfo.RegionSize, PAGE_READWRITE, &MemInfo.Protect)) { // 备份原数据,防止自身需要使用memcpy,不能使用类似接口 __inner_memcpy((unsigned char*)str_instruct_back, (unsigned char*)pOrigFunc, JMPCODE_LENGTH); // 修改目标地址指令为 jmp pDestFunc *(unsigned char*)pOrigFunc = JMPCMD; //拦截API,在函数代码段前面注入jmp xxx *(DWORD*)((unsigned char*)pOrigFunc + JMPCMD_LENGTH) = (DWORD)pNewFunc - (DWORD)pOrigFunc - JMPCODE_LENGTH; // 改回原属性 VirtualProtect(MemInfo.BaseAddress, MemInfo.RegionSize, MemInfo.Protect, &ProtectVar); // 修改后,还需要刷新cache FlushInstructionCache(GetCurrentProcess(), pOrigFunc, JMPCODE_LENGTH); is_stub_succ = true; } // 如果需要锁住所有其他线程,则先执行锁定动作 if (need_lock_other_thread) { UnlockOtherThread(); } } } } XSimpleStub::~XSimpleStub() { if (is_stub_succ) { DWORD TempProtectVar; //临时保护属性变量 MEMORY_BASIC_INFORMATION MemInfo; //内存分页属性信息 if (0 != VirtualQuery(str_func_addr, &MemInfo, sizeof(MEMORY_BASIC_INFORMATION))) { // 如果需要锁住所有其他线程,则先执行锁定动作 if (need_lock_other_thread_) { LockOtherThread(); } // 修改页面为可写 if(VirtualProtect(MemInfo.BaseAddress,MemInfo.RegionSize, PAGE_READWRITE,&MemInfo.Protect)) { // 恢复代码段 __inner_memcpy((unsigned char*)str_func_addr, (unsigned char*)str_instruct_back, JMPCODE_LENGTH); //改回原属性 VirtualProtect(MemInfo.BaseAddress,MemInfo.RegionSize, MemInfo.Protect,&TempProtectVar); // 修改后,还需要刷新cache FlushInstructionCache(GetCurrentProcess(), str_func_addr, JMPCODE_LENGTH); } // 如果需要锁住所有其他线程,则先执行锁定动作 if (need_lock_other_thread_) { UnlockOtherThread(); } } } }
Linux下一样有类似技术,可以参考IBM的一个文档:
https://www.ibm.com/developerworks/cn/linux/l-knldebug/index.html
这其中的差别在跳转指针的设计上,Linux上,是使用了7个字节,并不需要计算原函数、新函数的地址距离
整个替换流程的实现分为如下几个步骤: (1) 替换指令码: b8 00 00 00 00 /*movl $0, $eax;这里的$0将被具体替换函数的地址所取代*/ ff e0 /*jmp *$eax ;跳转函数*/ 将上述7个指令码存放在一个字符数组中: replace_code[7] (2) 用替换函数的地址覆盖第一条指令中的后面8个0,并保留原来的指令码: memcpy (orig_code, func, 7); /* 保留原函数的指令码 */ *((long*)&replace_code[1])= (long) replace_func; /* 赋替换函数的地址 */ memcpy (func, replace_code, 7); /* 用新的指令码替换原函数指令码 */ (3) 恢复过程用保留的指令码覆盖原函数代码: memcpy (func, orig_code, 7)
Linux下,实现代码页属性修改使用函数接口:mprotect
而相应的Linux下线程挂起、恢复使用如下接口:
#include <signal.h>
pthread_kill(ThreadID, SIGSTOP); // suspend
pthread_kill(ThreadID, SIGCONT); // resume
通过查看/proc/pid/task得知一个任务下的所有线程数