以文本方式查看主题 - 计算机科学论坛 (http://bbs.xml.org.cn/index.asp) -- 『 C/C++编程思想 』 (http://bbs.xml.org.cn/list.asp?boardid=61) ---- 挂钩Windows API (http://bbs.xml.org.cn/dispbbs.asp?boardid=61&rootid=&id=44122) |
-- 作者:卷积内核 -- 发布时间:3/20/2007 9:19:00 AM -- 挂钩Windows API ===========================[ 挂钩Windows API ]================== SoBeIt Author: Holy_Father <holy_father@phreaker.net> 1. 内容 这篇文章是有关在OS Windows下挂钩API函数的方法。所有例子都在基于NT技术的Windows版本NT 4.0及以上有效(Windows NT 4.0, Windows 2000, Windows XP)。可能在其它Windows系统也会有效。 一般来说我们的目的是用我们的代码取代一些函数里的代码。这些问题有时可以在进程运行前解决。这些大多数时候可以用我们运行的用户级进程来完成,目的可以是修改程序的行为。举个例子应用程序的破解,比方说有些程序会在启动时需要原光盘,我们想要不用光盘就启动它。如果我们修改获取驱动类型的函数我们就可以让程序从硬盘启动。 这里修改我们想要修改函数来自的物理模块(大多数时候是.exe或.dll)。在这里我们至少有3种可能的做法。 在运行前挂钩通常都非常特殊,并且是在内部面向具体的应用程序(或模块)。如果我们更换了kernel32.dll或ntdll.dll里的函数(只在NT操作系统里),我们就能完美地做到在所有将要运行的进程中替换这个函数。但说来容易做起来却非常难,因为我们不但得考虑精确性和需要编写比较完善的新函数或新模块,但主要问题是只有将要运行的进程才能被挂钩(要挂钩所有进程只能重启电脑)。另一个问题是如何进入这些文件,因为NT操作系统保护了它们。比较好的解决方法在进程正在运行时挂钩。这需要更多的有关知识,但最后的结果相当不错。在运行中挂钩只对能够写入它们的内存的进程能成功。为了能写入它自己我们使用API函数WriteProcessMemory。现在我们开始运行中挂钩我们的进程。 这里有很多种可能性。首先介绍如何用改写IAT挂钩函数的方法。接下来这张图描述了PE文件的结构: +-------------------------------+ - offset 0 这里对我们比较重要的是.idata部分的导入地址表(IAT)。这个部分包含了导入的相关信息和导入函数的地址。有一点很重要的是我们必须知道PE文件是如何创建的。当在编程语言里间接调用任意API(这意味着我们是用函数的名字来调用它,而不是用它的地址),编译器并不直接把调用连接到模块,而是用jmp指令连接调用到IAT,IAT在系统把进程调入内存时时会由进程载入器填满。这就是我们可以在两个不同版本的Windows里使用相同的二进制代码的原因,虽然模块可能会加载到不同的地址。进程载入器会在程序代码里调用所使用的IAT里填入直接跳转的jmp指令。所以我们能在IAT里找到我们想要挂钩的指定函数,我们就能很容易改变那里的jmp指令并重定向代码到我们的地址。完成之后每次调用都会执行我们的代码了。这种方法的缺点是经常有很多函数要被挂钩(比方说如果我们要在搜索文件的API中改变程序的行为我们就得修改函数FindFirstFile和FindNextFile,但我们要知道这些函数都有ANSI和WIDE版本,所以我们不得不修改FindFirstFileA、FindFirstFileW、FindNextFileA和FileNextFileW的IAT地址。但还有其它类似的函数如FindFirstFileExA和它的WIDE版本FindFirstFileExW,也都是由前面提到的函数调用的。我们知道FindFirstFileW调用FindFirstFileExW,但这是直接调用,而不是使用IAT。再比如说ShellAPI的函数SHGetDesktopFolder也会直接调用FindFirstFilwW或FindFirstFileExW)。如果我们能获得它们所有,结果就会很完美。 PVOID ImageDirectoryEntryToData( 在这里Base参数可以用我们程序的Instance(Instance通过调用GetModuleHandle获得): hInstance = GetModuleHandleA(NULL); DirectoryEntry我们可以使用恒量IMAGE_DIRECTORY_ENTRY_IMPORT。 #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 函数的结果是指向第一个IAT记录指针。IAT的所有记录是由IMAGE_IMPORT_DESCRIPTOR定义的结构。所以函数结果是指向IMAGE_IMPORT_DESCRIPTOR的指针。 typedef struct _IMAGE_THUNK_DATA { typedef struct _IMAGE_IMPORT_DESCRIPTOR { IMAGE_IMPORT_DESCRIPTOR里的Name成员变量是模块名字的指针。如果我们想要挂钩某个函数比如是来自kernel32.dll我们就在导入表里找属于名字kernel32.dll的描述符号。我们先调用ImageDirectoryEntryToData然后找到名字是"kernel32.dll"的描述符号(可能不只一个描述符号是这个名字),最后我们在这个模块的记录里所有函数的列表里找到我们想要的函数(函数地址通过GetProcAddress函数获得)。如果我们找到了就必须用VirtualProtect函数来改变内存页面的保护属性,然后就可以在内存中的这些部分写入代码了。在改写了地址之后我们要把保护属性改回来。在调用VirtualProtect之前我们还要先知道有关页面的信息,这通过VirtualQuery来实现。我们可以加入一些测试以防某些函数会失败(比方说如果第一次调用VirtualProctect就失败了,我们就没办法继续)。 PCSTR pszHookModName = "kernel32.dll",pszSleepName = "Sleep"; ULONG ulSize; while (pImportDesc->Name) PIMAGE_THUNK_DATA pThunk = while (pThunk->u1.Function) if (bFound) *ppfn = *pfnNew; DWORD dwOldProtect; 调用Sleep(1000)的结果如例子所示: 00407BD8: 68E8030000 push 0000003E8h Sleep: ;这是跳转到IAT里的地址 原始表: 所以最后会跳转到0x12345678。 |
-- 作者:卷积内核 -- 发布时间:3/20/2007 9:24:00 AM -- =====[ 3.2.2 改写入口点挂钩本进程 ]================== 改写函数入口点开始的一些字节这种方法相当简单。就象改变IAT里的地址一样,我们也要先修改页面属性。在这里对我们想要挂钩的函数是一开始的5个字节。为了之后的使用我们用动态分配MEMORY_BASIC_INFORMATION结构。函数的起始地址也是用GetProcAddress来获得。我们在这个地址里插入指向我们代码的跳转指令。接下来程序调用Sleep(5000)(所以它会等待5秒钟),然后Sleep函数被挂钩并重定向到new_sleep,最后它再次调用Sleep(5000)。因为新的函数new_sleep什么都不做并直接返回,所以整个程序只需要5秒钟而不是10秒种。 includelib lib\kernel32.lib kernel_name db "kernel32.dll",0 MEMORY_BASIC_INFORMATION_SIZE equ 28 PAGE_READWRITE dd 000000004h do_hook: push PAGE_READWRITE push MEMORY_BASIC_INFORMATION_SIZE call GetCurrentProcess lea eax,[esi+014h] mov byte ptr [edi],0E9h ;写入跳转指令 push offset old_protect free_mem: 004010A4: 6888130000 push 000001388h tabulka: new_sleep: |
-- 作者:卷积内核 -- 发布时间:3/20/2007 9:24:00 AM -- =====[ 3.2.3 保存原始函数 ]===================================== 更多时候我们需要的不仅仅是挂钩函数。比方说也许我们并不想取代给定的函数而只是想检查一下它的结果,或者也许我们只是想在函数被使用特定的参数来调用时才取代原函数。比较好的例子有前面提过的通过取代FindXXXFile函数来完成隐藏文件。所以如果我们想要隐藏指定的文件并且不想被注意的话,就得对其它所有文件只调用没有被修改过的原始函数。这对使用修改IAT的方法时是很简单的,为调用原始函数我们可以用GetProcAddress获得它的原始地址,然后直接调用。但修改入口点的方法就会有问题,因为修改了函数入口点的5个字节,使我们破坏了原函数。所以我们必须保存开始的那些指令。这将用到以下的技术。 old_hook: db 090h,090h,090h,090h,090h,090h,090h,090h ; version 1.05 C_MEM1 equ 0001h ; | p386 .code public disasm_main disasm_main: ;这是我的第一处修改,它只是这个函数的声明 mov ecx, [esp+4] ; ECX = opcode ptr xor edx, edx ; 标志 @@prefix: and dl, not C_PREFIX mov al, [ecx] or edx, table_1[eax*4] test dl, C_PREFIX cmp al, 0F6h cmp al, 0CDh cmp al, 0Fh and edx,C_MEM1+C_MEM2+C_MEM4+C_DATA1+C_DATA2+C_DATA4 ;这里是我的第二处修改,只有在原始版本这里是retn @@test: or dh, C_MODRM shr 8 @@int: or dh, C_DATA1 shr 8 @@0F: mov al, [ecx] cmp edx, -1 @@error: mov eax, edx @@dataw0: xor dh, C_DATA66 shr 8 @@mem67: xor dl, C_MEM2 @@data66: xor dh, C_DATA2 shr 8 @@modrm: mov al, [ecx] mov ah, al ; ah=mod, al=rm and ax, 0C007h test dl, C_67 @@modrm32: cmp al, 04h mov al, [ecx] ; sib @@a: cmp ah, 40h cmp ax, 0005h @@mem4: or dl, C_MEM4 @@mem1: or dl, C_MEM1 @@modrm16: cmp ax, 0006h @@mem2: or dl, C_MEM2 endp .data ;0F -- 在代码中分析,不需要标志(也就是标志(flag)必须为0) table_1 label dword ; 一般的指令 dd C_MODRM ; 00 table_0F label dword ; 0F为前缀的指令 dd C_MODRM ; 00 end |
-- 作者:卷积内核 -- 发布时间:3/20/2007 9:25:00 AM -- 现在我们可以获取任意地址的指令长度。我们重复调用这个函数直到读取了5个字节。完成后把这些字节拷贝到old_hook。我们知道了开始这些指令的长度,所以我们可以在原始函数的下条指令填入跳转地址。 .386p ... .data kernel_name db "kernel32.dll",0 ... MEM_RELEASE dd 000008000h ;16 nops + 一个跳转指令 do_hook: xor ecx,ecx ;下面的代码都是前面有的,所以不需要注解了 push PAGE_READWRITE push MEMORY_BASIC_INFORMATION_SIZE call GetCurrentProcess lea eax,[esi+014h] mov byte ptr [edi],0E9h push offset old_protect free_mem: 004010CC: 6888130000 push 000001388h tabulka: new_sleep: old_sleep: ;这个指令在Kernel32.Sleep(77E86779)后1个字节 Kernel32.77E8677F: 为了让这些看起来更清楚,这是原始版本的Kernel32.Sleep: Kernel32.Sleep:
|
-- 作者:卷积内核 -- 发布时间:3/20/2007 9:25:00 AM -- =====[ 3.2.4 挂钩其它进程 ]======================================== 现在我们来实践一下运行中挂钩。试想,谁会想只挂钩自己进程?这显然是非常不实用的。 我来演示3种不同的挂钩其它进程的方法。其中两种都使用了CreateRemoteThread这个API,它只在使用了NT技术的Windows版本里有效。对我来说在较老的Windows里挂钩没那么有趣。忘了说我将介绍的这3个方法我都没有实践过,所以可能会出点问题。 先介绍CreateRemoteThread。就象帮助里说的,这个函数可以在任意进程里创建新线程并运行它的代码。 HANDLE CreateRemoteThread( 句柄hProcess可以通过OpenProcess获得。这里我们必须获得足够权限。lpStartAddress是指向目标进程地址空间里存放新线程第一条指令地址的指针,因为新线程是在目标进程里创建,所以它存在于目标进程的地址空间里。lpParameter是指向提交给新线程的参数的指针。 我们可以在目标进程地址空间里任意地方运行我们的新线程。这看起来没什么用,除非在里面有我们完整的代码。第一种方法就是这么实现。它调用GetProcAddress获取LoadLibrary地址。然后把LoadLibrary赋值给参数lpStartAddress。LoadLibrary函数只有一个参数,就和目标进程里新线程的函数一样。 HINSTANCE LoadLibrary( 我们可以使用这点相似性,把lpParameter参数赋为我们的DLL库的名字。在新线程运行后lpParameter的位置就是lpLibFileName的位置。这里最重要的东西前面已经讲过了。在加载了新的模块到目标进程后就开始执行初始化部分。如果我们在这里放置了能够挂钩其它函数的特殊函数就OK了。在执行了初始化部分后,这个线程就什么都不做并被关闭,但我们的模块仍然在地址空间中。这种方法很不错而且很容易实现,它的名字叫DLL注入。但如果你和我一样不喜欢还得多个DLL库的话,请看下面的方法。但如果不介意多个DLL库的话这确实是最快的方法(从程序员的角度来看)。 实现独立的代码比较困难,但也容易给人深刻印象。独立的代码是不需要任何静态地址的代码。它里面所有东西都是互相联系地指向代码里面某些特定的地方。如果我们不知道这段代码开始执行的地址它也能自己完成 。当然,也有可能先获得地址然后重新链接我们的代码这样它可以完全正常地在新地址工作,但这比编写独立的代码更困难。这类型代码的例子比方说病毒的代码。病毒通过这种方法感染可执行文件,它把它自己的代码加入到可执行文件中的某个地方。在不同的可执行文件中放置病毒代码的位置也不一样,这取决于比方说文件结构的长度。 在非NT内核的老版本Windows里是没有CreateRemoteThread函数的,所以我们不能用以上的方法。可能会有比我现在介绍的这种方法好很多的方法,事实上我的这种方法还没有经过实践,但理论上来说是可行的。 我欢迎任何人提出更多的这里没有提到的挂钩方法,我肯定那会有很多。同样欢迎补充我介绍得不是很详细的方法。也可以把我懒得写的代码部分完成,把源代码发给我。这篇文档的目的是演示挂钩技术的细节,我希望我做到了。 |
W 3 C h i n a ( since 2003 ) 旗 下 站 点 苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》 |
514.648ms |