基于应用层自身反远程线程注入的研究
现状:
  目前所有已知的反远程注入方式:
  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 即可达到反远程线程注入目的。 运行效果:

 

posted on 2013-08-08 10:47  5t4rk  阅读(1874)  评论(0编辑  收藏  举报