13.3 嵌入补丁程序因为后面的4章内容都是围绕嵌入补丁技术来写的所以有必要了解一下该技术的编程方法。对嵌入补丁的讨论将会涉及补丁程序本身和补丁工具将补丁合理合法地嵌入PE文件内部而不影响PE运行的一个程序。本节主要讲补丁程序本身因为补丁工具会因嵌入补丁所在PE中的位置不同而有所不同所以补丁工具的编写将分别在接下来的第1417章中介绍。首先来看嵌入补丁程序框架。13.3.1 嵌入补丁程序框架框架是一项工作开始的基础在第2章我们已经初步领略了使用框架的好处。代码清单13-5是基于汇编语言的嵌入补丁程序框架。代码清单13-5 嵌入补丁程序框架chapter13\patch.asm;update.asm 补丁代码 ;本段代码使用了API函数地址动态获取以及重定位技术 ;程序功能弹出对话框 ;使用 nmake 或下列命令进行编译和链接: ;ml -c -coff update.asm ;link -subsystem:windows update.obj .386 .model flat,stdcall option casemap:none include C:/masm32/include/windows.inc ;include C:/masm32/include/user32.inc ;includelib C:/masm32/lib/user32.lib ;include C:/masm32/include/kernel32.inc ;includelib C:/masm32/lib/kernel32.lib ;注意此处不静态包含引入任何其他动态链接库 _ProtoGetProcAddress typedef proto :dword,:dword _ProtoLoadLibrary typedef proto :dword _ApiGetProcAddress typedef ptr _ProtoGetProcAddress _ApiLoadLibrary typedef ptr _ProtoLoadLibrary ;------------------------------------------- ; 补丁代码中引入的其他动态链接库的函数的声明 ;------------------------------------------- _ProtoMessageBox typedef proto :dword,:dword,:dword,:dword _ApiMessageBox typedef ptr _ProtoMessageBox ;被添加到目标文件的代码从这里开始到APPEND_CODE_END处结束 .code jmp _NewEntry ; 以下内容为两个重要函数名 ; 几乎所有补丁都必须使用的 szGetProcAddr db GetProcAddress,0 szLoadLib db LoadLibraryA,0 ;------------------------------------------------------ ; 补丁代码中其他全局变量的定义 ;------------------------------------------------------ szUser32Dll db user32.dll,0 szMessageBox db MessageBoxA,0 ;该方法在kernel32.dll中 szHello db HelloWorldPE,0 ;要创建的目录 ;----------------------------- ; 错误 Handler ;----------------------------- _SEHHandler proc _lpException,_lpSEH,_lpContext,_lpDispatcher pushad mov esi,_lpException mov edi,_lpContext assume esi:ptr EXCEPTION_RECORD,edi:ptr CONTEXT mov eax,_lpSEH push [eax0ch] pop [edi].regEbp push [eax8] pop [edi].regEip push eax pop [edi].regEsp assume esi:nothing,edi:nothing popad mov eax,ExceptionContinueExecution ret _SEHHandler endp ;------------------------------------ ; 获取kernel32.dll的基地址 ;------------------------------------ _getKernelBase proc local dwRet pushad assume fs:nothing mov eax,fs:[30h] ;获取PEB所在地址 mov eax,[eax0ch] ;获取PEB_LDR_DATA 结构指针 mov esi,[eax1ch] ;获取InInitializationOrderModuleList 链表头 ;第一个LDR_MODULE节点InInitializationOrderModuleList成员的指针 lodsd ;获取双向链表当前节点后继的指针 mov eax,[eax8] ;获取kernel32.dll的基地址 mov dwRet,eax popad mov eax,dwRet ret _getKernelBase endp ;------------------------------- ; 获取指定字符串的API函数的调用地址 ; 入口参数_hModule为动态链接库的基址 ; _lpApi为API函数名的首址 ; 出口参数eax为函数在虚拟地址空间中的真实地址 ;------------------------------- _getApi proc _hModule,_lpApi local ret local dwLen pushad mov ret,0 ;计算API字符串的长度含最后的零 mov edi,_lpApi mov ecx,-1 xor al,al cld repnz scasb mov ecx,edi sub ecx,_lpApi mov dwLen,ecx ;从pe文件头的数据目录获取导出表地址 mov esi,_hModule add esi,[esi3ch] assume esi:ptr IMAGE_NT_HEADERS mov esi,[esi].OptionalHeader.DataDirectory.VirtualAddress add esi,_hModule assume esi:ptr IMAGE_EXPORT_DIRECTORY ;查找符合名称的导出函数名 mov ebx,[esi].AddressOfNames add ebx,_hModule xor edx,edx .repeat push esi mov edi,[ebx] add edi,_hModule mov esi,_lpApi mov ecx,dwLen repz cmpsb .if ZERO? pop esi jmp F .endif pop esi add ebx,4 inc edx .until edx[esi].NumberOfNames jmp _ret : ;通过API名称索引获取序号索引再获取地址索引 sub ebx,[esi].AddressOfNames sub ebx,_hModule shr ebx,1 add ebx,[esi].AddressOfNameOrdinals add ebx,_hModule movzx eax,word ptr [ebx] shl eax,2 add eax,[esi].AddressOfFunctions add eax,_hModule ;从地址表得到导出函数的地址 mov eax,[eax] add eax,_hModule mov ret,eax _ret: assume esi:nothing popad mov eax,ret ret _getApi endp ;------------------------ ; 补丁功能部分 ; 传入三个参数 ; _kernel:kernel32.dll的基地址 ; _getAddr:函数GetProcAddress地址 ; _loadLib:函数LoadLibraryA地址 ;------------------------ _patchFun proc _kernel,_getAddr,_loadLib ;------------------------------------------------------ ; 补丁功能代码局部变量定义 ;------------------------------------------------------ local hUser32Base:dword local _messageBox:_ApiMessageBox pushad ;------------------------------------------------------ ; 补丁功能代码以下只是一个范例功能为弹出对话框 ;------------------------------------------------------ ;获取user32.dll的基地址 mov eax,offset szUser32Dll add eax,ebx mov edx,_loadLib push eax call edx mov hUser32Base,eax ;使用GetProcAddress函数的首址 ;传入两个参数调用GetProcAddress函数 ;获得MessageBoxA的首址 mov eax,offset szMessageBox add eax,ebx mov edx,_getAddr mov ecx,hUser32Base push eax push ecx call edx mov _messageBox,eax ;调用函数MessageBox !! mov eax,offset szHello add eax,ebx mov edx,_messageBox push MB_OK push NULL push eax push NULL call edx popad ret _patchFun endp _start proc local hKernel32Base:dword ;存放kernel32.dll基址 local _getProcAddress:_ApiGetProcAddress ;定义函数 local _loadLibrary:_ApiLoadLibrary pushad ;获取kernel32.dll的基地址 lea edx,_getKernelBase add edx,ebx call edx mov hKernel32Base,eax ;从基地址出发搜索GetProcAddress函数的首址 mov eax,offset szGetProcAddr add eax,ebx mov edi,hKernel32Base mov ecx,edi lea edx,_getApi add edx,ebx push eax push ecx call edx mov _getProcAddress,eax ;从基地址出发搜索LoadLibraryA函数的首址 mov eax,offset szLoadLib add eax,ebx mov edi,hKernel32Base mov ecx,edi lea edx,_getApi add edx,ebx push eax push ecx call edx mov _loadLibrary,eax ;调用补丁代码 lea edx,_patchFun add edx,ebx push _loadLibrary push _getProcAddress push hKernel32Base call edx popad ret _start endp ; EXE文件新的入口地址 _NewEntry: call F ; 免去重定位 : pop ebx sub ebx,offset B invoke _start jmpToStart db 0E9h,0F0h,0FFh,0FFh,0FFh ret end _NewEntry黑体部分为一个框架程序的大部分内容包括引入函数声明、全局数据变量定义、局部变量定义、补丁功能码等。从框架中可以看出代码流程的转向过程程序先调用函数_start行297初始化一些变量然后在该函数最后调用了补丁代码程序_patchFun行276283执行完补丁代码以后通过一个跳转指令E9返回到原始入口地址处执行。注意 E9指令的地址必须通过补丁工具进行修正。接下来具体看通用补丁框架的编写规则。13.3.2 嵌入补丁程序编写规则在编写补丁程序程序时必须考虑的因素有对补丁代码中用到的全局变量的数据处理、通过DLL基地址和函数名获取其他函数地址的方法、补丁函数的调用方法等。下面分别介绍。1.全局变量及局部变量的数据处理对全局变量的引用需要考虑重定位问题如下所示195 mov eax,offset szUser32Dll ;全局变量szUser32Dll存放了动态链接库名字字符串 196 add eax,ebx在整个程序框架中ebx寄存器存放了用于修正全局变量地址的值获取的全局变量与ebx相加后即可得到重定位后的正确地址。局部变量即为栈里的变量可以直接操作如201 mov hUser32Base,eax2.获取DLL基地址方法以下代码演示了如何获取某个特定DLL的基地址。该DLL以名称字符串标识通过调用函数kernel32.dll!LoadLibraryA将指定的动态链接库加载进内存并得到该动态链接库的基地址。194 ;获取user32.dll的基地址 195 mov eax,offset szUser32Dll ; DLL名字“user32.dll” 196 add eax,ebx ; 修正地址 197 198 mov edx,_loadLib ;函数参数3LoadLibraryA函数VA 199 push eax ;传入修正后的指向DLL名字的指针 200 call edx ;调用LoadLibraryA3.获取其他函数地址知道了函数所在动态链接库的基地址和函数的名字通过调用函数GetProcAddress即可获取动态链接库中的导出函数的地址206 ;获取MessageBoxA的首址 207 mov eax,offset szMessageBox ;全局变量需要修正 208 add eax,ebx 209 210 mov edx,_getAddr ;传入的参数2局部变量直接赋值 211 mov ecx,hUser32Base ;在函数中定义的局部变量直接赋值 212 push eax ;将参数压入栈 213 push ecx 214 call edx ;调用函数GetProcAddress 215 mov _messageBox,eax ;在函数中定义的局部变量直接赋值4.补丁函数代码调用其他公有函数如果在框架中添加了其他函数那么在调用这些函数的时候必须通过使用地址的方式如下所示248 ;从基地址出发搜索GetProcAddress函数的首址 249 mov eax,offset szGetProcAddr 250 add eax,ebx 251 252 mov edi,hKernel32Base 253 mov ecx,edi 254 lea edx,_getApi 将公有函数_getApi的地址传给edx 全局变量要修正 255 add edx,ebx 256 257 push eax 258 push ecx 259 call edx 260 mov _getProcAddress,eax凡是全局变量包含公用函数地址均需要加ebx进行修正局部变量在栈中如函数传递的参数函数内部定义的变量等则可以直接使用。以上简单描述了使用嵌入补丁通用框架编程时需要注意的几个问题下面来看通用补丁程序框架的字节码。13.3.3 嵌入补丁字节码实例分析以下为HelloWorldPE补丁的字节码内容00000200 E9 B3 01 00 00 47 65 74 50 72 6F 63 41 64 64 72 ...GetProcAddr 00000210 65 73 73 00 4C 6F 61 64 4C 69 62 72 61 72 79 41 ess.LoadLibraryA 00000220 00 75 73 65 72 33 32 2E 64 6C 6C 00 4D 65 73 73 .user32.dll.Mess 00000230 61 67 65 42 6F 78 41 00 48 65 6C 6C 6F 57 6F 72 ageBoxA.HelloWor 00000240 6C 64 50 45 00 55 8B EC 60 8B 75 08 8B 7D 10 8B ldPE.Uu.}. 00000250 45 0C FF 70 0C 8F 87 B4 00 00 00 FF 70 08 8F 87 E. p.... p. 00000260 B8 00 00 00 50 8F 87 C4 00 00 00 61 B8 00 00 00 ...P...a... 00000270 00 C9 C2 10 00 55 8B EC 83 C4 FC 60 64 A1 30 00 ...Ud0. 00000280 00 00 8B 40 0C 8B 70 1C AD 8B 40 08 89 45 FC 61 ...p..Ea 00000290 8B 45 FC C9 C3 55 8B EC 83 C4 F8 60 C7 45 FC 00 EUE. 000002A0 00 00 00 8B 7D 0C B9 FF FF FF FF 32 C0 FC F2 AE ...}. 2 000002B0 8B CF 2B 4D 0C 89 4D F8 8B 75 08 03 76 3C 8B 76 M.Mu..vv 000002C0 78 03 75 08 8B 5E 20 03 5D 08 33 D2 56 8B 3B 03 x.u.^ .].3V;. 000002D0 7D 08 8B 75 0C 8B 4D F8 F3 A6 75 03 5E EB 0C 5E }.u.Mu.^.^ 000002E0 83 C3 04 42 3B 56 18 72 E3 EB 22 2B 5E 20 2B 5D .B;V.r^ ] 000002F0 08 D1 EB 03 5E 24 03 5D 08 0F B7 03 C1 E0 02 03 ..^$.]..... 00000300 46 1C 03 45 08 8B 00 03 45 08 89 45 FC 61 8B 45 F..E...E.EaE 00000310 FC C9 C2 08 00 55 8B EC 83 C4 F8 60 B8 21 10 40 ..U!. 00000320 00 03 C3 8B 55 10 50 FF D2 89 45 FC B8 2C 10 40 ..U.P E,. 00000330 00 03 C3 8B 55 0C 8B 4D FC 50 51 FF D2 89 45 F8 ..U.MPQ E 00000340 B8 38 10 40 00 03 C3 8B 55 F8 6A 00 6A 00 50 6A 8...Uj.j.Pj 00000350 00 FF D2 61 C9 C2 0C 00 55 8B EC 83 C4 F4 60 8D . a..U 00000360 15 75 10 40 00 03 D3 FF D2 89 45 FC B8 05 10 40 .u... E.. 00000370 00 03 C3 8B 7D FC 8B CF 8D 15 95 10 40 00 03 D3 ..}.... 00000380 50 51 FF D2 89 45 F8 B8 14 10 40 00 03 C3 8B 7D PQ E....} 00000390 FC 8B CF 8D 15 95 10 40 00 03 D3 50 51 FF D2 89 ....PQ 000003A0 45 F4 8D 15 15 11 40 00 03 D3 FF 75 F4 FF 75 F8 E..... u u 000003B0 FF 75 FC FF D2 61 C9 C3 E8 00 00 00 00 5B 81 EB u a....[ 000003C0 BD 11 40 00 E8 8F FF FF FF E9 F0 FF FF FF C3 00 .. .如上所示字节码开始和结束都是跳转指令前一个跳转指令跳转到该数据块的起始代码处运行后一个跳转指令跳转到目标程序的入口地址处执行。整个数据块具备良好的可移植性通过下面的测试即可看出这一点。测试目标定位为PEInfo.exe。将补丁字节码覆盖PEInfo入口地址文件偏移0xF5F开始的指令字节码然后运行PEInfo.exe。你会发现PEInfo整个变成了HelloWorldPE程序。测试patch ./peinfo.exe对系统程序的测试如对记事本程序notepad.exe文件偏移0x679d的测试效果也是一样的测试用的文件在随书文件的chapter13\c目录中。13.4 万能补丁码本节介绍一种基于嵌入补丁技术的万能补丁码相关文件在随书文件chapter13\d目录中。13.4.1 原理当一个进程动态加载外部DLL文件时除了将DLL内容映射到内存外还会执行DLL的入口函数而动态加载DLL的API函数是kernel32.dll中的LoadLibraryX系列函数。所以补丁代码需要做的工作就是找到内存中的kernel32.dll的基地址然后查找其导出表获取LoadLibraryA的VA该地址在该系统下肯定是可用的。执行该函数加载特定的DLL程序在万能补丁码的例子中为pa.dll这个DLL的入口函数中存放着补丁代码。万能补丁码就是通过这种原理实现的一种补丁技术。因为其小巧可移植性强所以可用性比较高图13-8展示了万能补丁工作原理。图13-8 万能补丁工作原理示意图补丁代码-1负责嵌入目标PE文件内部执行加载动态链接库pa.dll的任务。在执行时会首先调用pa.dll的入口函数。在pa.dll的入口函数中嵌入真正要打的补丁是补丁代码-2通过这样一种传递方式使得补丁程序可以脱离开目标PE文件而获得执行。这样生产的补丁代码具有更大的扩展空间所以称为万能补丁码。下面介绍万能补丁码的源代码。13.4.2 源代码代码清单13-6为万能补丁的源代码。代码清单13-6 万能补丁源代码chapter13\d\getLoadLib.asminclude windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib ;数据段 .data szText db LoadLibrary的函数地址为%08x,0 szOut db %08x,0dh,0ah,0 szBuffer db 256 dup(0) ;代码段 .code start: mov edi,edi call loc0 db LoadLibraryA,0 ;特征函数名 db pa,0 ;动态链接库pa.dll loc0: pop edx ;edx中存放了特征函数名所在地址 push edx push edx assume fs:nothing mov eax,fs:[30h] ;获取PEB所在地址 mov eax,[eax0ch] ;获取PEB_LDR_DATA 结构指针 mov esi,[eax1ch] ;获取InInitializationOrderModuleList 链表头 ;第一个LDR_MODULE节点InInitializationOrderModuleList成员的指针 lodsd ;获取双向链表当前节点后继的指针 mov ebx,[eax8] ;获取kernel32.dll的基地址 loc2: ;遍历导出表 mov esi,dword ptr [ebx3ch] add esi,ebx ;esi指向PE头 mov esi,dword ptr [esi78h] add esi,ebx ;esi指向数据目录中的导出表 mov edi,dword ptr [esi20h] ;指向导出表的AddressOfNames add edi,ebx ;EDI为AddressOfNames数组起始位置 mov ecx,dword ptr [esi14h] ;指向导出表的NumberOfNames push esi xor eax,eax loc3: push edi push ecx mov edi,dword ptr [edi] add edi,ebx ;edi指向了第一个函数的字符串名起始 mov esi,edx ;esi指向了特征函数名起始 xor ecx,ecx mov cl,0ch ;特征函数名的长度 repe cmpsb je loc4 ;找到特征函数转移 pop ecx pop edi add edi,4 ;edi移动到下一个函数名所在地址 inc eax ;eax为索引 loop loc3 loc4: pop ecx pop edi pop esi ;esi指向数据目录中的导出表 mov edi,dword ptr [esi24h] ;指向导出表的Name索引 add edi,ebx ;edi为AddressOfNamesOrdinals数组起始位置 ;计算eax处的值 sal eax,1 ;eax中存放了指定索引距离数组的偏移 add edi,eax mov ax,word ptr [edi] ;又是一个索引指向AddressOfFunctions mov edi,dword ptr [esi1ch] ;AddressOfFunctions add edi,ebx sal eax,2 ;索引4 add edi,eax mov eax,dword ptr [edi] ;eax为取到的LoadLibraryA的RVA地址 add eax,ebx ;edx指向patch.dll ;加载dll引发对补丁的调用 pop edx add edx,0dh push edx call eax ;跳转 db 0E9h,0FFh,0FFh,0FFh,0FFh end start行2834是通过PEB获取kernel32.dll基地址。行3782是获取LoadLibraryA的函数VA。行8489是调用LoadLibraryA函数动态引入动态链接库pa.dll。行9091是跳转到入口地址执行该部分代码也可以使用FF 25无条件跳转指令。13.4.3 字节码使用FlexHex打开最终生成的getLoadLib.exe将代码段的字节码复制出来如下所示。该代码在源代码中已有比较详尽的描述经过特意调整动态链接库的名称为“pa”即“patch. dll”的意思。调整后的大小为80h即128个字节这个尺寸要比最小的弹出对话框的PE 133字节还要小但它实现的功能却超出你的想象00000200 8B FF E8 10 00 00 00 4C 6F 61 64 4C 69 62 72 61 ....LoadLibra 00000210 72 79 41 00 70 61 00 5A 52 52 64 A1 30 00 00 00 ryA.pa.ZRRd0... 00000220 8B 40 0C 8B 70 1C AD 8B 58 08 8B 73 3C 03 F3 8B .p.X.s. 00000230 76 78 03 F3 8B 7E 20 03 FB 8B 4E 14 56 33 C0 57 vx. .N.V3W 00000240 51 8B 3F 03 FB 8B F2 33 C9 B1 0C F3 A6 74 08 59 Q?.3.t.Y 00000250 5F 83 C7 04 40 E2 E8 59 5F 5E 8B 7E 24 03 FB D1 _.Y_^$. 00000260 E0 03 F8 66 8B 07 8B 7E 1C 03 FB C1 E0 02 03 F8 .f..... 00000270 8B 07 03 C3 5A 83 C2 0D 52 FF D0 E9 FF FF FF FF ..Z.R13.4.4 运行测试下面使用记事本程序进行测试。将以上代码复制到记事本程序的文件偏移00007b48处然后修改E9 FF FF FF FF为E9 D5 EB FF FF。【编辑】》【插入即时数据】修改后将记事本入口点位于文件偏移0x00000108处由原来的0000739D更改为00008748。运行记事本程序查看运行效果。修改后可以看到在运行记事本前先弹出了对话框。13.5 小结本章主要描述了对PE进程和PE文件进行补丁的不同方法即动态补丁和静态补丁。重点对静态补丁中的基于PE文件的嵌入补丁技术进行了描述内容包括补丁程序框架、补丁编写规则等。此外还为读者描述了一种基于嵌入补丁技术的万能补丁码。后续四章将分别介绍对PE文件实施嵌入补丁的四种不同方法因此熟练掌握本章的内容是学习后续章节的基础。