内核态下基于动态感染技术的应用程序执行保护(五 动态感染)
3766 点击·0 回帖
![]() | ![]() | |
![]() | 绝影做尘,铁甲四方逐鹿而争。风起云涌,八面九锡更兼策马奔。南面独傲,铜雀二乔,欲休却报诏到。假节钺,赞拜不名,剑履入殿谁能?青梅煮酒,本初不数,惊起雷霆咋休。玺绶之册,即阼亦重,却累身三秋。观之沧海,星汉一粟,不道身成五彩。隔江叹:景升之子,豚犬之流。 扯远了,赶紧返回。今天这章是这篇系列文章的最后一章。 前面我们的驱动程序已经可以监视进程的创建,下面我们希望做的事情有两点:一、判断进程是不是notepad.exe。二、如果是,我们向其进程注入一段代码并先于它原有的代码执行。所以,这里有三个重要的问题:一、从进程句柄获取进程名;二、向进程分配内存写入自定位的代码;三、修改进程原来的入口点,改为我们代码的入口点,在我们的代码执行完毕后,还得跳转到原来的入口点去。这整个过程本质上与文件感染没多少不同。 如何从进程句柄获取到进程名呢?答案是EPROCESS,ObReferenceObjectByHandle可以让我们通过进程句柄获取它的EPROCESS。微软在WDK中对EPROCESS的说明非常简短:The EPROCESS structure is an opaque structure that serves as the process object for a process.EPROCESS结构中保存有进程名,但不幸的是EPROCESS结构的定义随操作系统的不同而不同,这也许也是为什么微软对它的描述非常少,他也不推荐你使用EPROCESS结构。 在KmdKit中有个SharedEvent – ProcessMon例子,其中演示了通过EPROCESS获取进程名,它的做法是针对不同的系统定义不同的EPROCESS结构,在使用时先获取当前系统。 在《Windows内核安全编程》中提到了另外一种方法:当我们的内核模块DriverEntry被调用时,我们的内核正处于System进程中,我们可以使用PsGetCurrentProcess获取到此时的EPROCESS,在其中暴力搜索“System”,如果搜索出来,我们就可以确定本系统中进程名相对于EPROCESS首地址的偏移,以后就可以用这个偏移加别的进程的EPROCESS首地址来获取别的进程的名了。 后者显然要简洁得多,所以本文也采用了后者的办法。在DriverEntry中增加: invoke GetNameOffset mov g_uNameOffset,eax invoke DbgPrint,$CTA0("Driver entry, name offset:%08X"),g_uNameOffset GetNameOffset proc uses ebx local pProcess local len local dwOffset and dwOffset,0 invoke PsGetCurrentProcess .if eax mov pProcess,eax invoke strlen,$CTA0("System") mov len,eax xor ebx,ebx .while ebx<1024*3*4 mov eax,pProcess add eax,ebx invoke _strnicmp,$CTA0("System"),eax,len .if !eax mov dwOffset,ebx .break .endif inc ebx .endw .endif mov eax,dwOffset ret GetNameOffset endp 这个问题解决了,我们先把HookProc.asm中Hook_NtCreateThread代码贴出来: include Append.asm Hook_NtCreateThread proc ThreadHandle:PHANDLE,DesiredAccess:Dword,ObjectAttributes:POBJECT_ATTRIBUTES,ProcessHandle:HANDLE,ClientId:PCLIENT_ID,ThreadContext:PCONTEXT,InitialTeb:PVOID,CreateSuspended:Dword local pProcess:PVOID local ulEntryPoint:ULONG local dwAllocationSize:Dword local pBaseAddress:PVOID local pProcessName local dwMemorySize:Dword local pAppendStart:PVOID local pOldEntry:PVOID local pAppendEntry:PVOID pushad pushfd .if ThreadContext;;CreateSuspended;;ProcessHandle;;ProcessHandle!=-1 invoke ObReferenceObjectByHandle,ProcessHandle,PROCESS_ALL_ACCESS,NULL,UserMode,addr pProcess,NULL .if eax==STATUS_SUCCESS .if g_uNameOffset mov eax,pProcess add eax,g_uNameOffset mov pProcessName,eax invoke DbgPrint,$CTA0("New process:%s"),pProcessName invoke _stricmp,pProcessName,$CTA0("notepad.exe") .if !eax mov dwMemorySize,APPEND_CODE_LENGTH mov pAppendStart,offset APPEND_CODE_START mov pOldEntry,offset _ulOldEntry mov pAppendEntry,offset _AppendCodeEntry mov edi,ThreadContext assume edi:ptr CONTEXT M2M ulEntryPoint,[edi].regEax ;保存入口点 .if ulEntryPoint invoke KeDetachProcess invoke KeAttachProcess,pProcess ;分配内存 M2M dwAllocationSize,dwMemorySize mov pBaseAddress,NULL invoke ZwAllocateVirtualMemory,NtCurrentProcess,addr pBaseAddress,0,addr dwAllocationSize,MEM_COMMIT,PAGE_EXECUTE_READWRITE .if eax==STATUS_SUCCESS ;写入代码 invoke memcpy,pBaseAddress,pAppendStart,dwMemorySize ;填写Jmp地址 mov eax,pOldEntry sub eax,pAppendStart add eax,pBaseAddress M2M dword ptr [eax],ulEntryPoint ;修正入口点 mov eax,pAppendEntry sub eax,pAppendStart add eax,pBaseAddress mov ulEntryPoint,eax .endif invoke KeDetachProcess M2M [edi].regEax,ulEntryPoint .endif .endif .endif assume edi:nothing invoke ObDereferenceObject,pProcess .endif .endif popfd popad invoke g_lpOldNtCreateThread,ThreadHandle,DesiredAccess,ObjectAttributes,ProcessHandle,ClientId,ThreadContext,InitialTeb,CreateSuspended ret Hook_NtCreateThread endp Append.asm中是我们要感染的代码,等会来看。进程创建时主线程也会被创建。但在我们的Hook中,线程还没有开始运行。其中ThreadContext(这个结构跟用户态下用GetThreadContext获取到的差不多)的regEax就是主线程的起始地址,也就是我们常说的入口点。代码的思路非常简单,在目标进程中分配一块APPEND_CODE_LENGTH大小的内存(这个大小刚好够装下我们需要注入的代码);把我们程序中APPEND_CODE_START地址拷贝那么多数据过去(这其实就是把我们的感染代码拷贝过去;把进程原来的入口点保存到拷贝过去的那份代码的某个位置(具体那个位置看Append.asm就明白了),吧ThreadContext.regEax设置为我们注入代码的起始地址。 这下来看Append.asm APPEND_CODE_START equ this byte include proto.inc include Append.inc _ulOldEntry dd ? _hKernel32 dd ? _hUser32 dd ? _szUser32 db 'User32.dll',0 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> _lpGetProcaddress APIGetProcAddress ? _lpGetModuleHandleA APIGetModuleHandle ? _lpLoadLibraryA APILoadLibrary ? ;------------------------------------------------------------------------------- _szGetProcAddress db 'GetProcAddress',0 _szGetModuleHandleA db 'GetModuleHandleA',0 _szLoadLibraryA db 'LoadLibraryA',0,0 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> _lpMessageBoxA APIMessageBox ? ;------------------------------------------------------------------------------ _szMessageBoxA db 'MessageBoxA',0,0 _lpTitle db 'DynamicHook',0 _lpMsg db 'My append code!',0 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> include GetKernel.asm ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> _AppendCodeEntry: call @F @@: pop ebx sub ebx,offset @B .if dword ptr [esp]>7FFFFFFFh jmp OLD_ENTRY .endif invoke _GetKernelBase,[esp] .if !eax jmp OLD_ENTRY .endif mov [ebx+_hKernel32],eax invoke _GetApi,[ebx+_hKernel32],addr [ebx+offset _szGetProcAddress] .if !eax jmp OLD_ENTRY .endif mov [ebx+_lpGetProcAddress],eax lea esi,[ebx+offset _szGetModuleHandleA] lea edi,[ebx+offset _lpGetModuleHandleA] .while TRUE invoke [ebx+_lpGetProcAddress],[ebx+_hKernel32],esi .if !eax jmp OLD_ENTRY .endif mov [edi],eax add edi,4 @@: lodsb or al,al jnz @B .break .if ! byte ptr [esi] .endw invoke [ebx+_lpGetModuleHandleA],addr [ebx+_szUser32] .if !eax invoke [ebx+_lpLoadLibraryA],addr [ebx+_szUser32] .if !eax jmp OLD_ENTRY .endif .endif mov [ebx+_hUser32],eax lea esi,[ebx+offset _szMessageBoxA] lea edi,[ebx+offset _lpMessageBoxA] .while TRUE invoke [ebx+_lpGetProcAddress],[ebx+_hUser32],esi .if !eax jmp OLD_ENTRY .endif mov [edi],eax add edi,4 @@: lodsb or al,al jnz @B .break .if ! byte ptr [esi] .endw invoke [ebx+_lpMessageBoxA],NULL,addr [ebx+_lpMsg],addr [ebx+_lpTitle],0 OLD_ENTRY: jmp [ebx+_ulOldEntry] APPEND_CODE_END equ this byte APPEND_CODE_LENGTH equ offset APPEND_CODE_END-offset APPEND_CODE_START 这下大家明白APPEND_CODE_LENGTH、APPEND_CODE_START、_AppendCodeEntry、_ulOldEntry的含义了吧。在这里汇编语言的好处就显而易见了:可以精确获取指定代码段的长度(你若用高级语言的话,就得估摸着分配一块足够大的内存);可以使用相对于ShellCode来说稍微高级一点的语言写代码(ShellCode要想完成复杂一点的功能还相当麻烦呢)。 这里要注意的是,首先对所有用户态API的调用我们都不能直接调用,一是内核的导入库中根本没有提供这些函数,二是我们访问全局变量不能直接来访问,因为随着进程不同,我们注入到目标进程的代码起始地址也是不同的(分配的pBaseAddress不同)。这需要我们手动加载需要的DLL,获取API函数地址,并且编写自定位代码。这些技术早几年前就已经科普,这里就不再科普了。 GetKernel.asm中的_GetKernelBase用来获取kernel32.dll的基址。方法也有很多,这里直接用的《Windows环境下32位会变语言程序设计》中的代码。 现在来试运行一下: ![]() 哈哈!当我们启动notepad.exe时首先会弹出我们的对话框。如果你用OllyDBG调试一下notepad.exe,选“设置第一次暂停于:主模块入口点”,你会发现,当OllyDBG中断时,我们的代码已经执行过了。notepad.exe根本就没有机会知道我们的代码已经影响了它。 但是!任何事情就怕但是!notepad.exe在后面还有机会来扫描内存…….如果我们在 OLD_ENTRY: jmp [ebx+_ulOldEntry] 处用下面的办法呢?(伪代码) push 8000 push APPEND_CODE_LENGTH push offset [ebx+APPEND_CODE_START] push dword ptr [ebx+_ulOldEntry] jmp [ebx+_lpVirtualFree] 这种动态感染的办法后面还有后话,我们的代码虽然很早就执行了,但系统创建进程时,还得先运行很多代码,OllyDBG仍然可以中断在系统断点上。可是,我们也不应该把别人憋死。给别人留条活路就是给自己留条活路。 | |
![]() | ![]() |