基于应用层自身反远程线程注入的研究 现状: 目前所有已知的反远程注入方式: r0层hook 句柄的获取,返回失败,让应用层注入者拿不到目标进程的句柄,如hook ntopenprocess ntdublicatehandle R0层监控 线程创建,比较当前进程句柄 和ntcreatethread 注入进程句柄是否相同,如果不同则判定为 注入行为 R0层 对常规注入线程的threadproc 的地址进行hook ,如果 进程调用ldrLoadLibrary等系列函数,则表示有远程注入行为。 R3层,防止自身被注入,在自己进程里面hook loadlibrary ,这是基于远程注入点的防守,而不是基于线程的防守。 R3层,全局hook 所有进程中的ntcreatethread, 此方式相当于 r0 层的 ntcreatethread ,堵住远程线程调用的源头来实施远程线程的注入,对于防止和监控自身进程被远程注入是一种非常浪费的方式。 还有其他的一些方式 ,总体上分两种 第一种是从源头上监控线程的创建,然后去判断是否是远程注入,第二种是从threadproc 调用的方式来达到监控和防守。上面的几种方式对于防止自身被注入,虽然某些方式 比如r0层和r3层 全局hook 线程调用者,比较有效果,但动作都非常大,并不是一种很轻型的,很易于处理的方式。 下面,我将介绍一种简单可行而且效果明显的方法,来达到自身进程被注入,此方式不依靠第三方 程序和全局hook,以及r0层的驱动监控,稳定可靠易于重用。 技术理论: 为了防止远程注入,首先得从远程线程这个方式入手,而不应该从 threadproc 来监控,因为threadproc 不一定是普通的loadlibrary ,可能是一个写入的地址调用。 线程:自身线程创建的过程 Createthread --> ntcreatethread --> resumethread --> 最后线程切换以后,threadproc调用 远程线程的运行: Createthread --> ntcreatethread --> 交给驱动 ,让驱动给被注入进程 起一个线程 -->然后调用 threadproc 这里可以看出 整个系统调用的流程是不同的: 对于自身的进程 ,先准备一份白名单 hook NtResumeThread 存一份 <tid,threadproc>表,在这里去hook 是为了保证 我们的线程是真正创建成功,是存活的才有意义。 对于远程线程 ,只能跟踪 最原始的threadproc 地方 去hook,来获取tid,和白名单对比。 远程线程入口点分析: 为了在一个靠谱的threadproc 那里做到最开始的hook,要的是线程初始化堆栈(堆栈非常干净)的时候地方,而且线程一定会来到的地方,这样才能保证线程功能代码没有跑起来,对他来做否决,否则如果远程恶意线程起来了,监控到了 意义也不是很大。 手上只有win2000--win7 x86版本,通过分析和堆栈回溯 ,可以截获到很统一的位置,分析过程以及位置如下: 重点讲跟踪win2003的过程,其他方式一样包括x64: 首先断下:threadproc 地方,第一次回溯地址: Win2003: 7C8247F5 6A 0C push 0C 7C8247F7 68 3048827C push 7C824830 7C8247FC E8 08D2FFFF call 7C821A09 7C824801 8365 FC 00 and dword ptr [ebp-4], 0 7C824805 64:A1 18000000 mov eax, dword ptr fs:[18] ;异常装入 7C82480B 8178 10 001E000>cmp dword ptr [eax+10], 1E00 7C824812 75 0F jnz short 7C824823 7C824814 803D 08B0887C 0>cmp byte ptr [7C88B008], 0 7C82481B 75 06 jnz short 7C824823 7C82481D FF15 2813807C call dword ptr [<&ntdll.CsrNewThread>>; ntdll.CsrNewThread ;通知系统 线程起来通知 7C824823 FF75 0C push dword ptr [ebp+C] 7C824826 FF55 08 call dword ptr [ebp+8] ;调用地址 7C824829 50 push eax 7C82482A E8 C2B0FEFF call ExitThread ;线程退出地址,标志是否跟对 7C82482F CC int3 上面步骤的行为是 异常进栈,通知系统线程起来,调用threadproc ,运行完毕 退出线程 7C8247F5 到这里需要回溯 ,但 堆栈是没有 call 7C8247F5 ,搜索 jmp 7C8247F5 ,即可找到 唯一跳转处: 7C8217EC 33ED xor ebp, ebp 7C8217EE 53 push ebx -------> lParam 7C8217EF 50 push eax ----->threadproc 7C8217F0 6A 00 push 0 7C8217F2 E9 FE2F0000 jmp 7C8247F5 这段代码是 用线程context上下文来构建马上线程所需要的堆栈,也就是初始化的地方 看看 在 7C8217EE 此处堆栈的状况 此时最初的堆栈: 0144FFF0 00000000 ;esp 看到了吗? 很干净的堆栈,context 里面 eax == threadproc, ebx == lParam,和系统规定的上线文行为是一致的。 到此为止找到了最原始干净的地方,从 //xp 2003 以及以下的版本 Winxp: 7C810729 33ED xor ebp, ebp 7C81072B 53 push ebx ;ebx == lParam 7C81072C 50 push eax ;eax == proc 7C81072D 6A 00 push 0 7C81072F ^ E9 BEAFFFFF jmp 7C80B6F2 7C80B6F2 6A 10 push 10 7C80B6F4 68 30B7807C push 7C80B730 7C80B6F9 E8 D86DFFFF call 7C8024D6 7C80B6FE 8365 FC 00 and dword ptr [ebp-4], 0 7C80B702 64:A1 18000000 mov eax, dword ptr fs:[18] 7C80B708 8945 E0 mov dword ptr [ebp-20], eax 7C80B70B 8178 10 001E000>cmp dword ptr [eax+10], 1E00 7C80B712 75 0F jnz short 7C80B723 7C80B714 803D 0850887C 0>cmp byte ptr [7C885008], 0 7C80B71B 75 06 jnz short 7C80B723 7C80B71D FF15 F812807C call dword ptr [<&ntdll.CsrNewThread>>; ntdll.CsrNewThread 7C80B723 FF75 0C push dword ptr [ebp+C] 7C80B726 FF55 08 call dword ptr [ebp+8] ; threadproc 7C80B729 50 push eax 7C80B72A E8 C9090000 call ExitThread 看看//08 ,vista ,win7 以上 的版本 主要拿win7 来看看 Win7: 76F83733 8BFF mov edi, edi ; 标准的线程起始点,构建参数,以及异常链表 76F83735 55 push ebp 76F83736 8BEC mov ebp, esp 76F83738 51 push ecx 76F83739 51 push ecx 76F8373A 8D45 F8 lea eax, dword ptr [ebp-8] 76F8373D 50 push eax 76F8373E E8 49FFFFFF call RtlInitializeExceptionChain 76F83743 FF75 0C push dword ptr [ebp+C] 76F83746 FF75 08 push dword ptr [ebp+8] 76F83749 E8 06000000 call 76F83754 ; basethreadstartup 76F8374E CC int3 此处是有变化的 没有 直接call threadproc , 而是在 call 76F83754 调用线程入口,对于分析没有太大影响流程一样 ,标准的线程起始点,构建参数,以及异常链表,找最原始的地方 win7---最终调用 初始化 线程堆栈的地方 77067076 8BFF mov edi, edi ;这条没用 77067078 > 894424 04 mov dword ptr [esp+4], eax ;eax == proc 7706707C 895C24 08 mov dword ptr [esp+8], ebx ;ebx = lParam 77067080 E9 AEC60100 jmp 77083733 看看此时的堆栈: 此时最初的堆栈: 0144FFF0 00000000 ;esp 0144FFF4 00000000 ;准备用 0144FFF8 00000000 ;准备用 0144FFFC 00000000 ;reserved 一样干净 mov dword ptr [esp+4], eax ,没有push 而已 *******注意 断点处 不要短此处77067076 8BFF mov edi, edi 这条系统根本不会调用,不注意可能跑飞。 Win2008: 777A5E56 8BFF mov edi, edi ;没用 777A5E58 > 894424 04 mov dword ptr [esp+4], eax 777A5E5C 895C24 08 mov dword ptr [esp+8], ebx 777A5E60 ^ E9 0EBBFDFF jmp 77781973 0012FFF0 00000000 0012FFF4 00000000 0012FFF8 00000000 0012FFFC 00000000 77781973 8BFF mov edi, edi 77781975 55 push ebp 77781976 8BEC mov ebp, esp 77781978 51 push ecx 77781979 51 push ecx 7778197A 8D45 F8 lea eax, dword ptr [ebp-8] 7778197D 50 push eax 7778197E E8 D5FFFFFF call RtlInitializeExceptionChain 77781983 FF75 0C push dword ptr [ebp+C] 77781986 FF75 08 push dword ptr [ebp+8] 77781989 E8 06000000 call 77781994
761BD0D7 > 8BFF mov edi, edi ;hook 761BD0D9 55 push ebp 761BD0DA 8BEC mov ebp, esp 761BD0DC 85C9 test ecx, ecx 761BD0DE ^ 0F85 D6D6FDFF jnz 7619A7BA 761BD0E4 FF75 08 push dword ptr [ebp+8] 761BD0E7 FFD2 call edx 761BD0E9 50 push eax 761BD0EA FF15 08101776 call dword ptr [<&ntdll.RtlExitUserTh>; ntdll.RtlExitUserThread Vista: vista 的初始化也是这个特征---最终调用 初始化 线程堆栈的地方 77067076 8BFF mov edi, edi ;这条没用 77067078 > 894424 04 mov dword ptr [esp+4], eax ;eax == proc 7706707C 895C24 08 mov dword ptr [esp+8], ebx ;ebx = lParam 77067080 E9 AEC60100 jmp 77083733 *************需要2003 和xp 此初始化的地方位于 kernel32 空间中,vista 和win7 2008 位于ntdll 空间中。 只需在最初的地方选择一个合适地方hook 自己一下 就可以截获所有存活线程的动作,分析到此为止。 核心代码实现: 制作hook: 最大兼容hook制作思路 ,首先搜索最开始处的opcode: //2003 和xp 特征码是一样的 BYTE bOpcodeXp[] = {0x33,0xed,0x53,0x50,0x6a,0x00,0xe9}; /* 7C810729 33ED xor ebp, ebp 7C81072B 53 push ebx ;ebx == lParam 7C81072C 50 push eax ;eax == proc 7C81072D 6A 00 push 0 7C81072F ^ E9 BEAFFFFF jmp 7C80B6F2 */ //vista 和win7 2008 ,跳入后的运行不一样,所以选择此处作为特征起点搜索,将后面的跳转模拟成一个函数,反正系统给我放了参数 BYTE bOpcodeWin7[] = {0x8b,0xff,0x89,0x44,0x24,0x04,0x89,0x5c,0x24,0x08,0xe9}; /* 77067076 8BFF mov edi, edi ;这条没用 77067078 > 894424 04 mov dword ptr [esp+4], eax ;eax == proc 7706707C 895C24 08 mov dword ptr [esp+8], ebx ;ebx = lParam 77067080 E9 AEC60100 jmp 77083733 */ //win8 手上的pro版本还未装 关键搜索代码: //获取text 段的起始地址,以及执行段的大小 BOOL GetTextSectionInfo(__in HMODULE hMod,__inout PULONG pUlSize,__inout PULONG pUlTextAddress) { BOOL bRet = FALSE; ULONG ulSectionNum = 0; PIMAGE_SECTION_HEADER pStFirstSectionHeader = NULL; PIMAGE_DOS_HEADER pStDosHeader = NULL; PIMAGE_NT_HEADERS pStNtHeaders = NULL; do { pStDosHeader = (PIMAGE_DOS_HEADER)hMod; pStNtHeaders = (PIMAGE_NT_HEADERS)((DWORD)pStDosHeader + pStDosHeader->e_lfanew); ulSectionNum = pStNtHeaders->FileHeader.NumberOfSections; pStFirstSectionHeader = IMAGE_FIRST_SECTION(pStNtHeaders); for (ULONG ulIndex = 0;ulIndex < ulSectionNum;ulIndex++) { if (0 == lstrcmpiA((PCHAR)pStFirstSectionHeader->Name,".text")) { *pUlSize = pStFirstSectionHeader->Misc.VirtualSize; *pUlTextAddress = pStFirstSectionHeader->VirtualAddress + (DWORD)hMod; bRet = TRUE; break; }
pStFirstSectionHeader++; } } while (FALSE); return bRet; } //获取正确的opcode 特征码 ULONG SearchSigCode(__in HMODULE hMod,__in PBYTE bOpcode,__in ULONG ulLen) { ULONG ulTargeAddress = 0; ULONG ulStartAddress = 0; ULONG ulEndAddress = 0; ULONG ulSize = 0; do { if (FALSE == GetTextSectionInfo(hMod,&ulSize,&ulStartAddress)) { xDebugW((L"GetTextSectionInfo error \n")); break; } for (ULONG ulIndex = 0;ulIndex < ulSize;ulIndex++) { if (0 == memcmp((PVOID)ulStartAddress,bOpcode,ulLen)) { ulTargeAddress = ulStartAddress; xDebugW((L"The Targe Address is 0x%x \n",ulTargeAddress)); break; } ulStartAddress = ulStartAddress + 1; } } while (FALSE); return ulTargeAddress; } 观察堆栈 我们将 jmp 后面的目的地址模拟成一个函数调用 ,所有的版本堆栈都很靠谱,伪造函数声明: typedef VOID (WINAPI* LPFN_BaseThreadStartup)(__in ULONG ulProc,__in LPVOID lParam); 获取 伪造函数的地址: //获取正确的函数入口点: LPFN_BaseThreadStartup GetImitateFunctionBySigCode(__in ULONG ulSigCode,__in ULONG ulOffsetJmp) { ULONG ulJmpLen = 0; LPFN_BaseThreadStartup lpfn_BaseThreadStartup = NULL; PBYTE pTempAddr = NULL; do { pTempAddr =(PBYTE)(ulSigCode); pTempAddr = (PBYTE)pTempAddr + ulOffsetJmp; if (0xE9 != *pTempAddr) { xDebugW((L"e9 not correct \n")); break; } pTempAddr = pTempAddr +1; lpfn_BaseThreadStartup = (LPFN_BaseThreadStartup)(*((ULONG *)pTempAddr) + pTempAddr + 4); xDebugW((L"the call is 0x%x \n",(DWORD)lpfn_BaseThreadStartup)); } while (FALSE); return lpfn_BaseThreadStartup; } 模块hook: pStOsVersion = LibGetOSVersion(); if (NULL == pStOsVersion) { xDebugW((L"LibGetOSVersion error \n")); break; } hNtdll = GetModuleHandle(L"ntdll.dll"); if (NULL == hNtdll) { xDebugW((L"GetModuleHandle error \n")); break; } hKernel32 = GetModuleHandle(L"Kernel32.dll"); if (NULL == hKernel32) { xDebugW((L"GetModuleHandle Kernel32 error \n")); break; } if (LIB_OS_XP == pStOsVersion->OSType || LIB_OS_2003 == pStOsVersion->OSType) { //xp 2003 以下版本 //版本兼容 ulTargeAddress = SearchSigCode(hKernel32,bOpcodeXp,7); if (0 == ulTargeAddress) { xDebugW((L"SearchSigCode xp error \n")); break; } lpfn_BaseThreadStartupAddr = GetImitateFunctionBySigCode(ulTargeAddress,6); if (NULL == lpfn_BaseThreadStartupAddr) { xDebugW((L"SearchSigCode GetImitateFunctionBySigCode xp error \n")); break; } } else if (LIB_OS_2008 == pStOsVersion->OSType || LIB_OS_VISTA == pStOsVersion->OSType || LIB_OS_7 == pStOsVersion->OSType) { //08 ,vista ,win7 以上 ulTargeAddress = SearchSigCode(hNtdll,bOpcodeWin7,11); if (0 == ulTargeAddress) { xDebugW((L"SearchSigCode win7 error \n")); break; } lpfn_BaseThreadStartupAddr= GetImitateFunctionBySigCode(ulTargeAddress,10); if (NULL == lpfn_BaseThreadStartupAddr) { xDebugW((L"SearchSigCode GetImitateFunctionBySigCode error \n")); break; } } else { //现在还不支持win8 pro 不在手上 xDebugW((L"version not support\n")); break; } //自己伪造一份函数来达到通用目的 phiBaseThreadStartup=LibInlineHook(GetCurrentProcess(),(PVOID)lpfn_BaseThreadStartupAddr,(PVOID)Fake_BaseThreadStartup); lpfn_BaseThreadStartup=(LPFN_BaseThreadStartup)(PBYTE)phiBaseThreadStartup->bJumpToOrigin; 到Fake_BaseThreadStartup实现: //入口fake VOID WINAPI Fake_BaseThreadStartup(__in ULONG ulProc,__in LPVOID lParam) { ULONG ulTid = 0; do { ulTid = GetCurrentThreadId(); //关键的地方判断 EnterCriticalSection(&StCriticalSection); //查询是否是白名单tid if (FALSE == NodeIsInTable(g_pThreadMap,ulTid)) { xDebugW((L"remote thread ------the tid is %d, the proc is 0x%x, the param is 0x%x",ulTid,ulProc,lParam)); //关闭 /*LeaveCriticalSection(&StCriticalSection); ExitThread(1);*/ } LeaveCriticalSection(&StCriticalSection); } while (FALSE); lpfn_BaseThreadStartup(ulProc,lParam); } 白名单构建: lpfn_NtQueryInformationThread = (LPFN_NtQueryInformationThread)GetProcAddress(hNtdll,"NtQueryInformationThread"); lpfnNtResumeThreadAddr = (LPFN_NtResumeThread)GetProcAddress(hNtdll,"NtResumeThread"); if (NULL == lpfnNtResumeThreadAddr || NULL == lpfn_NtQueryInformationThread) { xDebugW((L"GetProcAddress error \n")); break; } pNtResumeInfo = LibInlineHook(GetCurrentProcess(),(PVOID)lpfnNtResumeThreadAddr,(PVOID)Fake_NtResumeThread); lpfn_NtResumeThread = (LPFN_NtResumeThread)(PBYTE)pNtResumeInfo->bJumpToOrigin; lpfnNtTerminateThreadAddr = (LPFN_NtTerminateThread)GetProcAddress(hNtdll,"NtTerminateThread"); if (NULL == lpfnNtTerminateThreadAddr) { xDebugW((L"GetProcAddress :NtTerminateThreaderror \n")); break; } phiNtTerminateThread = LibInlineHook(GetCurrentProcess(),(PVOID)lpfnNtTerminateThreadAddr,(PVOID)Fake_NtTerminateThread); lpfn_NtTerminateThread = (LPFN_NtTerminateThread)(PBYTE)phiNtTerminateThread->bJumpToOrigin; NTSTATUS NTAPI Fake_NtResumeThread ( IN HANDLE ThreadHandle, OUT PULONG SuspendCount ) { NTSTATUS NtStatus = STATUS_SUCCESS; THREAD_BASIC_INFORMATION *pStThreadBasicInfor = NULL; ULONG ulThreadStartAddr = 0; ULONG ulRet = 0; ULONG ulTid = 0; do { xDebugW((L"thread resume \n")); pStThreadBasicInfor = (THREAD_BASIC_INFORMATION *)xAlloc(sizeof(THREAD_BASIC_INFORMATION)); if (NULL == pStThreadBasicInfor) { xDebugW((L"xAlloc(sizeof(THREAD_BASIC_INFORMATION)) error \n")); break; } NtStatus = lpfn_NtQueryInformationThread(ThreadHandle,ThreadBasicInformation,pStThreadBasicInfor,sizeof(THREAD_BASIC_INFORMATION),&ulRet); if (!NT_SUCCESS(NtStatus)) { xDebugW((L"NtQueryInformationThread ThreadBasicInformation error \n")); break; } ulTid = (ULONG)pStThreadBasicInfor->ClientId.UniqueThread; NtStatus = lpfn_NtQueryInformationThread(ThreadHandle,ThreadQuerySetWin32StartAddress,&ulThreadStartAddr,sizeof(ULONG),&ulRet); if (!NT_SUCCESS(NtStatus)) { xDebugW((L"NtQueryInformationThread ThreadQuerySetWin32StartAddress error \n")); break; } xDebugW((L"the thread id is %d ---- the thread entry proc is 0x%x",ulTid,ulThreadStartAddr)); EnterCriticalSection(&StCriticalSection); if (TRUE == NodeIsInTable(g_pThreadMap,ulTid)) { LeaveCriticalSection(&StCriticalSection); break; } //插入记录 InsertNode(g_pThreadMap,ulTid,ulThreadStartAddr); LeaveCriticalSection(&StCriticalSection); } while (FALSE); if (NULL != pStThreadBasicInfor) { xFree(pStThreadBasicInfor); } NtStatus = lpfn_NtResumeThread(ThreadHandle,SuspendCount); return NtStatus; } // 正常轨迹的线程退出,清理节点 NTSTATUS NTAPI Fake_NtTerminateThread( IN HANDLE ThreadHandle, IN NTSTATUS ExitStatus ) { NTSTATUS NtStatus = STATUS_SUCCESS; THREAD_BASIC_INFORMATION *pStThreadBasicInfor = NULL; ULONG ulTid = 0; ULONG ulRet = 0; do { pStThreadBasicInfor = (THREAD_BASIC_INFORMATION *)xAlloc(sizeof(THREAD_BASIC_INFORMATION)); if (NULL == pStThreadBasicInfor) { xDebugW((L"xAlloc(sizeof(THREAD_BASIC_INFORMATION)) error \n")); break; } NtStatus = lpfn_NtQueryInformationThread(ThreadHandle,ThreadBasicInformation,pStThreadBasicInfor,sizeof(THREAD_BASIC_INFORMATION),&ulRet); if (!NT_SUCCESS(NtStatus)) { xDebugW((L"NtQueryInformationThread ThreadBasicInformation error :%d \n",GetLastError())); break; } ulTid = (ULONG)pStThreadBasicInfor->ClientId.UniqueThread; EnterCriticalSection(&StCriticalSection); if (TRUE == NodeIsInTable(g_pThreadMap,ulTid)) { //清理掉 CleanOneNode(g_pThreadMap,ulTid); } LeaveCriticalSection(&StCriticalSection); } while (FALSE); if (NULL != pStThreadBasicInfor) { xFree(pStThreadBasicInfor); } return lpfn_NtTerminateThread(ThreadHandle,ExitStatus); } 白名单简述:构建一张表 table 记录 ,对照查询即可。 目前我将防护放在一份dll里面,任意程序load ThreadAttachDll.dll 即可达到反远程线程注入目的。 运行效果: