当前位置: 首页 > news >正文

《Windows PE权威指南》学习之第21章 EXE加密

EXE加密是软件保护范畴的一种技术,通过对指定的PE文件进行加密,可以增加逆向分析代码的难度,在一定程度上保护软件代码的安全。

EXE加密技术经常用于对软件的加壳处理,通过PE分析软件对加密后的PE文件进行分析,只能看到与补丁代码有关的信息,原始的PE文件相关信息被隐藏;同时,如果在EXE补丁代码中使用一些技巧,也能有效地增加PE反调试的难度,达到保护软件的目的。

21.1 基本思路

加密一个PE文件的基本思路如下:

步骤1由补丁工具完成对目标PE文件数据目录表的修改,并将原始数据目录表的内容转移到补丁代码中。

步骤2由补丁工具完成对目标PE文件节区数据的加密。

步骤3由补丁工具设置目标PE文件入口地址指向补丁代码。

步骤4由补丁代码完成对目标PE文件数据目录表的还原。

步骤5由补丁代码完成对目标PE文件节区数据的解密。

步骤6由补丁代码完成对目标PE文件导入表中动态链接库的动态加载。

步骤7由补丁代码完成对目标PE文件IAT的修正。

加密以后的目标文件结构如图21-1所示。

图21-1 加密以后的目标PE文件结构

如图所示,由于加密以后的数据目录表被清零,通过结构分析工具(如PEInfo)查看目标PE文件时,数据目录表中注册的数据类型都不会显示,原始数据目录表会被补丁工具转移到补丁代码中。除数据目录表、SizeOfImage、最后一节的SizeOfRawData、函数入口地址AddressOfEntryPoint外,目标PE文件的头部数据基本保持不变。目标PE文件的节区数据全部被加密保存,但节区数据的长度不变,补丁代码被选择附加到目标PE文件的最后一节中。

21.2 加密算法

加密算法是EXE加密的核心。本节首先介绍了常用的两类加密算法,然后介绍了自行设计的可逆加密算法,并给出了加密代码。

21.2.1 加密算法的分类

按照加密后的信息是否可以被还原,常用的加密算法分为两大类:

❑ 不可逆加密算法

❑ 可逆加密算法

下面分别来介绍。

1.不可逆加密算法

不可逆加密算法的特征是,加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文。这种加密后的数据是无法被解密的,只有重新输入明文,并再次经过同样不可逆的加密算法处理,得到相同的加密密文并被系统重新识别后,才能真正解密。

为了避免有人通过可逆算法得到加密前的信息,在用户权限认证的系统中,通常会使用一张不可逆加密的表,表中存放着经过加密以后的用户密码。这个密码是任何人也破解不了的,除非被他有幸猜中,虽然不能还原最初的密码,但可以通过对用户密码的再一次加密实现用户密码的验证。

举个最简单的例子:假设用户最初输入的密码是“123456”,使用的不可逆加密算法是将每一位当成一个数字,然后与3相除,取余数作为每一位加密后的结果,最终用户密码被加密成“120120”。很显然,你无法正确地还原回去,因为许多原始密码经过这种加密算法得到的值都是“120120”,如原始密码为“789123”、“456123”等。但是,这种加密算法却可以用在对用户的验证上,即用户只要输入“123456”,其加密以后的值一定是表中存储的值。

很明显,在这类加密过程中,加密是自己,解密还得是自己;而所谓解密,实际上就是重新加一次密,所应用的“密码”也就是输入的明文。

不可逆加密算法不存在密钥保管和分发问题,非常适合在分布式网络系统上使用,但因加密计算复杂,工作量相当繁重,通常只在数据量有限的情形下使用,例如,广泛应用在计算机系统中的口令加密,利用的就是不可逆加密算法。

近年来,随着计算机系统性能的不断提高,不可逆加密的应用领域正在逐渐增大。在计算机网络中应用较多不可逆加密算法的有许多,例如RSA公司发明的MD5算法,以及由美国国家标准局建议的不可逆加密标准SHS(Secure Hash Standard,安全散列信息标准)等。

2.可逆加密算法

可逆加密算法又分为两大类:“对称式”和“非对称式”。

对称式加密 加密和解密使用同一个密钥,通常称之为“Session Key ”。这种加密技术目前被广泛采用,如美国政府所采用的DES加密标准就是一种典型的“对称式”加密法,它的Session Key长度为56Bits。

非对称式加密 加密和解密所使用的不是同一个密钥,而是两个密钥:一个称为“公钥”,另一个称为“私钥”;它们两个必须配对使用,否则不能打开加密文件。这里的“公钥”是指可以对外公布的,“私钥”则只能由持有人本人知道。它的优越性就在这里,因为如果是在网络上传输加密文件,对称式的加密方法就很难把密钥告诉对方,不管用什么方法都有可能被别人窃听到。而非对称式的加密方法有两个密钥,且其中的“公钥”是可以公开的,不怕别人知道,收件人解密时只要用自己的私钥即可以,这样就很好地避免了密钥的传输安全性问题。

最典型的可逆加密算法是异或运算。大家都知道,对一个值连续异或两次,其结果还是原值;于是,第一次异或被看成是加密,第二次异或被看成是解密。

对EXE进行加密必须使用一些可逆的加密算法,即不仅能加密数据,还要能还原数据。下面我们就自行开发一种简单的可逆加密算法。

21.2.2 自定义可逆加密算法实例

本节自行定义了一种可逆的加密算法,基本思路是:

首先,构造一个256字节的加密用基表,该基表中的每一项的值都不重复,这就意味着该基表中拥有了00h~0ffh所有的字节值。有了这个基表以后,再对PE中的每一个字节进行加密。加密方法非常简单,将要加密的字节当做是基表的索引,查找索引处的字节值,该值即为加密后的值。加密算法示意见图21-2。

图21-2 字节加密算法流程

如图所示,加密基表共有256项,其中存储着256个不同的值,并且这些值不是按顺序排列的。例如,待加密的字节为50h,将这个值当成是基表的索引,查找该表50h处的字节是98h,找到的这个值即为加密以后的值。

解密的过程刚好相反,根据要解密的字节98h遍历基表,如果找到与之相等的值,则记录此处的索引值,该索引值50h即为解密后的字节。

特别注意 基表的最后一个字节固定为00h。这个值的设置与基表的构造方式有关,详见下一节。

21.2.3 构造加密基表

基表的组成包括255个随机数(均不重复)和最后一个00h字节。其中随机数的构造方法如下:

;生成加密基表 mov @dwCount,0 mov edi,offset EncryptionTable .while TRUE invoke _getAByte mov byte ptr [edi],al inc edi inc @dwCount .break .if @dwCount==0ffh .endw

开始时,加密基表所有表项值均被初始化为00h,函数_getAByte得到一个与当前表项中已存在的所有值均不重复的字节,并将其添加到表项。以下是函数_getAByte的定义:

_getAByte proc local @ret pushad loc1: ; 取随机数 invoke _getRandom,1,255 mov @ret,eax ; 判断随机数是否在基表中 invoke _isExists,eax .if eax ;如果在,则重新获取随机数 jmp loc1 .endif popad mov eax,@ret ;如果不在基表,则返回 ret _getAByte endp

函数_getAByte首先取一个1~255之间的数(注意,这里舍弃了0,因为0被设置为基表索引255处,这样做主要是为了方便函数_isExists的运行)。取指定范围随机数的方法如下:

_getRandom proc _dwMin:dword, _dwMax:dword local @dwRet:dword pushad ; 取得随机数种子,当然,可用别的方法代替 invoke GetTickCount mov ecx, 19 ; X = ecx = 19 mul ecx ; eax = eax * X add eax, 37 ; eax = eax + Y(Y = 37) mov ecx, _dwMax ; ecx = 上限 sub ecx, _dwMin ; ecx = 上限 - 下限 inc ecx ; Z = ecx + 1(得到了范围) xor edx, edx ; edx = 0 div ecx ; eax = eax mod Z(余数在edx里面) add edx,_dwMin mov @dwRet, edx popad mov eax, @dwRet ; eax = Rand_Number ret _getRandom endp

获取随机数的基本算法为:

随机数=下限+(随机数*19+37)mod(上限-下限+ 1)

随机数(1~255之间)获取以后,首先通过函数_isExists判断该值是否在基表中已经存在。以下是函数_isExists的详细代码:

_isExists proc _byte local @ret pushad mov esi,offset EncryptionTable mov ecx,0 .while TRUE mov al,byte ptr [esi] .if al==0 mov @ret,FALSE .break .endif mov ebx,_byte .if al==bl mov @ret,TRUE .break .endif inc esi inc ecx .if ecx==0ffh mov @ret,FALSE .break .endif .endw popad mov eax,@ret ret _isExists endp

函数_isExists将循环检测基表,结束条件有两个:一是当函数在基表中找到了相同的值,则返回TRUE;另一个条件是函数已经完全遍历完基表,未发现相同的值,返回FALSE,如果在基表中碰到00h,则表示已经到了基表末尾,返回FALSE。这里解释了上一节最后提出的问题,即为什么要在基表的最后一个位置存放固定的字节00h。

通过以上方法,由计算机自动完成填充一个基表,以下字节码是代码chapter21\HelloWorld.exe运行期获取的一个基表内容。可以看到,基表中任何一项的值都是介于00h~0FFh的,每个值都不相等,且基表的最后一个字节是00h。

00401000 63 94 B2 E3 02 33 64 82 B3 E4 03 敳?3d偝? 00401010 34 52 83 B4 D2 04 35 53 84 A2 D3 05 23 54 85 A3 4R兇?5S劉?#T叄 00401020 D4 F2 24 55 73 A4 D5 F3 25 43 74 A5 C3 F4 26 44 则$Usふ?Ctッ?D 00401030 75 93 C4 F5 14 45 76 C5 15 46 95 C6 16 65 96 E5 u撃?Ev?F暺┬e栧 00401040 17 66 B5 E6 36 67 B6 06 37 86 B7 07 56 87 D6 08 ┤f垫6g?7喎●V囍■○○ 00401050 57 A6 D7 27 58 A7 F6 28 77 A8 F7 47 78 C7 F8 48 Wψ'X (w Gx区H 00401060 97 C8 18 49 98 E7 19 68 99 E8 38 69 B8 E9 39 88 椚↑I樼├h欒8i搁9 00401070 B9 09 3A 89 D8 0A 59 8A D9 29 5A A9 DA 2A 79 AA ?:壺.Y娰)Z┶*y 00401080 F9 2B 7A C9 FA 4A 7B CA 1A 4B 9A CB 1B 6A 9B EA ?z生J{?K毸←j涥 00401090 1C 6B BA EB 3B 6C BB 0B 3C 8B BC 0C 5B 8C DB 0D k弘;l?<嫾.[屰. 004010A0 5C AB DC 2C 5D AC FB 2D 7C AD FC 4C 7D CC FD 4D \ ,] -| L} 听 M 004010B0 9C CD 1D 4E 9D EC 1E 6D 9E ED 3D 6E BD EE 3E 8D 溚N濎-m烅=n筋> 00401090 6D 9E ED 3D 6E BD EE 3E 8D BE 0E 3F 8E DD 0F 5E -| L}~ 0溚N濎 004010A0 8F DE 2E 5F AE DF 2F 7E AF FE 30 7F CE FF 4F 80 忁._ /~ 0 ?O€ 004010B0 CF 1F 50 9F D0 20 6F A0 EF 21 70 BF F0 40 71 C0 ?P熜 o狅!p筐@q 004010C0 BE 0E 3F 8E DD 0F 5E 8F DE 2E 5F AE DF 2F 7E AF ??庉¤^忁._ /~ 004010D0 FE 30 7F CE FF 4F 80 CF 1F 50 9F D0 20 6F A0 EF ? ?O€?P熜 o狅 004010E0 21 70 BF F0 40 71 C0 10 41 90 C1 11 60 91 E0 12 !p筐@q?A惲▲`戉↑↓ 004010F0 61 B0 E1 31 62 B1 01 32 81 51 D1 A1 22 F1 72 42 a搬1b?2丵选"駌B 00401100 C2 92 13 E2 00 聮!!?.

构造加密基表的完整代码可以从随书文件chapter21\HelloWorld.asm中获得。

21.2.4 利用基表测试加密数据

接下来,我们就用上节生成的基表进行数据加密的测试。首先,在数据段中定义两部分数据,一部分为加密前的数据szSrc,另一部分为加密后的数据szDst。为了测试,只定义了8个字节,加密前的数据随便取一些值,加密后的数据全部初始化为00h。加密数据的代码见代码清单21-1。

代码清单21-1 加密数据的函数_encrptIt(chapter21\HelloWorld.asm)

138 ;------------------------------- 139 ; 加密算法,可逆算法,字节数不变 140 ; 入口参数: 141 ; _src:要加密的字节码起始地址 142 ; _dst:生成加密后的字节码起始地址 143 ; _size:要加密的字节码的数量 144 ;------------------------------- 145 _encrptIt proc _src,_dst,_size 146 local @ret 147 148 pushad 149 ;开始按照基表对字节进行加密 150 mov esi,_src 151 mov edi,_dst 152 .while TRUE 153 mov al,byte ptr [esi] 154 xor ebx,ebx ;将获取的值变成索引存储到ebx中 155 mov bl,al 156 mov al,byte ptr EncryptionTable[ebx] ;按索引值从基表里取出加密后的值 157 mov byte ptr [edi],al 158 159 inc esi 160 inc edi 161 dec _size 162 .break .if _size==0 163 .endw 164 popad 165 ret 166 _encrptIt endp

开始加密前,esi指向要加密的数据,edi指向存储加密结果的缓冲区,行152~163是一个循环,循环次数为要加密数据的字节个数。行153~155将获取的值当成一个索引存储在ebx寄存器中;行156~157按照该索引值从基表中取出加密后的值,存储到结果缓冲区。

以下是运行函数后的数据加密结果:

00403000 13 15 A0 00 17 !┴?┤ 00403010 01 00 FF 84 D3 AC 63 23 94 63 00 ┌. 動琧#攃....

从列出的字节码可以看出加密前后数据的对应关系,请对比以下所列自行查看基表,看这些值是否真正符合我们的加密算法。

加密前字节← →加密后字节 ---------------------------------------- 13← →84 15← → D3 A0← → AC 00← → 63 17← → 23 01← → 94 00← → 63 FF← → 00

21.3 开发补丁工具

补丁工具主要完成的操作包括:

❑ 处理目标PE文件的数据目录表,向补丁代码传递原始数据目录表。

❑ 生成加密基表,向补丁代码传递加密基表及其他要传递给补丁代码的参数。

❑ 加密节区数据。

❑ 将补丁代码附加到目标PE文件的最后一节。

本节相关文件在随书文件的目录chapter21\b中,下面分别介绍。

21.3.1 转移数据目录

由于导入表的数据全部存储在节区,而补丁工具会对这些数据进行加密处理,破坏原有的导入表结构,造成PE加载器加载目标PE文件失败,因此,必须事先将目标程序的数据目录表的导入表项设置为0,即告诉加载器:凡是目标PE中涉及的所有动态链接库不需要操作系统加载器来处理。不仅导入表的数据这样处理,数据目录表中的其他数据也需要这样处理。

修改数据目录表的方法是将所有项的RVA均设置为0。下面来看一个例子,通过第17章介绍的方法,将一个最简单的HelloWorld补丁打到记事本程序中,打补丁的过程输出如下信息:

补丁程序:D:\masm32\source\chapter21\patch.exe 目标PE程序:C:\notepad.exe 补丁代码段大小:00000196 PE文件大小:00010400 对齐以后的大小:00010400 目标文件最后一节在文件中的起始偏移:00008400 目标文件最后一节对齐后的大小:00008200 新文件大小:00010600 补丁代码中的E9指令后的操作数修正为:ffff4208

最终生成的补丁程序为chapter21\patch_notepad.exe,由于对原始notepad.exe程序的节数据没有进行加密处理,所以,使用OD加载该程序后内存空间分配情况见表21-1。

表21-1 未加密前的进程内存空间分配表

如表所示,第17行为进程的PE文件头部分,18~20行为进程的其他节所在的起始地址和结束地址。下面,将文件头部数据目录表项全部清零,重新使用OD加载程序测试内存布局,以下是清零以后的记事本的数据目录表字节码:

00000150 00 00 00 00 00 00 00 00 ........ 00000160 04 76 00 00 C8 00 00 00 00 B0 00 00 20 7F 00 00 .v........ ... 00000170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000180 00 00 00 00 00 00 00 00 50 13 00 00 1C 00 00 00 ........P....... 00000190 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000001A0 00 00 00 00 00 00 00 00 A8 18 00 00 40 00 00 00 ...........@... 000001B0 50 02 00 00 D0 00 00 00 00 10 00 00 48 03 00 00 P..........H... 000001C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000001D0 00 00 00 00 00 00 00 00 ........

从字节码可以看出,记事本程序数据目录中的以下数据类别项被定义:导入表、资源表、调试信息、加载配置、绑定引入和IAT。现在把以上加黑部分全部清零,然后用OD加载,此时的内存空间分配见表21-2。

表21-2 加密后的进程地址空间分配表

从表21-1和表21-2的对比来看,如果把数据目录中所有的内容全部清零,目标程序patch_notepad.exe自身(不含导入的其他动态链接库)加载进内存后的空间分配是一致的,如两个表的黑体部分所示。这就意味着,对数据目录表清零的操作不影响进程自身模块最终加载进内存的空间分配。

被加密的目标PE最终还是要运行的,所以,在运行前必须将数据目录表的内容恢复原样,这就要求必须首先保存目标PE文件的数据目录表的原始内容,保存的首选位置是嵌入到目标PE文件的补丁程序。代码清单21-2(该代码使用了随书文件chapter17\bind.asm)为补丁工具中模拟清除数据目录内容及备份内容到补丁程序的代码:

代码清单21-2 补丁工具中转移目标PE文件原始数据目录表内容的函数_openFile (chapter21\b\bind.asm)

1264 1265 ;---------------------------到此为止,数据复制完毕 1266 1267 ;清空目标文件中的数据目录表的内容 1268 ;并把该内容填充到补丁代码处 1269 ;该部分内容在(补丁代码起始+5个字节)处,共16个目录项 1270 ;该部分操作全部在lpDstMemory中完成 1271 1272 mov esi,lpDstMemory 1273 assume esi:ptr IMAGE_DOS_HEADER 1274 add esi,[esi].e_lfanew 1275 assume esi:ptr IMAGE_NT_HEADERS 1276 1277 ;复制数据目录表内容到补丁字节码处 1278 mov eax,[esi].OptionalHeader.NumberOfRvaAndSizes 1279 sal eax,3 1280 mov ecx,eax 1281 mov dwDDSize,ecx 1282 add esi,78h 1283 mov dwDDStart,esi 1284 1285 mov edi,lpDstMemory 1286 add edi,dwNewFileAlignSize 1287 add edi,5 ;跳转指令长度,定位到补丁程序中保存数据目录表的位置 1288 rep movsb 1289 1290 ;清空目标文件数据目录表内容 1291 mov edi,dwDDStart 1292 mov ecx,dwDDSize 1293 mov al,0 1294 rep stosb 1295

如上所示,行1277~1288负责将目标PE文件的原始目录表的数据(共78h个字节)复制到补丁代码指定的位置。行1290~1294则将目标PE文件的数据目录表的内容全部置0。

实现该功能的相关测试文件请参照随书文件目录chapter21\b。其中bind.asm是补丁工具, patch.asm为补丁程序,patch_notepad.exe是生成的打了补丁的记事本程序。使用FlexHex查看补丁后的记事本程序,会发现记事本数据目录中的所有内容均被移动到了补丁代码中。

21.3.2 传递程序参数

要传给补丁程序的参数包括解密用的基表,以及补丁前最后一节在文件中的大小。基表用于补丁程序解密使用,补丁前最后一节在文件中的大小需要事先传递给补丁程序,因为一旦补丁被打上,该值会发生变化。向补丁程序传递参数的相关代码见代码清单21-3。

1296 ;初始化加密基表 1297 invoke _encrptAlg 1298 1299 ;将基表复制到补丁代码中 1300 mov esi,offset EncryptionTable 1301 mov edi,lpDstMemory 1302 add edi,dwNewFileAlignSize 1303 add edi,5 ;5个字节的跳转指令 1304 add edi,16*8 ;16*8个数据目录表长度 1305 mov ecx,256 1306 rep movsb 1307 … 1327 1328 ;将最后一节在文件中对齐后的大小存储到补丁代码位置 1329 1330 mov edi,lpDstMemory 1331 assume edi:ptr IMAGE_DOS_HEADER

代码清单21-3 补丁工具向补丁程序传递参数(chapter21\b\bind.asm)

1332 add edi,[edi].e_lfanew 1333 assume edi:ptr IMAGE_NT_HEADERS 1334 ;获取节的个数 1335 movzx eax,[edi].FileHeader.NumberOfSections 1336 mov dwSections,eax 1337 add edi,sizeof IMAGE_NT_HEADERS 1338 1339 dec eax 1340 mov ecx,sizeof IMAGE_SECTION_HEADER ;定位到最后一个节 1341 mul ecx 1342 add edi,eax 1343 assume edi:ptr IMAGE_SECTION_HEADER 1344 mov ecx,[edi].SizeOfRawData 1345 1346 mov edi,lpDstMemory 1347 add edi,dwNewFileAlignSize 1348 add edi,5 ;5个字节的跳转指令 1349 add edi,16*8 ;16*8个数据目录表长度 1350 add edi,257 ;基表 1351 mov dword ptr [edi],ecx

如上所示,行1297调用函数_encrptAlg创建加密用的基表。行1299~1306将获得的基表内容复制到补丁程序的指定位置,共256个字节。行1328~1344得到目标PE文件最后一节的SizeOfRawData,行1346~1351将该参数传递给补丁程序。

补丁程序(chapter21\b\patch.asm)中存放加密基表和目标PE文件最后一节的SizeOfRawData两个参数定义如下:

jmp start ;补丁程序起始代码,该指令占了5个字节+0000h ;保存目标程序的相关信息: dstDataDirectory dd 32 dup(0) ; 原始目标程序的数据目录表+0005h EncryptionTable db 256 dup(0),0 ; 加密基表 +0085h dwLastSectionSize dd ? ; 最后一节的尺寸(以字节计)

如上所示,补丁程序中保存的与目标PE有关的信息主要包括三个:原始数据目录表、解密用的基表和目标PE文件最后一节的尺寸。以下节选了运行期该位置显示的数据内容:

00010400 E9 FC 05 00 00【00 00 00 00 00 00 00 00 04 76 00 ............v. 00010410 00 C8 00 00 00 00 B0 00 00 20 7F 00 00 00 00 00 ....... ...... 00010420 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010430 00 00 00 00 00 50 13 00 00 1C 00 00 00 00 00 00 .....P.......... 00010440 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010450 00 00 00 00 00 A8 18 00 00 40 00 00 00 50 02 00 ........@...P.. 00010460 00 D0 00 00 00 00 10 00 00 48 03 00 00 00 00 00 ........H...... 00010470 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010480 00 00 00 00 00 】【73 91 C2 F3 12 43 61 92 C3 E1 13 .....s.Ca. 00010490 44 62 93 B1 E2 14 32 63 94 B2 E3 02 33 64 82 B3 Db.2c.3d 000104A0 E4 03 34 52 83 B4 D2 04 35 53 84 A2 D3 05 23 54 .4R.5S.#T 000104B0 85 A3 D4 F2 24 55 A4 D5 25 74 A5 F4 26 75 C4 F5 $U%t&u 000104C0 45 76 C5 15 46 95 C6 16 65 96 E5 17 66 B5 E6 36 Ev.F.e.f6 000104D0 67 B6 06 37 86 B7 07 56 87 D6 08 57 A6 D7 27 58 g.7.V.W'X 000104E0 A7 F6 28 77 A8 F7 47 78 C7 F8 48 97 C8 18 49 98 (wGxH.I 000104F0 E7 19 68 99 E8 38 69 B8 E9 39 88 B9 09 3A 89 D8 .h8i9.: 00010500 0A 59 8A D9 29 5A A9 DA 2A 79 AA F9 2B 7A C9 FA .Y)Z*y+z 00010510 4A 7B CA 1A 4B 9A CB 1B 6A 9B EA 1C 6B BA EB 3B J{.K.j.k; 00010520 6C BB 0B 3C 8B BC 0C 5B 8C DB 0D 5C AB DC 2C 5D l.<.[.\,] 00010530 AC FB 2D 7C AD FC 4C 7D CC FD 4D 9C CD 1D 4E 9D -|L}M.N 00010540 EC 1E 6D 9E ED 3D 6E BD EE 3E 8D BE 0E 3F 8E DD .m=n>.? 00010550 0F 5E 8F DE 2E 5F AE DF 2F 7E AF FE 30 7F CE FF .^._/~0. 00010560 4F 80 CF 1F 50 9F D0 20 6F A0 EF 21 70 BF F0 40 O€.P o!p@ 00010570 71 C0 10 41 90 C1 11 60 E0 B0 31 01 81 51 D1 A1 q.A.`1.Q 00010580 22 F1 72 42 00】00【00 00 01 00】00 00 00 00 00 00 "rB............

如上所示,第一个【】包含了目标程序的数据目录表,第二个【】是解密用基表内容,第三个【】是目标程序最后一节的实际字节码长度。

21.3.3 加密节区内容

补丁工具负责对目标PE文件节区数据的加密工作。首先,计算出要加密数据的范围,从第一个节开始到文件末尾的数据,全部要进行加密。由于加密采用不变长度算法,能保证PE加载器在加载数据到内存以后所有的字节码位置保持相对不变,在进行解密以后不需要重新计算代码中与定位有关的数据,所以比较方便。对节区数据进行加密的代码见代码清单21-4。

1308 ;加密节区数据 1309 ;------------------------------ 1310 ;首先,计算加密范围 1311 ;取第一个节的文件起始偏移 1312 1313 mov edi,lpDstMemory 1314 assume edi:ptr IMAGE_DOS_HEADER 1315 add edi,[edi].e_lfanew 1316 add edi,sizeof IMAGE_NT_HEADERS 1317 1318 assume edi:ptr IMAGE_SECTION_HEADER 1319 mov eax,[edi].PointerToRawData 1320 1321 mov ecx,@dwFileSize1 1322 sub ecx,eax 1323 add eax,lpDstMemory ;起始偏移 1324 mov esi,eax 1325 1326 invoke _encrptIt,esi,esi,ecx

代码清单21-4 加密节区数据(chapter21\b\bind.asm)

函数_encrptIt在21.2.4节已经讲解过了,传入参数esi为第一个节在文件中的起始地址,ecx为从该位置到文件尾的长度。程序运行到此,目标文件中的指定范围的数据均被加密。

经过同一个加密算法生成的目标程序使用了相同的补丁程序,所以,使用一些PE分析工具得到的结果除了文件头部描述不相同外,其他地方的描述基本是相同的。使用PEInfo小工具分析加密以后的记事本程序结果显示如下:

文件名:D:\masm32\source\chapter21\b\patch_notepad.exe ----------------------------------------- 运行平台: 0x014c 节的数量: 3 文件属性: 0x010f 建议装入基地址: 0x01000000 文件执行入口(RVA地址): 0x13000 节的名称 未对齐前真实长度 内存中的偏移(对齐后的) 文件中对齐后的长度 文件中的偏移 节的属性 ----------------------------------------------------------------------------- .text 00007748 00001000 00007800 00000400 60000020 .data 00001ba8 00009000 00000800 00007c00 c0000040 .rsrc 00009000 0000b000 00008800 00008400 c0000060 未发现该文件有导入函数 未发现该文件有导出函数 未发现该文件有重定位信息 未发现该文件有资源表

从分析结果来看,这里显示的内容根本不是原始的记事本程序,用FlexHex查看字节码,也只是看到一堆乱码而已。

补丁工具还有最后一个功能,即负责将补丁代码嵌入到目标PE文件的最后一节。由于这部分功能在第17章中有详细描述,在此略过,具体代码请参照随书文件chapter21\b\bind.asm。

21.4 处理补丁程序

前几章讲过的补丁程序实现的功能都与目标PE无关,而这次补丁程序的代码针对的对象是目标程序本身,所以需要额外对目标程序的数据进行如下处理:

❑ 对目标PE数据目录进行还原。

❑ 对目标PE加密的节区的数据进行解密。

❑ 对目标PE导入表中的动态链接库实施动态加载。

❑ 对目标PE的IAT中的值进行修正。

以下将对这四项处理进行逐一介绍。

21.4.1 还原数据目录表

对EXE加密必须保证加密以后的PE文件可以正常运行,但补丁工具却将其数据目录表全部清零,所以在运行目标程序前补丁程序要做的第一件事情,就是依据事先保存在补丁程序中的目标PE文件的原始数据目录表信息恢复目标PE文件的数据目录表。代码清单21-5演示了还原目标PE文件数据目录表的整个过程:

447 ;获取目标进程的基地址 448 mov eax,offset dwImageBase 449 add eax,ebx 450 451 push eax 452 lea edx,_getImageBase

代码清单21-5 还原数据目录表(chapter21\b\patch.asm)

453 add edx,ebx 454 call edx 455 mov dwImageBase[ebx],eax 456 457 458 ;还原目标进程的数据目录表 459 mov esi,dwImageBase[ebx] 460 add esi,[esi+3ch] 461 add esi,78h 462 push esi 463 464 assume fs:nothing 465 mov eax,fs:[20h] 466 mov hProcessID[ebx],eax 467 468 469 push hProcessID[ebx] 470 push FALSE 471 push PROCESS_ALL_ACCESS 472 call _openProcess 473 mov hProcess[ebx],eax ;找到的进程句柄在hProcess中 474 475 476 477 ;设置文件头部分为可读可写可执行 478 lea edx,hOldPageValue 479 add edx,ebx 480 push edx 481 push PAGE_EXECUTE_READWRITE 482 ;获取SizeOfImage大小 483 push esi 484 mov esi,dwImageBase[ebx] 485 add esi,[esi+3ch] 486 assume esi:ptr IMAGE_NT_HEADERS 487 mov edx,[esi].OptionalHeader.SizeOfImage 488 pop esi 489 push edx ;设置页面大小 490 push dwImageBase[ebx] 491 push hProcess[ebx] 492 call _virtualProtectEx 493 494 pop esi 495 push NULL 496 push 16*8 497 mov edx,offset dstDataDirectory 498 add edx,ebx 499 push edx 500 push esi 501 push hProcess[ebx] 502 call _writeProcessMemory

大致思路是,通过OpenProcess函数传递PROCESS_ALL_ACCESS参数,打开目标进程。然后,使用WriteProcessMemory将补丁代码中保存的数据目录表项全部写回到目标进程头部数据目录表位置。代码行447~455获取目标PE的基地址,存储到变量dwImageBase中。行464~466从fs:[20h]位置处取出进程的ID号,存储到变量hProcessID中。行469~473调用OpenProcess函数打开目标进程。行477~492调用函数VirtualProtectEx将目标进程文件头部内存页设置为可读可写。行494~502调用函数WriteProcessMemory,将esi指向的数据写入变量dstDataDirectory指向的位置,即还原目标进程的数据目录表。

数据目录表被还原以后,程序还无法正常运行,因为此时数据目录表中的数据项指向的位置都是经过加密以后的数据,对正常的目标进程而言,这些数据根本无法识别。接下来要做的就是将节区对应的数据进行解密。

21.4.2 解密节区内容

加密的EXE程序运行前,需要先将补丁工具加密的节区数据解密。由于加密使用了不变长度的算法,解密后的节区数据大小和解密前一样;所以,解密时不需要额外构造解密用的缓冲区,相对比较简单。首先来看解密函数。

1.解密函数_UnEncrptIt

解密函数的运行依赖于加密时创建的基表。解密以字节为单位进行,每个字节的解密与其他字节没有关系,解密后的数据大小与解密前大小一致。解密函数见代码清单21-6。

代码清单21-6 解密函数_UnEncrptIt(chapter21\b\patch.asm)

65 ;------------------------------- 66 ; 解密算法,可逆算法,字节数不变 67 ; 入口参数: 68 ; _src:要解密的字节码起始地址 69 ; _size:要加密的字节码的数量 70 ;------------------------------- 71 _UnEncrptIt proc _src,_size,_writeProcessMemory 72 local @ret 73 local @dwTemp 74 75 pushad 76 ;开始按照基表对字节进行加密 77 mov esi,_src 78 .while TRUE 79 mov al,byte ptr [esi] 80 mov edi,offset EncryptionTable 81 add edi,ebx 82 mov @dwTemp,0 83 .while TRUE ;查找基表,索引在@dwTemp中 84 mov cl,byte ptr [edi] 85 .break .if al==cl ;如果找到,则退出 86 inc @dwTemp 87 inc edi 88 .endw 89 90 ;用解密后的字节更新字节码 91 mov ecx,@dwTemp 92 mov byte ptr dbEncrptValue[ebx],cl 93 94 ;使用远程写入 95 push NULL 96 push 1 97 mov edx,offset dbEncrptValue 98 add edx,ebx 99 push edx 100 push esi ;?? 101 push hProcess[ebx] 102 call _writeProcessMemory 103 104 inc esi 105 dec _size 106 .break .if _size==0 107 .endw 108 popad 109 ret 110 _UnEncrptIt endp

解密算法其实很简单,查找加密基表,如果在基表中找到指定字节码,则记录该位置在基表中的索引,这个索引值即为解密后的字节码,然后用这个字节码替换原来位置的字节值。

2.解密过程

程序将所有节的数据均进行一遍解密,还原目标程序为原始内容,然后才启动其他诸如动态加载DLL、修正IAT等操作。代码清单21-7是解密数据的代码。

504 ;解密数据 505 mov edi,dwImageBase[ebx] 506 assume edi:ptr IMAGE_DOS_HEADER 507 add edi,[edi].e_lfanew 508 assume edi:ptr IMAGE_NT_HEADERS 509 ;获取节的个数 510 movzx eax,[edi].FileHeader.NumberOfSections 511 mov dwSections[ebx],eax 512 add edi,sizeof IMAGE_NT_HEADERS 513 514 dec eax 515 mov ecx,sizeof IMAGE_SECTION_HEADER ;定位到最后一个节 516 mul ecx 517 add edi,eax 518 assume edi:ptr IMAGE_SECTION_HEADER 519 520 mov @first,1 521 522 .while TRUE 523 mov esi,[edi].VirtualAddress 524 add esi,dwImageBase[ebx] ;要解密的起始地址 525 526 .if @first ;如果是最后一节,补丁工具更改了此处的大小 527 ;必须使用由补丁工具传入的原始值 528 mov ecx,dwLastSectionSize[ebx] 529 mov @first,0 530 .else ;如果是其他节,则使用SizeOfRawData

代码清单21-7 解密节区数据过程(chapter21\b\patch.asm)

531 mov ecx,[edi].SizeOfRawData 532 .endif 533 534 push _writeProcessMemory 535 push ecx 536 push esi 537 mov edx,offset _UnEncrptIt 538 add edx,ebx 539 call edx 540 541 dec dwSections[ebx] 542 sub edi,sizeof IMAGE_SECTION_HEADER 543 .break .if dwSections[ebx]==0 544 .endw

如上所示,代码行505~518通过文件头部描述获取节的数量,并存储在变量dwSections中,然后将指针定位到最后一节。由于此时CPU的控制权尚处于补丁程序手里,所以最后一节的SizeOfRawData是被补丁工具修改以后的大小。如果用这个大小来解密数据,势必会越界,导致后面的补丁代码被修改,所以,最后一个节要解密的数据大小由补丁工具传进来的变量dwLastSectionSize决定,其他的节要解密的数据大小则直接通过每个节的SizeOfRawData来决定。

行522~544是解密目标进程所有节的一个循环。其中,变量@first如果为1,表示当前处理的是最后一节;否则,从最后一节向前处理,直到循环次数达到节的总数为止。

节区的数据解密完成,是否就可以跳转到目标进程的入口地址处运行了呢?答案是否定的。因为加密的目标PE文件在最初被操作系统加载器加载进内存时,是误认为该文件没有导入表的(导入表项被设置为0)。所以,记事本代码中用到的所有动态链接库在记事本进程地址空间中还不存在。如果补丁代码只还原完数据目录表,将加密的节区解密就直接转移到原始入口地址处运行,还不会成功;补丁代码需要继续完成对记事本导入表中登记的各模块的动态载入,以及IAT的修正后才会成功。

21.4.3 加载目标DLL

加密的目标PE文件被加载进内存后,其原始的导入信息丢失。加载器不会将其导入表中的动态链接库加载进进程的地址空间,这个操作必须由补丁程序代为完成。

首先,利用小工具程序PEInfo查看notepad.exe导入表中都引入了哪些动态链接库,这些链接库包括comdlg32.dll、SHELL32.dll、WINSPOOL.DRV、COMCTL32.dll、msvcrt.dll、ADVAPI32.dll、KERNEL32.dll、GDI32.dll和USER32.dll。但是,从OD加载的记事本进程地址空间中看到的链接库SECUR32.DLL、USP10.DLL、SHLWAPI.DLL、RPCRT4.DLL、LPK. DLL、IMM32.DLL并未出现在NOTEPAD.EXE的导入表定义中。这说明,这些动态链接库的导入应该是包含在其他动态链接库中的,例如:

❑ WINSPOOL.DRV→RPCRT4.DLL

❑ COMDLG32.DLL→SHLWAPI.DLL

❑ LPK.DLL→USP10.DLL

接下来,要为补丁代码增加动态加载DLL功能。通过遍历目标程序的导入表,调用LoadLibraryA函数,将导入表中列出的所有模块加载到内存中,并记录各模块的基地址。详细代码见代码清单21-8。

278 ;获取目标进程的基地址 279 mov eax,offset dwImageBase 280 add eax,ebx 281 282 push eax 283 lea edx,_getImageBase 284 add edx,ebx 285 call edx 286 mov dwImageBase[ebx],eax 287 288 ;遍历目标进程导入表 289 mov edi,offset dstDataDirectory 290 add edi,ebx 291 add edi,8 ;定位到导入表项 292 293 mov eax,dword ptr [edi] ;获取VirtualAddress 294 ;未做判断,假设处理的PE文件均有导入表 295 add eax,dwImageBase[ebx] ;所在内存偏移 296 297 mov edi,eax ;计算引入表所在文件偏移位置 298 assume edi:ptr IMAGE_IMPORT_DESCRIPTOR 299 300 mov eax,dword ptr [edi].Name1 ;取第一个动态链接库名字字符串所在的RVA值 301 add eax,dwImageBase[ebx] ;在内存定位只需加上基地址即可 302 ;invoke _messageBox,NULL,eax,NULL,MB_OK 303 304 ;动态加载该DLL 305 invoke _loadLibrary,eax 306 mov dwModuleBase[ebx],eax

代码清单21-8 动态加载目标进程导入表中登记的DLL(chapter21\patch2.asm)

只加载一个DLL(comdlg32.dll)的示例可以调试chapter21\bindC.exe文件。在OD环境下对比动态加载前后的内存分配,可以看到,comdlg32.dll被正确地加入到地址0x76320000起始处。以下是加载所有的动态链接库代码(chapter21\a\patch.asm):

...... mov edi,eax ;计算引入表所在文件偏移位置 assume edi:ptr IMAGE_IMPORT_DESCRIPTOR .while [edi].Name1 ;循环结束条件,只需简单判断Name1是否为0即可 push edi mov eax,dword ptr [edi].Name1 ;取第一个动态链接库名字字符串所在的RVA值 add eax,dwImageBase[ebx] ;在内存定位只需加上基地址即可 ;动态加载该DLL invoke _loadLibrary,eax mov dwModuleBase[ebx],eax ;修正从该链接库引入的函数IAT项 ;----------------------------- pop edi add edi,sizeof IMAGE_IMPORT_DESCRIPTOR .endw

离最终可运行的目标越来越近了,最后一步是修正目标文件的IAT。

21.4.4 修正目标IAT

引入的动态链接库被动态加载到内存以后,接下来要做的就是修正目标进程中的IAT内容。从导入表中得到每个函数的名称字符串,然后,从加载的模块基地址获取该函数在内存的VA值,并填到对应的IAT位置。

下面开始IAT修复工作,具体代码见代码清单21-9。

代码清单21-9 修正目标进程IAT的_updateIAT函数(chapter21\a\patch.asm)

240 ;------------------ 241 ; 修正IAT表 242 ; 传入全局变量参数 243 ; dwModuleBase 模块的地址 244 ; dwImageBase 进程基地址 245 ;------------------ 246 _updateIAT proc _lpIID,_writeProcessMemory 247 local @dwCount 248 249 pushad 250 mov @dwCount,0 251 252 mov edi,_lpIID 253 assume edi:ptr IMAGE_IMPORT_DESCRIPTOR 254 255 ;获取函数名字字符串 256 mov esi,[edi].OriginalFirstThunk 257 add esi,dwImageBase[ebx] 258 .while TRUE 259 mov eax,[esi] 260 .break .if !eax 261 add eax,dwImageBase[ebx] 262 add eax,2 ;跳过hint/name中的hint 263 264 ;此时eax指向了函数字符串 265 lea edx,_getApi ;获取函数地址 266 add edx,ebx 267 push eax 268 push dwModuleBase[ebx] 269 call edx 270 ;add eax,dwImageBase[ebx] ;获取函数VA值 271 272 ;将函数地址覆盖IAT对应位置 273 push esi 274 push eax 275 mov esi,[edi].FirstThunk 276 add esi,dwImageBase[ebx] ;esi指向IAT表开始 277 278 mov eax,@dwCount ;求索引对应偏移 279 sal eax,2 280 add esi,eax 281 pop eax 282 283 284 mov dwIATValue[ebx],eax 285 ;使用远程写入 286 push NULL 287 push 4 ; 写入长度 288 mov edx,offset dwIATValue 289 add edx,ebx 290 push edx ; 写入的值所在缓冲区 291 push esi ; 写入起始地址 292 push hProcess[ebx] 293 call _writeProcessMemory 294 295 ;mov dword ptr [esi],eax ;将函数VA值写入IAT 296 pop esi 297 298 inc @dwCount 299 add esi,4 300 .endw 301 302 popad 303 ret 304 _updateIAT endp

调用函数_updateIAT前,目标程序的数据目录表数据已经得到恢复。dwModuleBase中存放了当前动态加载的模块的基地址,dwImageBase中存放了目标进程的基地址。hProcess为打开的目标进程(为内存写作准备的)句柄。函数传入两个参数:参数1是_lpIID,该参数指向当前导入描述IMAGE_IMPORT_DESCRIPTOR结构;参数2为WriteProcessMemory的函数VA。

函数首先通过IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk找到函数名字字符串(注意,要跳过Hint/Name前的2个字节值);然后,通过调用_getApi函数获取该函数指令字节码所在内存的地址,并将该地址保存到IAT的相关项位置。

通过以上几步,对EXE加密、解密、运行的任务就完成了。运行补丁后的chapter21\b\path_notepad.exe程序,先出现补丁代码中的对话框提示,然后出现记事本程序。通过PEInfo小工具查看该程序显示的信息中大部分与记事本程序无关。

----------------------------------测试--------------------------------

打开补丁程序patch_a.exe

打开PE文件notepad.exe

生成bind21a.exe

在C盘目录下

用peInfo工具查看

运行:

打开是正常的,说明运行时解密

===============================

bind21b.exe测试

打开PE文件notepad.exe记事本程序

生成bind21b.exe

生成在C盘目录下:

用peInfo工具查看

运行:

21.5 小结

本章以记事本为例,向大家介绍了一种加密EXE文件的思路。方法是对目标PE文件的节区进行加密;运行时由补丁代码接管控制权,实施解密,并重新组织原程序的运行。本章用到了第17章中介绍的补丁工具。

本实例的加密方法很简单,大家可以根据实际需要对加密算法进行改进,以满足一些特殊需要。注意,本章介绍的代码只适合存在导入表,且导入函数均为名称导入的目标PE程序,如果大家想让其适应更多的程序,请根据所学知识自行修改。

http://www.jsqmd.com/news/697230/

相关文章:

  • 别再只用Ctrl+C/V了!这10个OneNote快捷键,让你在Windows上记笔记效率翻倍
  • MATLAB网格线进阶:从基础显示到自定义布局与样式
  • 从恒流源到互补推挽:手把手拆解LF411运放芯片内部电路,看懂每个晶体管的作用
  • 避坑指南:搞定Kylin V10+Samba共享,解决‘没有权限’和Windows访问失败的那些坑
  • 5步掌握Blender 3MF插件:3D打印文件导入导出完整指南
  • 思源黑体TTF实战指南:多语言字体渲染优化的终极解决方案
  • InfiAgent:从智能体到基础模型的架构跃迁与实战解析
  • lvgl_v8之动态添加控件代码示例
  • Qwen3.5-4B-AWQ实战教程:supervisor管理服务+日志定位+崩溃自恢复
  • 机器学习数据预处理实战:20+技巧提升模型效果
  • 从游戏角色瞄准到机械臂抓取:详解‘圆外一点求切线切点’的几何编程实战
  • SSC工具详解:从ESI文件生成到CiA402伺服驱动从站配置实战
  • 别再傻傻分不清了!Protobuf序列化时,SerializeToString和SerializePartialToString到底该用哪个?
  • Unity进阶:巧用FBX Exporter打通3DMax到Unity的无损数据管道
  • Java的java.util.random测试使用
  • 解锁B站视频自由:开源下载工具全解析与实战指南
  • 用Unity 2D复刻经典:如何为你的“Ruby‘s Adventure”添加完整的任务系统与NPC对话(含C#脚本详解)
  • 告别pip依赖地狱:从ERROR到成功安装的实战解决指南
  • FLAH写入和写出不一致怎么办?
  • Keil安装路径非默认导致DFP下载失败的排查与修复指南
  • 从AutoCAD到Revit:手把手教你用AutoLISP脚本批量导出天正墙体数据
  • py每日spider案例之某kedou视频解析参数逆向
  • 别再死记硬背了!用华为eNSP模拟器实战拆解OSPF的5种网络类型(BMA/P2P/P2MP/NBMA)
  • MT4 EA避坑指南:从Nerve Knife策略看如何设计‘永不爆仓’的风控模块
  • Linux系统之rename命令的版本差异与实战场景
  • DataX新手入门:5分钟搞定你的第一个数据同步任务(StreamReader到StreamWriter实战)
  • 别再傻傻分不清!STM32下载器STLINK和USB-TTL到底怎么选?附FlyMcu救砖指南
  • 如何在GTA V中安全使用YimMenu开源模组菜单:新手避坑指南
  • 第73篇:AI驱动市场研究与竞品分析——自动抓取、情感分析与趋势报告生成(项目实战)
  • 【嵌入式AI落地黄金公式】:3类芯片(STM32H7/ESP32-C3/NXP RT1170)+4种C内存模型+1套LLM适配框架=工业级边缘智能