【前言】没有其他目的,只是作个学习笔记存档,兼与与我这样的菜鸟交流。错误之处,欢迎批评指正。
【软件】加了ASPack 2.12壳的KillBox.exe,一个小工具程序,由天草老师教程中提供。
【工具】OD、PEiD、ImportREC
用PEiD查一下,是ASPack 2.12 - Alexey Solodovnikov壳,下面OD载入,开始分析。
程序入口:
00424001 K> 60 pushad ; 程序入口(EP)
00424002 E8 03000000 call KillBox.0042400A ; =======================
00424007 - E9 EB045D45 jmp 459F44F7 ; 这4行都是花指令
0042400C 55 push ebp ;
0042400D C3 retn ; =======================
0042400E E8 01000000 call KillBox.00424014 ; 目标地址隐藏了,单步进去
00424013 EB 5D jmp short KillBox.00424072 ; EB花指令,隐藏了上面call的目标地址
单步进去后是这样的:
00424014 5D pop ebp ; 代码重定位。弹出返回地址ebp=00424013
00424015 BB EDFFFFFF mov ebx,-13 ; 偏移量为13h字节
0042401A 03DD add ebx,ebp ; 00424013-13=00424000壳代码段起始位置
0042401C 81EB 00400200 sub ebx,24000 ; 00424000-24000=00400000(ImageBase)
00424022 83BD 22040000 00 cmp dword ptr [ebp+422],0 ; [ebp+422]=[00424435]=00000000
00424029 899D 22040000 mov dword ptr [ebp+422],ebx ; [ebp+422]存入00400000(ImageBase)
0042402F 0F85 65030000 jnz KillBox.0042439A ; 初始为0,进行解压、填充IAT处理,完成后再来这里判断,发现是00400000就去OEP)
00424035 8D85 2E040000 lea eax,dword ptr [ebp+42E] ; ASCII "kernel32.dll"
0042403B 50 push eax
0042403C FF95 4D0F0000 call dword ptr [ebp+F4D] ; GetModuleHandleA获取kernel32.dll句柄
00424042 8985 26040000 mov dword ptr [ebp+426],eax ; 保存句柄
00424048 8BF8 mov edi,eax
0042404A 8D5D 5E lea ebx,dword ptr [ebp+5E] ; ASCII "VirtualAlloc"
0042404D 53 push ebx
0042404E 50 push eax ; kernel32.dll句柄
0042404F FF95 490F0000 call dword ptr [ebp+F49] ; GetProcAddress获取kernel32.VirtualAlloc函数地址
00424055 8985 4D050000 mov dword ptr [ebp+54D],eax ; 保存函数地址
0042405B 8D5D 6B lea ebx,dword ptr [ebp+6B] ; ASCII "VirtualFree"
0042405E 53 push ebx
0042405F 57 push edi
00424060 FF95 490F0000 call dword ptr [ebp+F49] ; GetProcAddress获取kernel32.VirtualFree函数地址
00424066 8985 51050000 mov dword ptr [ebp+551],eax ; 保存函数地址
0042406C 8D45 77 lea eax,dword ptr [ebp+77] ; 地址=0042408A
0042406F FFE0 jmp eax ; 目标地址0042408A又藏花指令的后面了
跳到了这里,看到上面的两个函数,猜到下面可能是开始解压代码了。
0042408A 8B9D 31050000 mov ebx,dword ptr [ebp+531] ; ss:[00424544]=00000000
00424090 0BDB or ebx,ebx
00424092 /74 0A je short KillBox.0042409E ; 跳
00424094 |8B03 mov eax,dword ptr [ebx]
00424096 |8785 35050000 xchg dword ptr [ebp+535],eax
0042409C |8903 mov dword ptr [ebx],eax
0042409E \8DB5 69050000 lea esi,dword ptr [ebp+569] ; 地址=0042457C。这是什么地址?数据窗口看看,是一组数据:00001000、00020000、00021000、00001000、000230E8、00000F18,应该与解压区段有关系,这里暂称其为信息表吧。
004240A4 833E 00 cmp dword ptr [esi],0 ; 信息表第1项是否为0?(ds:[0042457C]=00001000)
004240A7 0F84 21010000 je KillBox.004241CE ; 非0不跳
004240AD 6A 04 push 4 ; flProtect=PAGE_READWRITE
004240AF 68 00100000 push 1000 ; flAllocationType=MEM_COMMIT提交物理内存
004240B4 68 00180000 push 1800 ; dwSize=1800h字节
004240B9 6A 00 push 0 ; lpAddress=NULL,不指定起始地址
004240BB FF95 4D050000 call dword ptr [ebp+54D] ; VirtualAlloc申请第一块内存
004240C1 8985 56010000 mov dword ptr [ebp+156],eax ; 保存申请到的内存起始地址。我这里是00D50000
这里开始一个大循环,各段解压:
004240C7 8B46 04 mov eax,dword ptr [esi+4] ; 信息表中取出第2项:00020000。从下面知道这是VirtualAlloc的dwSize,也就是要解压的区段的大小了。
004240CA 05 0E010000 add eax,10E ; 每个大小还要加上10Eh字节,为什么?
004240CF 6A 04 push 4 ; /flProtect=PAGE_READWRITE
004240D1 68 00100000 push 1000 ; |flAllocationType=MEM_COMMIT提交物理内存
004240D6 50 push eax ; |dwSize
004240D7 6A 00 push 0 ; |
004240D9 FF95 4D050000 call dword ptr [ebp+54D] ; \VirtualAlloc申请第二块内存,且称其为缓冲区吧
004240DF 8985 52010000 mov dword ptr [ebp+152],eax ; 保存申请到的内存起始地址。我这里是00D60000
004240E5 56 push esi ; 信息表地址入栈保存
004240E6 8B1E mov ebx,dword ptr [esi] ; 取出信息表第1项:00001000
004240E8 039D 22040000 add ebx,dword ptr [ebp+422] ; RVA+ImageBase
到这里明白了,信息表第1项是待解压的代码段起始地址的RVA,而第2项是大小,在内存窗口看看,果然是.text段的参数。再看剩下的两组数据:
00021000、00001000和000230E8、00000F18,分别是.data段的RVA和.rsrc段偏移E8字节的RVA,这里分别只解压1000h和0F18h字节。为什么会这样呢?代码段全部解压,数据段解压一半,而资源段只选择性的解压,大概是因为资源段不好被全部压缩,否则在WINDOWS下怎么能看到图标之类的信息呢?
004240EE FFB5 56010000 push dword ptr [ebp+156] ; 申请的第一块内存地址
004240F4 FF76 04 push dword ptr [esi+4] ; 信息表中取出的该段待解压数据大小
004240F7 50 push eax ; 缓冲区地址
004240F8 53 push ebx ; 待解压的起始地址
004240F9 E8 6E050000 call KillBox.0042466C ; 解压到缓冲区
004240FE B3 01 mov bl,0 ; 初始指令为:mov bl,0,00424105处指令将这里改为mov bl,1
00424100 80FB 00 cmp bl,0
00424103 75 5E jnz short KillBox.00424163 ; 初始时bl=0,所以不跳,先对解压到缓冲区中的代码中的call和jmp指令进行解密,再将缓冲区的数据(指令)复制到目标区段(代码段)。
00424105 FE85 EC000000 inc byte ptr [ebp+EC] ; 这里有玄机,将004240FE处指令改为“mov bl,1”,所以处理完代码段后对其他区段解压时,再来判断就跳了,直接复制解压后的数据到目标区段。
0042410B 8B3E mov edi,dword ptr [esi] ; ds:[0042457C]=00001000
0042410D 03BD 22040000 add edi,dword ptr [ebp+422] ; 1000h+ImageBase=代码段首地址
00424113 FF37 push dword ptr [edi] ; 00401000处4字节代码入栈保存
00424115 C607 C3 mov byte ptr [edi],0C3 ; 将00401000处1字节改为C3(retn)
00424118 FFD7 call edi ; edi=00401000 (retn)“到此一游”
{
00401000 C3 retn
}
0042411A 8F07 pop dword ptr [edi] ; 恢复00401000处4字节代码
0042411C 50 push eax
0042411D 51 push ecx
0042411E 56 push esi
0042411F 53 push ebx
00424120 8BC8 mov ecx,eax
00424122 83E9 06 sub ecx,6 ; ecx=00020000-6=0001FFFA
00424125 8BB5 52010000 mov esi,dword ptr [ebp+152] ; 缓冲区
0042412B 33DB xor ebx,ebx ; 初始化计数器
下面开始修改解压后的代码,将E804和E904开始的call和jmp目标地址解密:
0042412D / /0BC9 or ecx,ecx ; 查找范围字节数。初始=0001FFFA
0042412F | |74 2E je short KillBox.0042415F ; 为0则跳
00424131 | |78 2C js short KillBox.0042415F ; 为负数(即超出了搜索范围)则跳
00424133 | |AC lods byte ptr [esi] ; 从缓冲区查找特征字符(E8、E9)
00424134 | |3C E8 cmp al,0E8 ; 是否为call指令
00424136 | |74 0A je short KillBox.00424142 ; 为call指令则去解密
00424138 |/ |EB 00 jmp short KillBox.0042413A
0042413A || |3C E9 cmp al,0E9 ; 是否为jmp指令
0042413C ||/|74 04 je short KillBox.00424142 ; 为jmp指令则则去解密
0042413E ||||43 inc ebx ; 计数器加1
0042413F ||||49 dec ecx ; 查找范围字节数-1
00424140 |||\EB EB jmp short KillBox.0042412D
00424142 |\\ 8B06 mov eax,dword ptr [esi] ; E8或E9后面再取一字符,即jmp XXXXXXXX指令的目标地址最低位字节。
00424144 | EB 00 jmp short KillBox.00424146
00424146 | 803E 04 cmp byte ptr [esi],4 ; 判断是否“E904”或“E804”(00D602EC处才有E904,00D60DE5处才有E804)
00424149 | ^ 75 F3 jnz short KillBox.0042413E ; 不是继续循环搜索
0042414B | 24 00 and al,0 ; 对jmp和call的目标地址进行解密
0042414D | C1C0 18 rol eax,18
00424150 | 2BC3 sub eax,ebx
00424152 | 8906 mov dword ptr [esi],eax ; 修改jmp和call目标地址
00424154 | 83C3 05 add ebx,5 ; 计数器加5(jmp指令共5字节)
00424157 | 83C6 04 add esi,4 ; 指针后移4字节,即指向jmp指令后面的一条指令处
0042415A | 83E9 05 sub ecx,5 ; 查找范围字节数-5
0042415D \ ^ EB CE jmp short KillBox.0042412D
修改完毕后跳到这里:
0042415F 5B pop ebx
00424160 5E pop esi
00424161 59 pop ecx
00424162 58 pop eax
00424163 EB 08 jmp short KillBox.0042416D ; 解压其他区段时是直接跳到这里
跳到了这里,将缓冲区的代码(数据)复制到目标区段:
0042416D 8BC8 mov ecx,eax ; eax=解压数据大小
0042416F 8B3E mov edi,dword ptr [esi] ; 本区段待解压数据的起始地址RVA
00424171 03BD 22040000 add edi,dword ptr [ebp+422] ; RVA+ImageBase=目标地址
00424177 8BB5 52010000 mov esi,dword ptr [ebp+152] ; 缓冲区
0042417D C1F9 02 sar ecx,2
00424180 F3:A5 rep movs dword ptr es:[edi],dword ptr [esi] ; 复制
00424182 8BC8 mov ecx,eax
00424184 83E1 03 and ecx,3
00424187 F3:A4 rep movs byte ptr es:[edi],byte ptr [esi]
00424189 5E pop esi
0042418A 68 00800000 push 8000
0042418F 6A 00 push 0
00424191 FFB5 52010000 push dword ptr [ebp+152] ; 缓冲区
00424197 FF95 51050000 call dword ptr [ebp+551] ; VirtualFree释放缓冲区
0042419D 83C6 08 add esi,8 ; 信息表中指向下一个待解压的区段
004241A0 833E 00 cmp dword ptr [esi],0 ; 是否还有待解压的段?
004241A3 ^ 0F85 1EFFFFFF jnz KillBox.004240C7 ; 有则循环继续解压
至此解压全部完成,接下来清理内存:
004241A9 68 00800000 push 8000
004241AE 6A 00 push 0
004241B0 FFB5 56010000 push dword ptr [ebp+156] ; 申请的第一块内存地址
004241B6 FF95 51050000 call dword ptr [ebp+551] ; VirtualFree释放第一块内存
004241BC 8B9D 31050000 mov ebx,dword ptr [ebp+531] ; ss:[00424544]=00000000
004241C2 0BDB or ebx,ebx
004241C4 74 08 je short KillBox.004241CE ; 跳
004241C6 8B03 mov eax,dword ptr [ebx]
004241C8 8785 35050000 xchg dword ptr [ebp+535],eax
004241CE 8B95 22040000 mov edx,dword ptr [ebp+422] ; ImageBase
004241D4 8B85 2D050000 mov eax,dword ptr [ebp+52D] ; ImageBase
004241DA 2BD0 sub edx,eax
004241DC 74 79 je short KillBox.00424257 ; 相等,跳
至此,程序解压完毕,跳到了这里,开始填充IAT。
00424257 8B95 22040000 mov edx,dword ptr [ebp+422] ; ImageBase
0042425D 8BB5 41050000 mov esi,dword ptr [ebp+541] ; ss:[00424554]=00000000
00424263 0BF6 or esi,esi
00424265 /74 11 je short KillBox.00424278 ; 跳
00424267 |03F2 add esi,edx
00424269 |AD lods dword ptr [esi]
0042426A |0BC0 or eax,eax
0042426C |74 0A je short KillBox.00424278
0042426E |03C2 add eax,edx
00424270 |8BF8 mov edi,eax
00424272 |66:AD lods word ptr [esi]
00424274 |66:AB stos word ptr es:[edi]
00424276 ^|EB F1 jmp short KillBox.00424269
00424278 \BE B4F80100 mov esi,1F8B4 ; 跳到这里
0042427D 8B95 22040000 mov edx,dword ptr [ebp+422] ; ImageBase
00424283 03F2 add esi,edx ; 1F8B4+ImageBase定位到输入表
00424285 8B46 0C mov eax,dword ptr [esi+C] ; 取IID的NAME成员
00424288 85C0 test eax,eax ; NAME成员是否为0
0042428A 0F84 0A010000 je KillBox.0042439A ; 是,则跳,即IAT填充完毕就跳出循环。
00424290 03C2 add eax,edx ; 否,则定位dll,1FB94+ImageBase
00424292 8BD8 mov ebx,eax ; ASCII "MSVBVM60.DLL"(VB程序)
00424294 50 push eax
00424295 FF95 4D0F0000 call dword ptr [ebp+F4D] ; GetModuleHandleA获取dll句柄
0042429B 85C0 test eax,eax
0042429D 75 07 jnz short KillBox.004242A6 ; 成功则跳
0042429F 53 push ebx ; 否则LoadLibraryA动态调入dll库
004242A0 FF95 510F0000 call dword ptr [ebp+F51]
004242A6 8985 45050000 mov dword ptr [ebp+545],eax ; 保存dll句柄
004242AC C785 49050000 00000000 mov dword ptr [ebp+549],0 ; 初始IAT首地址偏移量为0,以后每填充完一个就+4
004242B6 8B95 22040000 mov edx,dword ptr [ebp+422] ; ImageBase
004242BC 8B06 mov eax,dword ptr [esi] ; [esi]处是输入表MSVBVM60.DLL的IID,这里取OriginalFirstThunk
004242BE 85C0 test eax,eax ; OriginalFirstThunk是否为0?
004242C0 /75 03 jnz short KillBox.004242C5 ; 否,则跳去找函数名(本例非0)
004242C2 |8B46 10 mov eax,dword ptr [esi+10] ; 是,则取FirstThunk(=00001000)
004242C5 \03C2 add eax,edx ; OriginalFirstThunk+ImageBase定位到IMAGE_THUNK_DATA数组首地址
004242C7 0385 49050000 add eax,dword ptr [ebp+549] ; IMAGE_THUNK_DATA数组首地址+偏移量(N*4字节),定位到待填充函数的IMAGE_THUNK_DATA地址
004242CD 8B18 mov ebx,dword ptr [eax] ; 取IMAGE_THUNK_DATA
004242CF 8B7E 10 mov edi,dword ptr [esi+10] ; 取FirstThunk
004242D2 03FA add edi,edx ; FirstThunk+ImageBase定位到IAT首地址
004242D4 03BD 49050000 add edi,dword ptr [ebp+549] ; IAT首地址+偏移量(N*4字节),定位到本次待填充的IAT目标地址
004242DA 85DB test ebx,ebx ; IMAGE_THUNK_DATA数组是否已到末尾?
004242DC 0F84 A2000000 je KillBox.00424384 ; 是,则结束
004242E2 F7C3 00000080 test ebx,80000000 ; 否,则判断该函数IMAGE_IMPORT_BY_NAME还是BY_INDEX方式导入。(在填充了部分函数后,我们可以看到本例是VB语言写的程序,以"rtc"开头的函数都是以序号导入的方式,其他函数的则是BY_NAME方式)
004242E8 75 04 jnz short KillBox.004242EE ; by_INDEX方式则跳。
004242EA 03DA add ebx,edx ; by_NAME方式则+ImageBase定位函数名
004242EC 43 inc ebx
004242ED 43 inc ebx ; 后移2位跳过成员Hint定位到函数名地址
004242EE 53 push ebx ; by_NAME方式时为函数名地址,by_INDEX方式是时为IMAGE_THUNK_DATA
004242EF 81E3 FFFFFF7F and ebx,7FFFFFFF ; 取序号导入的函数序号(按名称导入的不影响)
004242F5 53 push ebx ; 函数名地址或函数序号
004242F6 FFB5 45050000 push dword ptr [ebp+545] ; dll句柄
004242FC FF95 490F0000 call dword ptr [ebp+F49] ; GetProcAddress获取函数地址
00424302 85C0 test eax,eax
00424304 5B pop ebx
00424305 75 6F jnz short KillBox.00424376 ; 成功则跳
00424376 8907 mov dword ptr [edi],eax ; 跳到了这里,填充IAT
00424378 8385 49050000 04 add dword ptr [ebp+549],4 ; 距首地址偏移量+4,准备处理下一个函数
0042437F ^ E9 32FFFFFF jmp KillBox.004242B6 ; 循环处理
一个DLL的IAT填充完毕后跳转到这里:
00424384 8906 mov dword ptr [esi],eax ; OriginalFirstThunk成员指向00000000
00424386 8946 0C mov dword ptr [esi+C],eax ; NAME成员指向00000000
00424389 8946 10 mov dword ptr [esi+10],eax ; FirstThunk成员指向00000000
0042438C 83C6 14 add esi,14 ; 指针移到下一个IID
0042438F 8B95 22040000 mov edx,dword ptr [ebp+422] ; ImageBase
00424395 ^ E9 EBFEFFFF jmp KillBox.00424285
全部IAT填充完毕后跳到这里,下面开始处理OEP:
0042439A B8 60230000 mov eax,2360 ; OEP的RVA
0042439F 50 push eax
004243A0 0385 22040000 add eax,dword ptr [ebp+422] ; RVA+ImageBase = OEP
004243A6 59 pop ecx ; 弹出OEP的RVA
004243A7 0BC9 or ecx,ecx ; OEP的RVA是否为0
004243A9 8985 A8030000 mov dword ptr [ebp+3A8],eax ; 修改程序代码,将OEP写入压栈指令
修改程序代码之前下面的几行代码是这样的:
004243AF 61 popad
004243B0 75 08 jnz short KillBox.004243BA ; OEP的RVA不为0则跳,否则game over
004243B2 B8 01000000 mov eax,1
004243B7 C2 0C00 retn 0C
004243BA 68 00000000 push 0
004243BF C3 retn
修改程序代码后是这样的:
004243AF 61 popad
004243B0 75 08 jnz short KillBox.004243BA ; OEP的RVA不为0则跳,否则game over
004243B2 B8 01000000 mov eax,1
004243B7 C2 0C00 retn 0C
004243BA 68 60234000 push KillBox.00402360 ; 这里被修改成了push 00402360
004243BF C3 retn ; 前往OEP
到达OEP:
00402360 68 2C4D4000 push KillBox.00404D2C ; ASCII "VB5!6&*vb6chs.dll"
00402365 E8 EEFFFFFF call KillBox.00402358
0040236A 0000 add byte ptr [eax],al
0040236C 0000 add byte ptr [eax],al
0040236E 0000 add byte ptr [eax],al
00402370 3000 xor byte ptr [eax],al
00402372 0000 add byte ptr [eax],al
00402374 3800 cmp byte ptr [eax],al
00402376 0000 add byte ptr [eax],al
00402378 0000 add byte ptr [eax],al
0040237A 0000 add byte ptr [eax],al
0040237C 4F dec edi
0040237D C2 F150 retn 50F1
现在可以用OD的OllyDump插件脱壳了,用方式一脱后可运行;用方式二脱后不能运行,原因是方式二没能正确识别以序号方式导入的函数,均成为无效函数,需要修复输入表(用ImportREC即可)。
小结:
一、壳的工作流程是:
1、根据壳中的伪输入表,动态调入壳运行所需的函数。
2、解压。将原程序的各个段依次解压到缓冲区,再从缓冲区中复制到目标区段。如果是代码段,则在缓冲区中将所有E804和E904开头的call和jmp指令解密出正确的目标地址,再行复制。
3、填充IAT。保留了输入表,根据输入表动态加载dll库,再根据OriginalFirstThunk迭代搜索函数名,动态获取函数地址再填入IAT,填充完一个dll的函数后,却又把相应的IID的OriginalFirstThunk、Name、FirstThunk三个成员全指向了0。
4、转往OEP。壳通过自修改代码,以push+retn的方式跳往OEP。
二、程序保护方面
这个壳也是一个早期的压缩壳,所以在保护方法也没采取什么措施。下面是一些简单的措施:
1、采用了一些花指令干扰静态调试。
2、对部分call和jmp的目标地址似乎是加密了,不过解压后在缓冲区中又解密并复制到代码段中。
3、两次用了SMC技术,第一次却不是出于保护目的,而是为了控制正常流程(将mov bl,0 改为 mov bl,1),使程序除代码段之外不去执行额外的修改call和jmp目标地址的工作;第二次倒是起到干扰静态调试的作用(将push 0 改为 push 00402360)。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)