整理了一篇,马上发上来,我算是怕了。发上估计是最安全了。
写的时候是HTML形式,拷贝到这里后,格式和色彩标记都会去掉。
着色的还是看附件好了。
=========================================================================
【文章标题】: ASPack 2.12 外壳流程分析
【作者声明】: 初学脱壳,没什么好东西。个人的一些经验,希望与大家分享,错误之处,欢迎指正!
【测试软件】:Notepad + ASPack
--------------------------------------------------------------------------------
【详细过程】
从脱壳的角度来说ASPack 1.08.03, 2.11 和 2.12 这3个版本都非常相似。
在流程分析中,我只对2.12作了详细的分析,其他两个也就没必要了。
――――――――――――――――――――脱壳流程(ASPack 2.12)――――――――――――――――――――
01013000 90 nop
01013001 > 60 pushad
01013002 E8 03000000 call 0101300A ; 进
01013007 - E9 EB045D45 jmp 465E34F7
0101300C 55 push ebp
0101300D C3 retn
0101300E E8 01000000 call 01013014 ; 进
01013013 90 nop ; 花指令
01013014 5D pop ebp ; Notepad_.01013013
01013015 BB EDFFFFFF mov ebx, -13
0101301A 03DD add ebx, ebp
0101301C 81EB 00300100 sub ebx, 13000
01013022 83BD 22040000 0>cmp dword ptr [ebp+422], 0
01013029 899D 22040000 mov [ebp+422], ebx
0101302F 0F85 65030000 jnz 0101339A ; 长跳转跟随
01013035 8D85 2E040000 lea eax, [ebp+42E]
0101303B 50 push eax
0101303C FF95 4D0F0000 call [ebp+F4D]
01013042 8985 26040000 mov [ebp+426], eax
01013048 8BF8 mov edi, eax
0101304A 8D5D 5E lea ebx, [ebp+5E]
跟随后来到:
0101339A B8 9D730000 mov eax, 739D ; 鼠标点击这一行,F4
0101339F 50 push eax
010133A0 0385 22040000 add eax, [ebp+422]
010133A6 59 pop ecx
010133A7 0BC9 or ecx, ecx
010133A9 8985 A8030000 mov [ebp+3A8], eax ;执行这一行之后,OEP值出现
010133AF 61 popad
010133B0 75 08 jnz short 010133BA
010133B2 B8 01000000 mov eax, 1
010133B7 C2 0C00 retn 0C
010133BA 68 00000000 push 0 ; OEP
010133BF C3 retn
用LordPE 来DUMP,直接ImportRec修复即可
(1)与1.08.03相比较没有多大变化,只是在壳入口处增加了花指令及近跳转变形CALL。
(2)长跳转依然存在,脱起来还是这么方便。
步骤总结:直接在壳入口处找到jnz的一个长跳转,跟随,OEP就在下面几行了。
脱这个壳没有多大意义,
---------------------------------ASPack 2.12 完全分析 --------------------------
首先观察区块表:
3个经过压缩的原始区块,".text",".data",".rsrc".
".aspck"块为壳的代码块
".adata"块为壳的数据块
OD载入:
01013001 > 60 pushad
01013002 E8 03000000 call 0101300A ; 进
01013007 - E9 EB045D45 jmp 465E34F7
0101300C 55 push ebp
0101300D C3 retn
0101300E E8 01000000 call 01013014 ; 进
01013013 EB 5D jmp short 01013072 ; 花指令
01013015 BB EDFFFFFF mov ebx, -13
0101301A 03DD add ebx, ebp
0101301C 81EB 00300100 sub ebx, 13000
01013022 83BD 22040000 >cmp dword ptr [ebp+422], >
01013029 899D 22040000 mov [ebp+422], ebx
0101302F 0F85 65030000 jnz 0101339A ; 跟随
01013035 8D85 2E040000 lea eax, [ebp+42E]
0101303B 50 push eax ; 字符串"kernel32.dll"
0101303C FF95 4D0F0000 call [ebp+F4D] ; CALL kernel32.GetModuleHandleA
01013042 8985 26040000 mov [ebp+426], eax
01013048 8BF8 mov edi, eax
0101304A 8D5D 5E lea ebx, [ebp+5E]
0101304D 53 push ebx ; 字符串“VirtualAlloc”
0101304E 50 push eax
0101304F FF95 490F0000 call [ebp+F49] ; CALL kernel32.GetProcAddress
01013055 8985 4D050000 mov [ebp+54D], eax
0101305B 8D5D 6B lea ebx, [ebp+6B]
0101305E 53 push ebx ; 字符串“VirtualFree”
0101305F 57 push edi
01013060 FF95 490F0000 call [ebp+F49] ; CALL kernel32.GetProcAddress
01013066 8985 51050000 mov [ebp+551], eax
0101306C 8D45 77 lea eax, [ebp+77]
0101306F FFE0 jmp eax ; Notepad_.0101308A
取得VirtualAlloc和VirtualFree地址,分别保存在[ebp+426]和[ebp+54D]位置
0101308A 8B9D 31050000 mov ebx, [ebp+531] ; 跳转到这里(有花指令)
01013090 0BDB or ebx, ebx
01013092 74 0A je short 0101309E ; 跳
01013094 8B03 mov eax, [ebx]
01013096 8785 35050000 xchg [ebp+535], eax
0101309C 8903 mov [ebx], eax
0101309E 8DB5 69050000 lea esi, [ebp+569] ; esi = 0X101357C,指向原始区块信息(数据窗口跟随esi,如下图所示)
010130A4 833E 00 cmp dword ptr [esi], 0 ;
010130A7 0F84 21010000 je 010131CE ;
010130AD 6A 04 push 4
010130AF 68 00100000 push 1000
010130B4 68 00180000 push 1800 ; size = 0x1800,(*".data"段长度?*)
010130B9 6A 00 push 0
010130BB FF95 4D050000 call [ebp+54D] ; CALL kernel32.VirtualAlloc
010130C1 8985 56010000 mov [ebp+156], eax ; EBP[156] = 0x930000:缓冲区起始地址(块一)
图中蓝色部分数据,显然为3个区块的起始地址和长度(看到这些数据,比较敏感)。经过进一步分析,如0x0000B568,虽然不是".rsrc"块的起始地址,但在".rsrc"内。且0XB568 + 0X7A98 = 0X8000,刚好与".rsrc"块的长度相同,这也就说明ASPack在对资源段进行压缩时并不是从起始位置开始的,而是压缩距离资源段偏移位置(0X568--0X8000)部分。
下面就进入了区块解压部分(在本例中下面这段代码会执行3次,每一次执行意味着解压一个区块):
010130C7 8B46 04 mov eax, [esi+4] ; 取得当前要解压块长度,保存在eax
010130CA 05 0E010000 add eax, 10E ; eax += 0x10E
010130CF 6A 04 push 4
010130D1 68 00100000 push 1000
010130D6 50 push eax ; 缓冲区长度
010130D7 6A 00 push 0
010130D9 FF95 4D050000 call [ebp+54D] ; CALL kernel32.VirtualAlloc,分配解压缓冲区
010130DF 8985 52010000 mov [ebp+152], eax ; EBP[152] = 0x940000(解压缓冲区起始地址)
010130E5 56 push esi
010130E6 8B1E mov ebx, [esi]
010130E8 039D 22040000 add ebx, [ebp+422]
010130EE FFB5 56010000 push dword ptr [ebp+156] ; 参数4:缓冲区一起始地址
010130F4 FF76 04 push dword ptr [esi+4] ; 参数3:解压区块数据长度
010130F7 50 push eax ; 参数2:解压缓冲区起始地址
010130F8 53 push ebx ; 参数1:解压区块起始地址
010130F9 E8 6E050000 call 0101366C ; 取得解压数据保存在解压缓冲区中
010130FE B3 01 mov bl, 0 ; 这条指令的第二操作数会变化(如果是代码段为1,其他为0)
01013100 80FB 00 cmp bl, 0
01013103 75 5E jnz short 01013163 ; 事实上,当前不是代码段时,会跳过下面这段蓝色代码
01013105 FE85 EC000000 inc byte ptr [ebp+EC]
0101310B 8B3E mov edi, [esi]
0101310D 03BD 22040000 add edi, [ebp+422]
01013113 FF37 push dword ptr [edi] ;干扰指令(这里开始的4个指令)
01013115 C607 C3 mov byte ptr [edi], 0C3 ; 把0x1001000处的第一字节改为0XC3,即ret指令
01013118 FFD7 call edi ; 变形CALL,相当于无效指令(edx = 0X1001000)
0101311A 8F07 pop dword ptr [edi]
0101311C 50 push eax
0101311D 51 push ecx
0101311E 56 push esi
0101311F 53 push ebx
01013120 8BC8 mov ecx, eax
01013122 83E9 06 sub ecx, 6
01013125 8BB5 52010000 mov esi, [ebp+152]
0101312B 33DB xor ebx, ebx
0101312D 0BC9 or ecx, ecx
0101312F 74 2E je short 0101315F
01013131 78 2C js short 0101315F
01013133 AC lods byte ptr [esi]
01013134 3C E8 cmp al, 0E8
01013136 74 0A je short 01013142
01013138 EB 00 jmp short 0101313A
0101313A 3C E9 cmp al, 0E9
0101313C 74 04 je short 01013142
0101313E 43 inc ebx
0101313F 49 dec ecx
01013140 ^ EB EB jmp short 0101312D
01013142 8B06 mov eax, [esi]
01013144 EB 00 jmp short 01013146 ; 无效指令,相当于NOP
01013146 803E 06 cmp byte ptr [esi], 6
01013149 ^ 75 F3 jnz short 0101313E
0101314B 24 00 and al, 0
0101314D C1C0 18 rol eax, 18
01013150 2BC3 sub eax, ebx
01013152 8906 mov [esi], eax
01013154 83C3 05 add ebx, 5
01013157 83C6 04 add esi, 4
0101315A 83E9 05 sub ecx, 5
0101315D ^ EB CE jmp short 0101312D
0101315F 5B pop ebx
01013160 5E pop esi
01013161 59 pop ecx
01013162 58 pop eax
01013163 EB 08 jmp short 0101316D ;不是代码段时跳到这里
01013165 0000 add [eax], al
01013167 94 xchg eax, esp
01013168 0000 add [eax], al
0101316A 90 nop
0101316B 90 nop
0101316C 90 nop
0101316D 8BC8 mov ecx, eax
0101316F 8B3E mov edi, [esi]
01013171 03BD 22040000 add edi, [ebp+422] ; edi 指向区块起始地址
01013177 8BB5 52010000 mov esi, [ebp+152] ; esi 指向解压缓冲区
0101317D C1F9 02 sar ecx, 2 ; ecx 保存该区块解压数据的长度
01013180 F3:A5 rep movs dword ptr es:[edi], dword ptr [esi] ; 区段数据覆盖(对0x1001000设置内存写入断点,第3次中断来到这里,因为前面的一个干扰指令会在这个地址写入ret和擦除。中断后要立即撤消该断点)
01013182 8BC8 mov ecx, eax
01013184 83E1 03 and ecx, 3 ; 为了保证4字节对齐
01013187 F3:A4 rep movs byte ptr es:[edi], byte ptr [esi]
01013189 5E pop esi
0101318A 68 00800000 push 8000
0101318F 6A 00 push 0
01013191 FFB5 52010000 push dword ptr [ebp+152] ; push 0x940000,释放解压缓冲区
01013197 FF95 51050000 call [ebp+551] ; CALL kernel32.VirtualFree
0101319D 83C6 08 add esi, 8 ;取下一个区块信息
010131A0 833E 00 cmp dword ptr [esi], 0 ; 检测下一区块信息是否为空
010131A3 ^ 0F85 1EFFFFFF jnz 010130C7 ; 不为空则跳转(在本例中跳转3次)
010131A9 68 00800000 push 8000 ; 一旦到了这里,就表示解压完成
010131AE 6A 00 push 0
010131B0 FFB5 56010000 push dword ptr [ebp+156] ; push 0x930000,释放第一个缓冲区
010131B6 FF95 51050000 call [ebp+551] ; CALL kernel32.VirtualFree
010131BC 8B9D 31050000 mov ebx, [ebp+531]
010131C2 0BDB or ebx, ebx
010131C4 74 08 je short 010131CE ; 跳
010131C6 8B03 mov eax, [ebx]
010131C8 8785 35050000 xchg [ebp+535], eax
010131CE 8B95 22040000 mov edx, [ebp+422] ; edx = 0x1000000,基地址
010131D4 8B85 2D050000 mov eax, [ebp+52D] ; eax = 0x1000000,基地址
010131DA 2BD0 sub edx, eax
010131DC 74 79 je short 01013257 ; 跳
到这里外壳部分已经对软件的3个区块进行了解压
下面开始进入的IAT的还原部分
01013257 8B95 22040000 mov edx, [ebp+422] ; edx = 0x1000000
0101325D 8BB5 41050000 mov esi, [ebp+541] ; esi = 0
01013263 0BF6 or esi, esi
01013265 74 11 je short 01013278 ; 跳
01013278 BE 04760000 mov esi, 7604 ; 7604为输入表的偏移地址(壳备份)
0101327D 8B95 22040000 mov edx, [ebp+422] ; edx = 0x1000000
01013283 03F2 add esi, edx ; esi = 0x1007604指向备份输入表
01013285 8B46 0C mov eax,[esi+C] ; ImportTable->dwDllNameOffset(外循环起始)
01013288 85C0 test eax, eax ; 测试esi所指向的输入表项是否为空(即DLL是否处理完了)
0101328A 0F84 0A010000 je 0101339A ; 若为空,就跳出(跳出外循环)
01013290 03C2 add eax, edx ; DLL名字的偏移地址+基地址=虚拟内存地址
01013292 8BD8 mov ebx, eax
01013294 50 push eax ; eax指向DLL名字的字符串
01013295 FF95 4D0F0000 call [ebp+F4D] ; CALL kernel32.GetModuleHandleA
0101329B 85C0 test eax, eax
0101329D 75 07 jnz short 010132A6 ; 取得DLL模块句柄成功,则跳转
0101329F 53 push ebx
010132A0 FF95 510F0000 call [ebp+F51]
010132A6 8985 45050000 mov [ebp+545], eax ; 把dll模块地址保存在[ebp+545]
010132AC C785 49050000 >mov dword ptr [ebp+549], 0 ; [ebp+549]保存着地址表的偏移地址
010132B6 8B95 22040000 mov edx, [ebp+422] ; edx 为基地址(内循环起始)
010132BC 8B06 mov eax, [esi] ; eax 为IT->OriginalFirstThunk表的偏移地址
010132BE 85C0 test eax, eax ; 测试DLL的IT->OriginalFirstThunk项是否为空
010132C0 75 03 jnz short 010132C5 ; 若不为空则跳转
010132C2 8B46 10 mov eax, [esi+10] ; IT->OriginalThunk项为空,eax为IT->FirstThunk
010132C5 03C2 add eax, edx ; eax = eax + 基地址
010132C7 0385 49050000 add eax, [ebp+549] ; eax = eax + 地址表偏移地址
010132CD 8B18 mov ebx, [eax] ; ebx为OriginalFirstThunk表中的一个值(表示当前dll中的某个函数的地址)
010132CF 8B7E 10 mov edi, [esi+10] ; edi为IT->FirstThunk表的偏移地址,即IAT表
010132D2 03FA add edi, edx ; edi = edi + 基地址
010132D4 03BD 49050000 add edi, [ebp+549] ; eax = eax + 地址表偏移地址
010132DA 85DB test ebx, ebx ; 如果ebx=0,意味当前dll的所有函数入口地址都已经处理完了
010132DC 0F84 A2000000 je 01013384
010132E2 F7C3 00000080 test ebx, 80000000
010132E8 75 04 jnz short 010132EE
010132EA 03DA add ebx, edx ; ebx = ebx + 基地址
010132EC 43 inc ebx
010132ED 43 inc ebx
010132EE 53 push ebx
010132EF 81E3 FFFFFF7F and ebx, 7FFFFFFF
010132F5 53 push ebx ; ebx指向函数名
010132F6 FFB5 45050000 push dword ptr [ebp+545] ; [ebp+545]为dll模块句柄
010132FC FF95 490F0000 call [ebp+F49] ; CALL kernel32.GetProcAddress
01013302 85C0 test eax, eax
01013304 5B pop ebx
01013305 75 6F jnz short 01013376 ; 取函数地址成功,就跳转
……(忽略一段除错处理)
01013376 8907 mov [edi], eax ; 跳到这里,把所取得的函数地址存在IAT中
01013378 8385 49050000 >add dword ptr [ebp+549], 4 ; 下一个函数,增加偏移量
0101337F ^ E9 32FFFFFF jmp 010132B6 ; 继续找函数地址(内循环尾部)
01013384 8906 mov [esi], eax
01013386 8946 0C mov [esi+C], eax
01013389 8946 10 mov [esi+10], eax
0101338C 83C6 14 add esi, 14 ; esi指向输入表的下一个表项
0101338F 8B95 22040000 mov edx, [ebp+422]
01013395 E9 EBFEFFFF jmp 01013285 ; 继续枚举dll输入表项(外循环尾部)
上面的双重循环是用来恢复IAT的,工作流程如下所示:
for ( 每一个输入表项 )
{
for ( 每一个函数 )
{
取函数地址
写入IAT
}
}
跳出上面的双重循环后,来到这里:
0101339A B8 9D730000 mov eax, 739D ; 0x739D为OEP偏移
0101339F 50 push eax
010133A0 0385 22040000 add eax, [ebp+422] ; eax = eax + 基地址
010133A6 59 pop ecx
010133A7 0BC9 or ecx, ecx
010133A9 8985 A8030000 mov [ebp+3A8], eax ;改写OEP存储地址
010133AF 61 popad
010133B0 75 08 jnz short 010133BA
010133B2 B8 01000000 mov eax, 1
010133B7 C2 0C00 retn 0C
010133BA 68 9D730001 push 0100739D ; OEP = 0X0100739D,动态生成
010133BF C3 retn
――――――――――――――――――――――――――――――――――――――
总结ASPack2.12外壳执行流程:
1、取得VirtualAlloc和VirtualFree函数地址
2、分配缓冲区,解压区块(此例中为3个区块),释放缓冲区
3、双重循环,填充IAT表
4、转到OEP
=========================================================================================
(说明:DLB是为了便于我自己的理解而起的名字,作为心得写在这里。如果哪位觉得无聊,就别看了。想找我拍砖的,就免了。)
DLB(Double Loop Body)原理:
对于一般的压缩壳,外壳需要先解压缩,然后再填充IAT。|
而在填充IAT的时候,通常都会调用GetModuleHandleA,就利用这一点,返回到用户空间之后,
要做的第一件事就找内层循环体,然后再找外层循环体。而当找到“双重循环体(DLB)”之后,OEP就不远了。
在这里提出DLB原理,因为当我把ASPack 2.12分析完之后,我把ASPack的DLB当作标准DLB,
因为在后面的实践中,我发现DLB的形式并不都是这样,有一定的变形,但本质都是一样。
再来回顾一下DLB的特征:
for ( 每一个输入表项 )
{
取得模块地址(GetModuleHandleA),当若干次中断,然后返回到用户空间的时候,在这里的下一行
for ( 每一个函数 )
{
取函数地址
写入IAT
}
}
当使用外壳是调用系统的GetModuleHandleA,可以直接设断,中段若干次后返回到用户空间,
返回的地址必定在DLB中,然后找DLB的两个边界。
如果壳中自己实现了这个函数,那就不能直接对这个函数设置断点,需要通过分析来找DLB了。
要熟练利用DLB原理的前提:非常熟悉PE结构,对一些数据很敏感,例如IAT项大小为0X14等。
需要对IAT的填充形式非常清楚,最好能写个程序测试下,仔细观察DLB实现中的每个细节。
关于DLB原理的实践应用,将整理到下一篇。
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!