本帖最后由 ximo 于 2009-9-10 20:54 编辑
好久不脱壳了,都快生疏了,于是拣个软柿子捏捏,顺便复习下PE格式。
壳很简单,FSG,大部分人接触脱壳破解几个星期估计都会脱。
做个详细的分析,并纯手动修复输入表。高手就飘过吧。。。
哎,高手都玩TMD,NP了,我这种层次只能捏压缩壳玩,实在惭愧。
一、压入壳的参数01000154 > 8725 58050201 xchg dword ptr ds:[1020558],esp
0100015A 61 popad
0100015B 94 xchg eax,esp
开头的3句式把[1020558]的内容压入了堆栈,等效于:01000154 > /E9 A70B0000 jmp np_fsg2.01000D00
01000159 |90 nop
01000D00 B8 5C050201 mov eax,np_fsg2.0102055C
01000D05 B9 007D0000 mov ecx,7D00
01000D0A BA 80000000 mov edx,80
01000D0F BB 5C050201 mov ebx,np_fsg2.0102055C
01000D14 BC C4FF0600 mov esp,6FFC4
01000D19 BD 04760001 mov ebp,np_fsg2.01007604
01000D1E BE ECBD0101 mov esi,np_fsg2.0101BDEC
01000D23 BF 00100001 mov edi,np_fsg2.01001000
01000D28 ^ E9 2FF4FFFF jmp np_fsg2.0100015C
01000D2D 90 nop
B8 5C 05 02 01 B9 00 7D 00 00 BA 80 00 00 00 BB 5C 05 02 01 BC C4 FF 06 00 BD 04 76 00 01 BE EC
BD 01 01 BF 00 10 00 01 E9 2F F4 FF FF 90
不过FSG用了xchg语句,更为巧妙和简捷。
观察下[1020558]里的内容:0102053C 01001000 np_fsg2.01001000-----------EDI------------------------代码段的起始RVA
01020540 0101BDEC np_fsg2.0101BDEC-----------ESI------------------------保存加密数据的起始地址
01020544 01007604 np_fsg2.01007604-----------EBP------------------------输入表RVA
01020548 00000000-----------------------------ESP------------------------0102053C+4*8=0102055C
0102054C 0102055C np_fsg2.0102055C-----------EBX------------------------FSG自己的结构
01020550 00000080-----------------------------EDX------------------------dl=80h
01020554 00007D00-----------------------------ECX------------------------加密数据的长度(大小)
01020558 0006FFC4-----------------------------EAX------------------------保存原始的ESP
FSG自身结构:0102055C 010001E8 np_fsg2.010001E8--------------------------------------解密函数(aplib)
01020560 010001DC np_fsg2.010001DC--------------------------------------解密函数(aplib)
01020564 010001DE np_fsg2.010001DE--------------------------------------解密函数(aplib)
01020568 0100739D np_fsg2.0100739D--------------------------------------未加壳程序的入口地址(OEP)
0102056C > 7C801D77 kernel32.LoadLibraryA
01020570 > 7C80ADA0 kernel32.GetProcAddress
其中所代表的各个值有后面的调试可以知道。
二、解压缩0100015C 55 push ebp
0100015D A4 movs byte ptr es:[edi],byte ptr ds:[esi]
0100015E B6 80 mov dh,80
01000160 FF13 call dword ptr ds:[ebx]
01000162 ^ 73 F9 jnb short np_fsg2.0100015D
01000164 33C9 xor ecx,ecx
01000166 FF13 call dword ptr ds:[ebx]
01000168 73 16 jnb short np_fsg2.01000180
0100016A 33C0 xor eax,eax
0100016C FF13 call dword ptr ds:[ebx]
0100016E 73 1F jnb short np_fsg2.0100018F
01000170 B6 80 mov dh,80
01000172 41 inc ecx
01000173 B0 10 mov al,10
01000175 FF13 call dword ptr ds:[ebx]
01000177 12C0 adc al,al
01000179 ^ 73 FA jnb short np_fsg2.01000175
0100017B 75 3A jnz short np_fsg2.010001B7
0100017D AA stos byte ptr es:[edi]
0100017E ^ EB E0 jmp short np_fsg2.01000160
01000180 FF53 08 call dword ptr ds:[ebx+8]
01000183 02F6 add dh,dh
01000185 83D9 01 sbb ecx,1
01000188 75 0E jnz short np_fsg2.01000198
0100018A FF53 04 call dword ptr ds:[ebx+4]
0100018D EB 24 jmp short np_fsg2.010001B3
0100018F AC lods byte ptr ds:[esi]
01000190 D1E8 shr eax,1
01000192 74 2D je short np_fsg2.010001C1
01000194 13C9 adc ecx,ecx
01000196 EB 18 jmp short np_fsg2.010001B0
01000198 91 xchg eax,ecx
01000199 48 dec eax
0100019A C1E0 08 shl eax,8
0100019D AC lods byte ptr ds:[esi]
0100019E FF53 04 call dword ptr ds:[ebx+4]
010001A1 3B43 F8 cmp eax,dword ptr ds:[ebx-8]
010001A4 73 0A jnb short np_fsg2.010001B0
010001A6 80FC 05 cmp ah,5
010001A9 73 06 jnb short np_fsg2.010001B1
010001AB 83F8 7F cmp eax,7F
010001AE 77 02 ja short np_fsg2.010001B2
010001B0 41 inc ecx
010001B1 41 inc ecx
010001B2 95 xchg eax,ebp
010001B3 8BC5 mov eax,ebp
010001B5 B6 00 mov dh,0
010001B7 56 push esi
010001B8 8BF7 mov esi,edi
010001BA 2BF0 sub esi,eax
010001BC F3:A4 rep movs byte ptr es:[edi],byte ptr ds:[esi]
010001BE 5E pop esi
010001BF ^ EB 9F jmp short np_fsg2.01000160
很明显这是个解压缩函数,所使用的压缩引擎为apLib。
这段是原来的.code段,也就是01001000处,长度为7D00的数据给解压缩了。
而压缩后的数据保存的地址为0101BDEC中。
0100015D A4 movs byte ptr es:[edi],byte ptr ds:[esi]
ds:[esi]=[0101BDEC]=A3
es:[edi]=[01001000]=00
由此可知:
0102053C 01001000 np_fsg2.01001000-----------EDI------------------------代码段的起始RVA
01020540 0101BDEC np_fsg2.0101BDEC-----------ESI------------------------保存加密数据的起始地址
这2句结构的意思。
aplib压缩引擎是开源的,就不具体深究了。有兴趣的可以去参考它的源码。
三、输入表的处理:010001C1 5E pop esi ;解压数据完毕,并把输入表的RVA传给esi
010001C2 AD lods dword ptr ds:[esi] ;取出IID.FirstThunk
010001C3 97 xchg eax,edi
010001C4 AD lods dword ptr ds:[esi] ;取出dll的name放eax
010001C5 50 push eax ;把DLL的name压栈
010001C6 FF53 10 call dword ptr ds:[ebx+10] ;eax=hDll=LoadLibraryA("comdlg32.dll"),以第一个DLL为例
010001C9 95 xchg eax,ebp
010001CA 8B07 mov eax,dword ptr ds:[edi]
010001CC 40 inc eax ;IAT存放在了[edi]-1的地方,手动修复的时候要用
010001CD ^ 78 F3 js short np_fsg2.010001C2 ;一个dll内的输入表处理完就跳,去进行下一个DLL处理
010001CF 75 03 jnz short np_fsg2.010001D4 ;处理完所有的,则不跳
010001D1 FF63 0C jmp dword ptr ds:[ebx+C] ;跳向OEP
010001D4 50 push eax ;压入hDll
010001D5 55 push ebp ;压入要处理的API名称
010001D6 FF53 14 call dword ptr ds:[ebx+14] ;eax=GetProcAddress(hDll,"PageSetupDlgW")以第一个函数为例
010001D9 AB stos dword ptr es:[edi] ;填充IAT
010001DA ^ EB EE jmp short np_fsg2.010001CA ;循环处理
填充IAT的过程很简单,详细见上面的注释。
处理完所有的IAT,就用010001D1 FF63 0C jmp dword ptr ds:[ebx+C] 跳向了OEP。
由于ebx中保存的是FSG自身的结构,查上面的结构可知,[ebx+C]指向的值即为OEP。
至此,整个流程就基本分析清楚了。
由上面的分析,可以得到2种快捷的脱壳方法:
法1.查找跳向OEP的特征代码jmp dword ptr ds:[ebx+C],也就是一般脚本里的FF 63 0C,在这里下好断点,中断后F7,就到OEP了。此时dump,IR修复OK。
法2.由于
01000154 > 8725 58050201 xchg dword ptr ds:[1020558],esp
0100015A 61 popad
执行完这2条语句后,堆栈中的内容为FSG自己的结构,而其自身结构的第四个值即为原始的OEP值,因此直接在OEP处下硬件断点,断下后即可脱壳了
故一般所谓的快速方法为:
1.载如OD,F8两次,然后再堆栈处,从上往下数4行,或者找LoadLibraryA前1行,在这个地址处下硬件断点,F9,中断后,即可。
四、纯手动修复IAT
首先在010001C1 5E pop esi 处dump程序,因为此时数据已经全部解压完毕,而IAT还没有填充。
下面就开始手动处理:
1.新建IID表:
找个空白的位置,用于新建IID表,我以9860为例。
由于用于修复的文件是从内存中dump出来的,因此不存在RVA和offset的区别,任何1个地址都是相同的。
先来看下IID的结构:IID的结构如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
由5个DWORD组成
可知,他有5个DWORD值组成,具体如下:
1.OriginalFirstThunk
2.TimeDateStamp
3.ForwarderChain
4.Name
5.FirstThunk
下面就来开始填充。以第一个DLL为例:
由FSG结构中输入表的RVA得,下面的结构:01007604 010012C4 np_fsg2.010012C4-------------------------------------IID->FirstThunk
01007608 01007AAC ASCII "comdlg32.dll"---------------------------------IID->Name
0100760C 01001174 np_fsg2.01001174
01007610 01007AFA ASCII "SHELL32.dll"
01007614 010012B4 np_fsg2.010012B4
01007618 01007B3A ASCII "WINSPOOL.DRV"
0100761C 01001020 np_fsg2.01001020
01007620 01007B5E ASCII "COMCTL32.dll"
01007624 010012EC np_fsg2.010012EC
01007628 01007C76 ASCII "msvcrt.dll"
0100762C 01001000 np_fsg2.01001000
01007630 01007D08 ASCII "ADVAPI32.dll"
01007634 0100108C np_fsg2.0100108C
01007638 010080EC ASCII "KERNEL32.dll"
0100763C 01001028 np_fsg2.01001028
01007640 0100825E ASCII "GDI32.dll"
01007644 01001188 np_fsg2.01001188
01007648 0100873C ASCII "USER32.dll"
上述结构中,各个分支都存放着2个值:
1.dll的名称
2.起始FirstThunk的值
填充如下:
1.OriginalFirstThunk的值可以填充为00000000,我这里填充的是FirstThunk的值
2.TimeDateStamp的值可以填充为00000000,我这里填充的是FFFFFFFF
3.ForwarderChain的值可以填充为00000000,我这里填充的是FFFFFFFF
4.关键数据,这里的值是指向dll的name的,从上面的表可知,第一个dll是comdlg32.dll,而其地址为01007AAC,减去基址得00007AAC,因此这里填AC7A 0000
5.关键数据,这里的值是FirstThunk,由上表得,地址为010012C4,减去基址整理得:C412 0000
这里你可以自己找个空白的地址,然后自己填充IAT表,然后把修改FirstThunk的值,这里我直接使用的原始的输入表信息
下面的几个按照上面的方法进行填充
完全填充后如下:
2.填充IAT
以第一个DLL:comdlg32.dll的第一个函数为例子:
先来看下OD中的数据
OD中的数据:010012C4 01007A7B np_fsg2.01007A7B
010012C8 01007A5F np_fsg2.01007A5F
010012CC 01007A9F np_fsg2.01007A9F
010012D0 01007A51 np_fsg2.01007A51
010012D4 01007A41 np_fsg2.01007A41
010012D8 01007A8B np_fsg2.01007A8B
010012DC 01007A6B np_fsg2.01007A6B
010012E0 01007A15 np_fsg2.01007A15
010012E4 01007A2D np_fsg2.01007A2D
分析下:
IID->FirstThunk=12C4,
看下12C4的值:
[010012C4]=01007A7B
翻到7A7B看下数据:
01007A7B 00 50 61 67 65 53 65 74 75 70 44 6C 67 57 00 00 .PageSetupDlgW..
不过7A7B并没有直接指向函数的头部,而是在前1个字节,这也是上面壳在填充输入表的时候,要inc eax的原因。
很明显是指向API函数的
不过由于windows在自己装载PE的时候,是允许按序号转载的,所以在每个函数前,都会预留2个字节,给存放序号用。
故我们的在填充FirstThunk的时候,需要把原始的数据再减去1(因为本来就已经向前1个字节了)
因此,修改后的数据为:01007A7B
再减去基址01000000,整理后得:7A7A 0000
当FirstThunk所指向的值为000000000的时候,改dll填充完毕。
因此我们要把遇到的FFFFFF7F都修改为00000000
后面的都是按照这个原则进行修改,就是把原来的值-01000001
完全修改后:
这样输入表就处理好了。
最后的工作就是修复输入表的RVA和 size,以及改写OEP的值了
OEP的值=739D
输入表的RVA=9860
size=(01001340-01001000)/4-8=C8,当然这个值可以改写的稍大点。
全处理完后,保存,OK了。
文件正常运行。脱壳到此结束。
整理一下:
1.运行到010001C1,dump文件
2.按上面的过程手动处理输入表
3.修改输入表RVA,size,以及OEP的值
4.保存,完工
基本上把这个压缩壳剖析完了,可能讲的又点啰嗦了。不过觉得该讲的也都讲述清楚了。
希望新手朋友看完有所收获吧。
作者:ximo[LCG] |