1.UPX壳
我们先来看一下数据是怎么被解压出来的,首先刚进入外壳时,就有这么几句代码:
/*462A40*/ pushad
/*462A41*/ mov esi,43F000
/*462A46*/ lea edi,dword ptr ds:[esi+FFFC2000]
/*462A4C*/ mov dword ptr ds:[edi+4C09C],BF0111E6
/*462A56*/ push edi
/*462A57*/ or ebp,FFFFFFFF
首先我们可以看到esi被赋值为43F000,那么看看这个43F000是什么东西,在数据窗口跳转过去之后看到了熟悉的Boolean,这是DELPHI程序CODE区段的典型特征,那么再来看下一句代码,
lea edi,dword ptr ds:[esi+FFFC2000]
这里对edi赋值了,动态调试会发现,其赋值结果为00401000
00401000是大多数WIN32程序第一个区段的起始地址,而且此时这里全部都是00,那么大胆的猜测一下,在解压过程中,esi指向压缩后的数据存放地址,edi指向解压后的数据存放地址;
那么接下来的解压算法就很简单了,因为我们这里并不去关注它具体的解压算法和压缩算法的细节,因为分析起来还是比较吃力的,暂时只关注它的解压所需的参数,以及解压的代码!那么这里简单的表达为如下代码(标注为XXXX的,都是加壳时产生的数值,并不固定):
mov esi,XXXXXXXX // 压缩后的数据存放地址
lea edi,[esi + XXXXXXXX] // 获取解压后的数据存放地址
mov dword ptr ds:[edi+XXXXX],XXXXXXXX // 可能是一个Key吧
// 准备参数,下面这段是解压算法
8A 06 46 88 07 47 01 DB 75 07 8B 1E 83 EE FC 11 DB 72 ED B8 01 00 00 00 01 DB 75 07 8B 1E 83 EE
FC 11 DB 11 C0 01 DB 73 0B 75 28 8B 1E 83 EE FC 11 DB 72 1F 48 01 DB 75 07 8B 1E 83 EE FC 11 DB
11 C0 EB D4 01 DB 75 07 8B 1E 83 EE FC 11 DB 11 C9 EB 52 31 C9 83 E8 03 72 11 C1 E0 08 8A 06 46
83 F0 FF 74 75 D1 F8 89 C5 EB 0B 01 DB 75 07 8B 1E 83 EE FC 11 DB 72 CC 41 01 DB 75 07 8B 1E 83
EE FC 11 DB 72 BE 01 DB 75 07 8B 1E 83 EE FC 11 DB 11 C9 01 DB 73 EF 75 09 8B 1E 83 EE FC 11 DB
73 E4 83 C1 02 81 FD 00 FB FF FF 83 D1 02 8D 14 2F 83 FD FC 76 0E 8A 02 42 88 07 47 49 75 F7 E9
42 FF FF FF 8B 02 83 C2 04 89 07 83 C7 04 83 E9 04 77 F1 01 CF E9 2C FF FF FF
当解压完毕的时候,我们可以发现代码段中还是存在一些问题,比如说CALL和JMP指令的目的地址是完全错误的,估计是因为压缩算法的关系,那么下面这段代码就是用来将其修正的,参数只有一个,也就是edi,作为指向CodeBase的指针
mov edi,esi // edi = esi = 00401000(CodeBase)
mov ecx,02681h // 外壳事先统计好的E8,E9的数量
L002:
mov al,byte ptr ds:[edi] // 从00401000开始搜索E8,E9指令
inc edi
sub al,00E8h
L005:
cmp al,01h
ja L002
cmp byte ptr ds:[edi],014h
jnz L002 // 从00401000开始搜索E8,E9指令
mov eax,dword ptr ds:[edi] // eax = Jump Code
mov bl,byte ptr ds:[edi+04h]
shr ax,08h // ax = ax / 0x100
rol eax,010h // 将eax的高16位移到低16位上
xchg ah,al // 然后将低16位的低8位和高8位互换
sub eax,edi // eax = eax - edi(jump指令的下一条指令的地址)
sub bl,00E8h
add eax,esi // eax = eax + CodeBase(401000)
mov dword ptr ds:[edi],eax // 将eax作为jump Code填写回去
add edi,05h
mov al,bl
loopd L005[1]
接下来我们再来看看IAT表是怎么被填写上去的
/*462B6E*/ lea edi,dword ptr ds:[esi+XXXXXXXX]
获取加密后的函数表的位置,根据该表来手动填写IAT
该表的数据结构如下:
[Dll Name Key] -> [IAT RVA,ImageBase = 00401000] -> [Dll End Mark] -> [Function Name] -> [0x00]
DWORD -> DWORD -> byte -> char[?] -> byte
/*462B7D*/ lea eax,dword ptr ds:[eax+esi+XXXXXXXX]
DllName = Key + 00401000 + XXXXXXXX(该值是加壳时生成的)
现在了解了这些结构之后要编写一个静态脱壳机也不难了,首先数据的解压可以通过上面的第一段汇编代码来进行,然后修正E8,E9可以通过上面的第二段汇编代码来进行,唯一需要关注的是IAT,因为导入表被换了一种形式存储,需要根据其还原出正常的导入表;
2.UPXFIX的工作原理
那么接下来来关注一下UpxFix是怎么进行工作的;我们知道,UPX这款壳因为其简单,易用,诞生了不少变种,稍作改动之后就不能用UPX自带的-d命令来进行脱壳,而UPXFIX就是对其变种进行修复的,那么这让我们感兴趣的是,他是怎么对这些变种进行修复的?或者更通俗一些,这些变种是修改了哪里导致UPX无法用-d命令进行脱壳?UPX的-d命令是依靠什么来进行脱壳的?
这三个问题足够引起我们对upxfix的关注,那么就来分析一下UPXFIX的工作原理,看看它是如何对UPX变种壳进行修复的;
打开upxfix.exe后,随便选中一个UPX加壳的文件(即时是没有经过变种的,它也会对其进行修复一番,当然,这种修复是毫无意义的);不管修复是否成功,其都会弹出一句” Fix UPX file success”,表示修复成功,其实这是因为它做的只是判断目标文件是否是UPX壳,然后根据自己掌握的知识对其进行修复,并不去判断是否修复成功;
具体的分析在” upxfix.idb”中,关键函数是sub_401730,这很容易得到,只要断MessageBoxA函数就可以了;
那么来说一下它修复的关键:
首先它将第一个区段和第二个区段的名称修复成”UPX0”和”UPX1”,然后定位到OEP offset + 5的位置,读出4个字节,如果需修复文件是EXE,判断这4字节是否是0x00BE8D00,如果需修复文件是DLL,就判断这4字节是否是0x08247C80,由此来更近一步判断是否是UPX壳,然后从文件中再次读取数据:
起始地址:&pSectionHeader[0](第一个区段首地址) – 0x20
数据长度:0x20个字节
这组数据是相当关键的,其中存放了UPX-d命令脱壳时候判断其是否是UPX壳,以及脱壳时候所必需的数据,该数据结构大致如下(有部分是猜测的,并不一定准确):
Typedef struct _UPXDATA
{
0x00 DWORD UPXMark; // 必须为"UPX!",也就是0x21585055
0x04 WORD UPXMark1; // 必须为0x090C
0x06 BYTE UPXMark2; // 用意不明
0x07 BYTE UPXMark3; // 必需为0x07
0x08 DWORD Check1; // 具体含义不明,如果改动该值会导致UPX-d命令失败,猜测可能是校验值
0x0C DWORD Check2; //具体含义不明,如果改动该值会导致UPX-d命令失败,猜测可能是校验值(这2个DWORD的值并没有在UPXFIX中进行处理,所以并不知道其所代表的含义,在upx-d命令分析时应该会得到结果)
0x10 DWORD UPXSectionSize; // UPX0和UPX1这两个区段的VirtualSize之和
0x14 DWORD DWORD1; // 含义不明,计算方法:外壳OEP Offset - OEP上方0字节的长度 - UPX0的文件起始地址
0x18 DWORD UPXSectionSizebrk; // UPX0和UPX1这两个区段的VirtualSize之和,不明白为什么要保存2份
0x1C WORD WORD1; // 具体含义暂时不明
0x1E BYTE BYTE1; // 该值必须为0
0x1F BYTE Number; // 从UPXMark1的起始地址开始,一直到BYTE2结束(0x1B字节),这段数据的累加值
}
以上即为对UPXFIX的分析,更具体的过程可以用IDA打开” upxfix.idb”查看sub_401730函数中代码的注释
3.UPX-d命令的分析 --- Crack_Qs补
60 pushad //保存现场
BE 00F04300 mov esi, 0043F000 //把代码段放到esi寄存器
8DBE 0020FCFF lea edi, dword ptr [esi+FFFC2000] //得到基址
C787 9CC00400 7>mov dword ptr [edi+4C09C], 46CD167B //将第一个函数的地址放到[edi+ 4C09C]
57 push edi //将基址压栈
83CD FF or ebp, FFFFFFFF
EB 0E jmp short 004629FA
90 nop
90 nop
90 nop
90 nop
8A06 mov al, byte ptr [esi] //取出0043F004的一个字节
46 inc esi //指向下一个字节
8807 mov byte ptr [edi], al //从00401000开始,开始还原代码
47 inc edi //指向下一个地址
01DB add ebx, ebx //ebx + ebx,当ebx不等于零的时候跳转,下面的adc如果为,就取出下一个地址,并放到ebx中
75 07 jnz short 00462A01
8B1E mov ebx, dword ptr [esi] //将0043F000放到ebx中
83EE FC sub esi, -4 //0043F000加4
11DB adc ebx, ebx //进位加法器
72 ED jb short 004629F0 // 向上跳转,ebx做为是否回跳的标志,循环处理代码
B8 01000000 mov eax, 1 // eax = 1
01DB add ebx, ebx // ebx依然作为循环的标志
75 07 jnz short 00462A13
8B1E mov ebx, dword ptr [esi] //esi指向的地址放到ebx里面
83EE FC sub esi, -4 //esi + 4
11DB adc ebx, ebx //进位加法
11C0 adc eax, eax //进位加法
01DB add ebx, ebx //ebx + ebx
73 0B jnb short 00462A24
75 28 jnz short 00462A43 //跳到下面
8B1E mov ebx, dword ptr [esi]
83EE FC sub esi, -4
11DB adc ebx, ebx
72 1F jb short 00462A43
48 dec eax
01DB add ebx, ebx
75 07 jnz short 00462A30
8B1E mov ebx, dword ptr [esi]
83EE FC sub esi, -4
11DB adc ebx, ebx
11C0 adc eax, eax
EB D4 jmp short 00462A08
01DB add ebx, ebx
75 07 jnz short 00462A3F
8B1E mov ebx, dword ptr [esi]
83EE FC sub esi, -4
11DB adc ebx, ebx
11C9 adc ecx, ecx
EB 52 jmp short 00462A95
31C9 xor ecx, ecx // 清零ecx
83E8 03 sub eax, 3 // eax - 3
72 11 jb short 00462A5B
C1E0 08 shl eax, 8
8A06 mov al, byte ptr [esi]
46 inc esi
83F0 FF xor eax, FFFFFFFF
74 75 je short 00462ACA
D1F8 sar eax, 1
89C5 mov ebp, eax
EB 0B jmp short 00462A66
01DB add ebx, ebx
75 07 jnz short 00462A66
8B1E mov ebx, dword ptr [esi]
83EE FC sub esi, -4
11DB adc ebx, ebx
72 CC jb short 00462A34
41 inc ecx
01DB add ebx, ebx
75 07 jnz short 00462A74
8B1E mov ebx, dword ptr [esi]
83EE FC sub esi, -4
11DB adc ebx, ebx
72 BE jb short 00462A34
01DB add ebx, ebx
75 07 jnz short 00462A81
8B1E mov ebx, dword ptr [esi]
83EE FC sub esi, -4
11DB adc ebx, ebx
11C9 adc ecx, ecx
01DB add ebx, ebx
73 EF jnb short 00462A76
75 09 jnz short 00462A92
8B1E mov ebx, dword ptr [esi]
83EE FC sub esi, -4
11DB adc ebx, ebx
73 E4 jnb short 00462A76
83C1 02 add ecx, 2
81FD 00FBFFFF cmp ebp, -500 //迷惑指令
83D1 02 adc ecx, 2 //进位加法
8D142F lea edx, dword ptr [edi+ebp] //edi + ebp的地址装载到edx,即原来的代码段的地址
83FD FC cmp ebp, -4 //判断跳转标志,EBP小于等于-4就跳
76 0E jbe short 00462AB4
8A02 mov al, byte ptr [edx] //取出代码段的一字节
42 inc edx //指向下一个地址
8807 mov byte ptr [edi], al //取出的代码放到edi里面
47 inc edi //指向下一个代码
49 dec ecx //计数器
75 F7 jnz short 00462AA6 //关于计数器(ecx)的跳转
E9 42FFFFFF jmp 004629F6 //向上面跳,跳到add ebx,ebx
8B02 mov eax, dword ptr [edx] //处理输入表
83C2 04 add edx, 4 //edx + 4,指向下一个地址
8907 mov dword ptr [edi], eax //将代码放到edi
83C7 04 add edi, 4 // edi + 4, 存放代码的地址
83E9 04 sub ecx, 4 //ecx - 4
77 F1 ja short 00462AB4
01CF add edi, ecx //edi + ecx,指向接收代码的地址的最后一个字节
E9 2CFFFFFF jmp 004629F6 //跳到 add ebx,ebx
5E pop esi
89F7 mov edi, esi
B9 81260000 mov ecx, 2681
8A07 mov al, byte ptr [edi] //指向我们原来代码段的代码,取出到AL里面
47 inc edi //指向下一个字节
2C E8 sub al, 0E8 //处理CALL
3C 01 cmp al, 1 //判断al是否大于1
77 F7 ja short 00462AD2 //循环,到下一个CALL的第一个字节为止
803F 14 cmp byte ptr [edi], 14
75 F2 jnz short 00462AD2
8B07 mov eax, dword ptr [edi] //取出里面的地址,里面的地址是定位CALL的绝对地址要用到的
8A5F 04 mov bl, byte ptr [edi+4] //得到下条地址的开始字节放到AL里面,CALL绝对地址就是下条指令开始+刚才上面取出的那个数字
66:C1E8 08 shr ax, 8 //ax右移8位
C1C0 10 rol eax, 10 //eax算术左移 8位
86C4 xchg ah, al //交换内容
29F8 sub eax, edi //eax - edi
80EB E8 sub bl, 0E8 //再减去E8
01F0 add eax, esi //eax + esi,其中 esi是代码段开始的地方
8907 mov dword ptr [edi], eax //这里处理CALL的地址,算出CALL的偏移到EDI里面
83C7 05 add edi, 5 //edi + 5,指向call的后面
88D8 mov al, bl //bl的内容放到al中
E2 D9 loopd short 00462AD7 //循环处理CALL,其中ecx作为计数器
8DBE 00F00500 lea edi, dword ptr [esi+5F000] //代码段的起始地址 + 5F000
8B07 mov eax, dword ptr [edi] //现在EDI指向我们的代码的输入表
09C0 or eax, eax //eax 或 eax ,判断eax是否为零
74 3C je short 00462B46
8B5F 04 mov ebx, dword ptr [edi+4] //取得这个地址的数据放到ebx
8D8430 AC2D0600 lea eax, dword ptr [eax+esi+62DAC] // 取得外壳段的KERNEL32.DLL的地址放eax
01F3 add ebx, esi //我们代码段的起始地址加上刚才取出的那个数据
50 push eax //kernel32.dll的地址
83C7 08 add edi, 8 //edi + 8
FF96 4C2E0600 call dword ptr [esi+62E4C] //装载kernel32.dll
95 xchg eax, ebp //交换数据,即eax指向kernel32.dll的地址
8A07 mov al, byte ptr [edi] //取得现在的EDI的地址指向的数据放到AL
47 inc edi //指向下一个函
08C0 or al, al //al 或 al,判断al是否为零
74 DC je short 00462B04
89F9 mov ecx, edi //取出的函数的名字放到ecx里面
57 push edi //函数名字压栈
48 dec eax //eax - 1
F2:AE repne scas byte ptr es:[edi]
55 push ebp //kernel32.dll的基址
FF96 502E0600 call dword ptr [esi+62E50] //外壳的GetProcaddress
09C0 or eax, eax //eax或eax,得到函数的地址
74 07 je short 00462B40
8903 mov dword ptr [ebx], eax //处理输入表
83C3 04 add ebx, 4 //ebx + 4,指向下一个输入表的地址
EB E1 jmp short 00462B21
FF96 602E0600 call dword ptr [esi+62E60]
8BAE 542E0600 mov ebp, dword ptr [esi+62E54] //VirtualProtect的地址放到ebp
8DBE 00F0FFFF lea edi, dword ptr [esi-1000] //指向PE头,即映像基址
BB 00100000 mov ebx, 1000 //把1000放到ebx,即ebx = 1000
50 push eax
54 push esp
6A 04 push 4
53 push ebx
57 push edi
FFD5 call ebp //改变属性
8D87 1F020000 lea eax, dword ptr [edi+21F] //现在eax指向PE头中区段的偏移起始位置
8020 7F and byte ptr [eax], 7F //改写区段名字
8060 28 7F and byte ptr [eax+28], 7F //改写区块属性第一个区块的属性
58 pop eax
50 push eax
54 push esp
50 push eax
53 push ebx
57 push edi
FFD5 call ebp
58 pop eax
61 popad //恢复现场
8D4424 80 lea eax, dword ptr [esp-80]
6A 00 push 0
39C4 cmp esp, eax
75 FA jnz short 00462B7A
83EC 80 sub esp, -80
E9 109FFEFF jmp 0044CA98 //跨区段的转移,跳到OEP
A0 2B4600B0 mov al, byte ptr [B000462B]
2B46 00 sub eax, dword ptr [esi]
9C pushfd
idb下载:
链接: http://pan.baidu.com/s/1c0ACDJ6 密码: 4hd2
201310-upx3.08分析-Spider[4sT TeAm] 修:Crack_Qs,布布扣,bubuko.com
201310-upx3.08分析-Spider[4sT TeAm] 修:Crack_Qs
原文地址:http://www.cnblogs.com/ReverseSec/p/3824798.html