【文章标题】: 突破封锁线:第一章--脱壳篇
【文章作者】: Austin
【作者邮箱】: austiny.cn@gmail.com
【软件名称】: 神州数码网络客户登陆程序
【软件大小】: 不重要
【下载地址】: http://www.dcnetworks.com.cn
【加壳方式】: UltraProtect 1.x
【保护方式】: 加壳 网络封禁
【编写语言】: VC6.0
【使用工具】: PEid OllyDbg LordPE ImportRec
【操作平台】: Windows2003
【软件介绍】: 神州数码小区宽带网络登陆客户端,诸多限制
【作者声明】: 技术交流中。。。
--------------------------------------------------------------------------------
【详细过程】
一、(背景)山和云的彼端
那时候天很蓝,网很宽,资源可以共享,BT随便可以下。后来有了客户端,一切化为泡影。
神州数码是一套小区宽带解决方案,使用802.x认证等技术,限制了网络的很多使用:
1. 帐号与机器绑定(换机器不能登陆)
2. 限制共享网络连接(发现代理,强制下线)
3. 限制BT下载(强制下线)
4. 无法从路由后连接(拒绝登陆)
...
诸多不便,一一破之。
二、(调查)英雄莫问出处
神州数码(DigitalChina下简称DIGI)安装文件夹大概就是一个主EXE文件外加一堆DLL文件和配置文件。
DLL文件都是系统常用如MFC42.DLL和AMT.DLL等,故搞定EXE一个即可。
1. 先看看加壳否
用PEid(侦壳工具)打开DIGI.EXE,得到 "UltraProtect 1.x -> RISCO Software Inc."。(看来盔甲穿得挺厚)
2. 再看仔细些
用LordPE(DUMP和PE修改工具)打开,看到程序有5个段。
出特别指明,文中都使用十六进制
地址 长度
.text 00410000 10000
.rdata 00411000 4000
.data 00415000 204000
.rsrc 00619000 3000
.perplex 0061C000 1D000
前4个都很典型(正常编译的程序一般都有这几个段),故不出意外,壳代码都在perplex段中。
3. 战略
UltraProtect壳没有脱壳机,只有自己动手脱光它了:
调试 -> 找OEP -> dump -> 修复IAT -> 修复资源 -> 减肥
三、(行动)骡子还是马,拉出来遛遛先
用OllyDbg(调试工具)装载,运行。啪!OllyDbg退出了。(烈马!)看来壳中有反调试检查。
难不倒我们:打开HideOD插件的隐藏选项,再运行。再啪!OllyDbg再次退出了。
(我看这马挺烈吧)(那是相当烈啊)
去看雪搜了一下,发现UltraProtect有检查加载者并于进程列表比对的习惯,而非常规的检查调试标志。
故HideOD骗不倒它。
再来,加载之,停在0061c000处:(正是在.perplex段中)
0061C000 > 60 pushad
0061C001 FC cld
0061C002 4D dec ebp
0061C003 72 01 jb short 0061C006
0061C005 4E dec esi
0061C006 50 push eax
0061C007 E8 01000000 call 0061C00D
0061C00C 71 58 jno short 0061C066
0061C00E 58 pop eax
0061C00F 0F84 03000000 je 0061C018
0061C015 66:13EF adc bp, di
0061C018 76 03 jbe short 0061C01D
0061C01A 77 01 ja short 0061C01D
0061C01C - 7E C1 jle short 0061BFDF
0061C01E DD22 frstor [edx]
0061C020 7C 03 jl short 0061C025
0061C022 7D 01 jge short 0061C025
0061C024 9A BA21D624 CBB>call far BACB:24D621BA
将断点下至Process32First函数(获得进程列表会调用)
bp Process32First+2
F9运行,断在了7c85a26e处:(应该在Process32FirstA中)
7C85A26E > 55 push ebp
7C85A26F 8BEC mov ebp, esp
7C85A271 81EC 30020000 sub esp, 230
7C85A277 A1 08D1887C mov eax, [7C88D108]
7C85A27C 56 push esi
7C85A27D 8B75 0C mov esi, [ebp+C]
7C85A280 85F6 test esi, esi
7C85A282 8945 FC mov [ebp-4], eax
7C85A285 8B45 08 mov eax, [ebp+8]
7C85A288 0F84 9D000000 je 7C85A32B
7C85A28E 813E 28010000 cmp dword ptr [esi], 128
7C85A294 0F82 91000000 jb 7C85A32B
7C85A29A 53 push ebx
7C85A29B 8D8D D0FDFFFF lea ecx, [ebp-230]
7C85A2A1 51 push ecx
7C85A2A2 50 push eax
7C85A2A3 C785 D0FDFFFF 2>mov dword ptr [ebp-230], 22C
7C85A2AD E8 FDFEFFFF call Process32FirstW
Shift+F9回到用户代码:
00620B3F > /E9 AD000000 jmp 00620BF1 ;直接跳转到下面
00620B44 |60 pushad
00620B45 |8DBD E1444000 lea edi, [ebp+4044E1]
00620B4B |B9 26000000 mov ecx, 26
00620B50 |E8 A9EDFFFF call 0061F8FE
00620B55 |AB stos dword ptr es:[edi]
00620B56 ^|E2 F8 loopd short 00620B50
00620B58 |61 popad
00620B59 |60 pushad
00620B5A |E8 F2EFFFFF call 0061FB51
.......
00620BF1 \0BC0 or eax, eax ;跳转到此,单步往下走
00620BF3 0F84 27020000 je 00620E20 ;此处未跳转
00620BF9 8B95 45424000 mov edx, [ebp+404245] ;[ebp+404245]的值从0开始,依次为各进程PID
00620BFF 3B95 35424000 cmp edx, [ebp+404235] ;[ebp+404235]中为父进程OllyDbg的PID,此句进行比对
00620C05 0F84 DC000000 je 00620CE7 ;在比对前将[ebp+404235]的值改为Explorer.exe的PID
00620C0B B8 3D424000 mov eax, 0040423D ;就可以骗过DIGI.EXE
00620C10 03C5 add eax, ebp
00620C12 50 push eax
00620C13 FFB5 39424000 push dword ptr [ebp+404239]
00620C19 50 push eax
00620C1A 8B85 05454000 mov eax, [ebp+404505]
00620C20 8038 CC cmp byte ptr [eax], 0CC
00620C23 74 10 je short 00620C35
不多说,改之。顺利通过检测。:)
清除刚才的断点。(重要,除非你愿意一次又一次重打第一关BOSS)
四、(继续)万里长征第二步
通过了第一关,开始进行寻找OEP的伟大战役。
(注:OEP=Original Entry Point,就是加壳前程序开始的地方,也是我们dump的起点)
常识告诉我们,OEP应该在.text段。(当然也有不是的时候,不过大多数的时候还是是的)
Alt+M打开内存图,F2设一个访问断点在.text段上。(没有用二次断点,因为幸运的是,此时代码段已经解压完毕)
F9往下跑,程序停在0040fb74处,在.text段。(简直就是VC程序的标准入口,这里应该就是EOP没错了)
0040FB74 > . 68 A82A4100 push 00412AA8
0040FB79 . 68 D4FC4000 push 0040FCD4 ; jmp 到 msvcrt._except_handler3; SE 处理程序安装
0040FB7E . 64:A1 0000000>mov eax, fs:[0]
0040FB84 . 50 push eax
0040FB85 . 64:8925 00000>mov fs:[0], esp
0040FB8C . 83EC 68 sub esp, 68
0040FB8F . 53 push ebx
0040FB90 . 56 push esi
0040FB91 . 57 push edi
0040FB92 . 8965 E8 mov [ebp-18], esp
0040FB95 . 33DB xor ebx, ebx
0040FB97 . 895D FC mov [ebp-4], ebx
0040FB9A . 6A 02 push 2
0040FB9C . FF15 64144100 call [411464] ; msvcrt.__set_app_type
0040FBA2 . 59 pop ecx
0040FBA3 . 830D 5C8E6100>or dword ptr [618E5C], FFFFFFFF
0040FBAA . 830D 608E6100>or dword ptr [618E60], FFFFFFFF
0040FBB1 . FF15 68144100 call [411468] ; msvcrt.__p__fmode
0040FBB7 . 8B0D A06F4100 mov ecx, [416FA0]
0040FBBD . 8908 mov [eax], ecx
0040FBBF . FF15 6C144100 call [41146C] ; msvcrt.__p__commode
0040FBC5 . 8B0D 9C6F4100 mov ecx, [416F9C]
至此,EOP已经找到。
由于可能多次重复,用OllyMachine插件完成整个上述过程:
;LOAD.TXT
;//////////////////////////////////////////////////////
invoke bp, 0x7c85a26e;Process32First+2
run
runtousercode
invoke bp, 0x7c85a26e;Process32First+2
run
invoke bc, 0x7c85a26e
runtousercode
invoke writememlong, 0x61f235, 1960, 2
invoke bp, 0x0040fb74
run
;//////////////////////////////////////////////////////
五、(出击)衣带渐宽终不悔
局势很明朗,dump之。这里用了OllyDbg的插件OllyDump。
注意dump的时候不要选择Rebuild Import(术业有专攻,这个留给ImportRec来处理),保存为DIGI_DUMP.EXE。
打开ImportRec(修复IAT之王),在它的进程表中找到正在调试的DIGI。
在OEP中填入刚刚找到的0000FB74(偏移量),点击"IAT AutoSearch",再点击"Get Imports",得到Imported Functions列表。
表中有两块为解决的项:
? FThunk:00011048 NbFunc:2D (decimal:45) valid:NO 这一项中有45个函数不可用
? FThunk:00011504 NbFunc:16 (decimal:22) valid:NO 这一项中有22个函数不可用
注意表中00011048为偏移地址,所以实际地址应加上基地址00400000,为00411048。
在OllyDbg的数据窗口中来到00411048(按地址方式显示):
00411048 0061C000 offset DigitalC.<模块入口点>
0041104C 0061C00D DigitalC.0061C00D
00411050 0061C01A DigitalC.0061C01A
00411054 0061C027 DigitalC.0061C027
00411058 0061C034 DigitalC.0061C034
0041105C 0061C041 DigitalC.0061C041
00411060 0061C04E DigitalC.0061C04E
表中00411048指向0061c000(注意这正是.perplex段开始的地方),在反汇编窗口中跟踪:
0061C000 > 68 D6DDEAA8 push A8EADDD6
0061C005 813424 A6AD68D4 xor dword ptr [esp], D468ADA6
0061C00C C3 retn
0061C00D 68 2666EDA8 push A8ED6626
0061C012 813424 4AC468D4 xor dword ptr [esp], D468C44A
0061C019 C3 retn
0061C01A 68 9D6FEDA8 push A8ED6F9D
0061C01F 813424 7ECC68D4 xor dword ptr [esp], D468CC7E
0061C026 C3 retn
0061C027 68 75D6EAA8 push A8EAD675
0061C02C 813424 42E668D4 xor dword ptr [esp], D468E642
0061C033 C3 retn
注意每隔三句为表中指向的一项。
如第一项所做的操作相当于 jmp xxxxxxxx (xxxxxxxx = A8EADDD6 xor D468ADA6),下面的项对应相同。
故将算出的xxxxxxxx写入00411048即为真实的Imported Function地址。
过程繁琐,用OllyMachine插件脚本来完成。(两个invalid的表一起解决掉)(此脚本来源于看雪论坛)
;修复IAT.TXT
;//////////////////////////////////////////////////////
mov reg01,0x411048
lp:
invoke ReadMemLong,reg01,4; ---先取出要跳转的指针
;invoke PrintNum,reg00,16; ----这些是我最初拿来验证数据是否正确的
mov reg02,reg00; --------------存一下,下面还要用,寄存器多就是好
inc reg00; --------------------上面的代码中可以看出来,xor的前一个数在被指向地址+1处
invoke ReadMemLong,reg00,4; ---读取
;invoke PrintNum,reg00,16
mov reg10,reg00
add reg02,8; ------------------xor后一个数在+8处
invoke ReadMemLong,reg02,4; ---读取
xor reg00,reg10; --------------xor,我们要的api地址
;invoke PrintNum,reg00,16
invoke WriteMemLong,reg01,reg00,4; ---写入到IAT中
add reg01,4
cmp reg01,0x4110f9; -----------比较修复是否完成
jb lp
mov reg01,0x411504
lp1:
invoke ReadMemLong,reg01,4
;invoke PrintNum,reg00,16
mov reg02,reg00
inc reg00
invoke ReadMemLong,reg00,4
;invoke PrintNum,reg00,16
mov reg10,reg00
add reg02,8
invoke ReadMemLong,reg02,4
xor reg00,reg10
;invoke PrintNum,reg00,16
invoke WriteMemLong,reg01,reg00,4
add reg01,4
cmp reg01,0x411559
jb lp1
;//////////////////////////////////////////////////////
运行后发现00411048处的函数表已经修复,可以正确识别:
00411048 7C827070 kernel32.SetThreadPriority
0041104C 7C85A26C kernel32.Process32First
00411050 7C85A3E3 kernel32.Process32Next
00411054 7C823037 kernel32.CreateThread
00411058 7C82BC7C kernel32.MultiByteToWideChar
0041105C 7C806165 kernel32.DeleteFileA
00411060 7C826919 kernel32.GlobalHandle
00411064 7C8270B2 kernel32.SetEvent
00411068 7C8266C5 kernel32.GlobalUnlock
再回到Import Rec中,"Get Imports",形势一片大好,全部valid。
将Add new section选中,点击"Fix Dump",选择之前保存的DIGI_DUMP.EXE:
得到修复了Import Table的DIGI_DUMP_.EXE文件。
六、(:)脱掉脱掉脱掉
脱到这里,脑海里已经响起杜德伟的歌声。
得意的运行DIGI_DUMP_.EXE文件。(冷水!)却得到一个引用0x0000000内存的错误。
分析一下,应该是修改了入口地址(EP),壳中有些代码没有运行,所以程序未能正常运行。
关闭对内存异常的忽略(因为是内存错误),再用OllyDbg打开新得到的DIGI_DUMP.EXE文件。
从入口点0040fb74开始单步跟踪:
0040FC93 |> \50 push eax
0040FC94 |. 56 push esi
0040FC95 |. 53 push ebx
0040FC96 |. 53 push ebx ; /pModule
0040FC97 |. FF15 CC104100 call [<&kernel32.GetModuleHandleA>] ; \GetModuleHandleA
0040FC9D |. 50 push eax
0040FC9E |. E8 13010000 call 0040FDB6 ;单步过此处出现异常
0040FCA3 |. 8945 98 mov [local.26], eax
0040FCA6 |. 50 push eax ; /status
0040FCA7 |. FF15 C0144100 call [<&msvcrt.exit>] ; \exit
0040FCAD |. 8B45 EC mov eax, [local.5]
0040FCB0 |. 8B08 mov ecx, [eax]
0040FCB2 |. 8B09 mov ecx, [ecx]
程序停在0061d137:
0061D131 FF25 C0951500 jmp [1595C0]
0061D137 > FF25 C4951500 jmp [1595C4] ;程序停在这里
0061D13D FF25 C8951500 jmp [1595C8]
0061D143 FF25 CC951500 jmp [1595CC]
0061D149 FF25 D0951500 jmp [1595D0]
0061D14F FF25 D4951500 jmp [1595D4]
0061D155 FF25 D8951500 jmp [1595D8]
0061D15B FF25 DC951500 jmp [1595DC]
0061D161 FF25 E0951500 jmp [1595E0]
0061D167 FF25 E4951500 jmp [1595E4]
这里(0061d137)已经是.perplex的领域,就是壳的代码段,看在程序运行后还是在调用壳中的代码。
看一下堆栈:
0012AEF8 0040FB6D 返回到 digi_dum.0040FB6D 来自 digi_dum.0061D137
0012AEFC 00000000
0012AF00 00000000
0012AF04 00000000
看来就是从0040fb6d之前调用了这里,Ctrl+G 跳过去:
0040FB64 |. 8501 test [ecx], eax
0040FB66 |. 8BE1 mov esp, ecx
0040FB68 |. E8 CAD52000 call 0061D137 ;就是这里调用了壳中代码
0040FB6D |. 50 push eax
0040FB6E \. C3 retn
0040FB6F 63 db 63 ; CHAR 'c'
0040FB70 72 db 72 ; CHAR 'r'
0040FB71 6A db 6A ; CHAR 'j'
0040FB72 D8 db D8
0040FB73 D4 db D4
0040FB74 >/$ 68 A82A4100 push 00412AA8
0040FB79 |. 68 D4FC4000 push <jmp.&msvcrt._except_handler3> ; SE 处理程序安装
0040FB7E |. 64:A1 0000000>mov eax, fs:[0]
果不其然,就是这里调用了壳中的代码。
分析一下:
这是一种典型的壳驻留方式,就是在解压完毕之后,任有部分壳中代码被运行,以防止壳失去控制权(或者说,被脱掉)。
这里用了一种叫做Code Splitting的技术(之后再具体分析)。
应该是壳正常调用时,会在001595c4写入正常的函数地址以被调用。
而现在壳代码没有运行,001595c4中就无没有正确的地址了。
问题的关键就是得到001595c4中正确的地址。记下0061d137这个地方。
将之前的内存异常忽略选项打开,用OllyDbg打开未脱壳的DIGI程序,在0061d137下断点。
用之前的OllyMachine脚本将程序跑起来,停在0040fb7e处,再跑,停到0061d137处:
0061D137 - FF25 C4951500 jmp [1595C4]
0061D13D - FF25 C8951500 jmp [1595C8]
在数据窗口(地址模式)看1595c4的内容:
001595BC 0015A56E
001595C0 0015A574
001595C4 0015A57A ;这就是将要jmp到的地方
在反汇编窗口跟过去:
0015A56E 8BCF mov ecx, edi
0015A570 8957 FC mov [edi-4], edx
0015A573 C3 retn
这三句就是壳程序在内存中写入的代码,
在 0040FB68 |. E8 CAD52000 call 0061D137 的调用最后就是调用了这三句代码。
注意到 0040fb68 中的call语句占用了5个字节,而三句代码中的前两句占用的也是5个字节(第三句用于返回)。
这就是Code Splitting的秘密!!!
将原程序中的部分语句用壳中的一个call来代替,这样脱出来的程序就离不开壳来运行。
解决方法就是将0015a56e开始的5个字节代码拷回至0040fb68就可以了。
这么繁琐的工作,让OllyMachine来做:
;修复代码1.TXT
;//////////////////////////////////////////////////////
mov reg01, 0x00401030
lp:
add reg01, 5
invoke Find, reg01, "e8????2000"
cmp reg00, -1
je end1
mov reg01, reg00 //reg01="call 0061????"
invoke printbuftodump, reg01
;invoke printbuf, reg01, 5
;invoke MsgYn, "Process?"
;or reg00, 0
;je lp
mov reg02, reg01
inc reg02 //reg02="0020????"
invoke readmemlong, reg02, 4
mov reg03, reg00 //reg03=0020????
;invoke printnum, reg01, 16
;invoke printnum, reg03, 16
add reg03, reg01
add reg03, 5 //reg03=61????(jmp[15????])
add reg03, 2 //reg03="15???"
invoke printbuftodump, reg03
invoke readmemlong, reg03, 4
mov reg04, reg00 //reg04=15????
invoke readmemlong, reg04, 4
mov reg05, reg00 //reg05=[15????](xor ...)
invoke printbuftodump, reg05
;invoke printbuf, reg05, 5
;invoke MsgYn, "Process?"
;or reg00, 0
;je lp
invoke readmemlong , reg05, 4
invoke writememlong, reg01, reg00, 4
add reg01, 4
add reg05, 4
invoke readmemlong, reg05, 1
invoke writememlong, reg01, reg00, 1
sub reg01, 4
invoke printbuftodump, reg01
;invoke MsgYn, "Continue?"
;or reg00, 0
jne lp
end1:
;//////////////////////////////////////////////////////
;修复代码2.TXT
;//////////////////////////////////////////////////////
mov reg01, 0x00401030
lp:
add reg01, 5
invoke Find, reg01, "e8????2100"
cmp reg00, -1
je end1
mov reg01, reg00 //reg01="call 0061????"
invoke printbuftodump, reg01
;invoke printbuf, reg01, 5
;invoke MsgYn, "Process?"
;or reg00, 0
;je lp
mov reg02, reg01
inc reg02 //reg02="0020????"
invoke readmemlong, reg02, 4
mov reg03, reg00 //reg03=0020????
;invoke printnum, reg01, 16
;invoke printnum, reg03, 16
add reg03, reg01
add reg03, 5 //reg03=61????(jmp[15????])
add reg03, 2 //reg03="15???"
invoke printbuftodump, reg03
invoke readmemlong, reg03, 4
mov reg04, reg00 //reg04=15????
invoke readmemlong, reg04, 4
mov reg05, reg00 //reg05=[15????](xor ...)
invoke printbuftodump, reg05
;invoke printbuf, reg05, 5
;invoke MsgYn, "Process?"
;or reg00, 0
;je lp
invoke readmemlong , reg05, 4
invoke writememlong, reg01, reg00, 4
add reg01, 4
add reg05, 4
invoke readmemlong, reg05, 1
invoke writememlong, reg01, reg00, 1
sub reg01, 4
invoke printbuftodump, reg01
;invoke MsgYn, "Continue?"
;or reg00, 0
jne lp
end1:
;//////////////////////////////////////////////////////
运行上述两个脚本,以及之前的修复IAT脚本。 Dump出来,用ImportRes修复。(啤酒拿出来,准备庆祝)
运行一下新的DIGI_DUMP_.EXE程序 ~~ 程序界面飘然而至 :)
七、(狂欢)I am a super dancing queen!
为了确定已经脱光了,用OllyDbg打开之,清除所有断点,对.perplex段下访问断点(在Alt+M中),运行。
试验各项功能,并没有被断下来,说明.perplex中的壳代码再也没有运行。
脱壳成功!!!
--------------------------------------------------------------------------------
【经验总结】
至此,神州数码的UltraProtect壳已经被彻底的脱下来了。为后续的工作做好了准备。
脱壳中注意的关键点有:
1. 壳会对进程列表进行比对,关键点是Process32First函数 ......... LOAD.TXT脚本
2. 壳中有奇怪形式的IAT表,注意修复 .......................... 修复IAT.TXT脚本
3. 壳使用了Code Splitting技术,注意还原 ..................... 修复代码1.TXT 修复代码2.TXT 脚本
4. 脚本真是个好东西
突破封锁线:第一章--脱壳篇 [完]
谢谢您耐心的看到这里,在下一章--资源篇中,将详细讲述如何修复资源和加入自己的资源到程序中。
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!
2006年04月28日 14:49:03