第一课——脱壳基础

1. 程序是什么语言编译的

从目前国内接触到程序看,比较流行的编译器有:VC系列、易语言、.NET、Delphi,一些曾经用的很多但渐渐少了有:VB、ASM、BC++,还有一些用的比较少的有:AutoIt、PB、QT等,下面提供一些实例,结合实例来看看“入口点代码”、“程序区段”和“加载模块”等特征。

1.1 VC6

1
2
3
4
00401700 >/$  55            push ebp
00401701 |. 8BEC mov ebp,esp
00401703 |. 6A FF push -0x1
00401705 |. 68 00254000 push 吾爱破解.00402500

VC6特点:入口点代码是固定的代码,入口调用的API也是相同的,其中有的push地址不同程序可能会有所不同;区段有四个也是固定的.text、.rdata、.data和.rsrc。

1.2 VS2008与VS2013

1
2
00B5DDAC > $  E8 EF4E0000   call 吾爱破解.00B62CA0
00B5DDB1 .^ E9 79FEFFFF jmp 吾爱破解.00B5DC2F
1
2
00B03359 > $  E8 A9520000   call 吾爱破解.00B08607
00B0335E .^ E9 7FFEFFFF jmp 吾爱破解.00B031E2
1
2
00B08607  /$  55            push ebp
00B08608 |. 8BEC mov ebp,esp

VS特点:入口点只有两行代码,一个CALL后直接JMP,第一个CALL进去后调用的API也是相同的;区段相对于VC6多了一个.reloc。

1.3 易语言

易语言编译无壳程序分为独立编译和非独立编译。由于易语言独立编译是调用VC的链接程序编译的,所以从区段和入口代码特征和VC相同,用exeinfoPE查壳也显示是VC6编译的。而非独立编译却显示不知名EXE,可能查壳工具里面没有存易语言非独立编译的特征码吧。

易语言特点:从程序里找一些call调用,最终都会走到上面位置(文字不太好表达),这个方法可以区分易语言和VC,非独立编译比较容易识别,入口处和E模块都可以找到krnln.fnr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
00401000 >/$  E8 89000000                                        call 吾爱破解.0040108E
00401005 |. 50 push eax ; /ExitCode = 0x0
00401006 \. E8 B5010000 call <jmp.&KERNEL32.ExitProcess> ; \ExitProcess
0040100B . 47 65 74 4E 65 77 53 6F 63 6B 00 ascii "GetNewSock",0
00401016 . 45 72 72 6F 72 00 ascii "Error",0
0040101C . 6B 72 6E 6C 6E 2E 66 6E 65 00 ascii "krnln.fne",0
00401026 . 4E 6F 74 20 66 6F 75 6E 64 20 74 68 65 20 6B 65 ascii "Not found the ke"
00401036 . 72 6E 65 6C 20 6C 69 62 72 61 72 79 20 6F 72 20 ascii "rnel library or "
00401046 . 74 68 65 20 6B 65 72 6E 65 6C 20 6C 69 62 72 61 ascii "the kernel libra"
00401056 . 72 79 20 69 73 20 69 6E 76 61 6C 69 64 21 00 ascii "ry is invalid!",0
00401065 . 6B 72 6E 6C 6E 2E 66 6E 72 00 ascii "krnln.fnr",0
0040106F . 50 61 74 68 00 ascii "Path",0
00401074 . 53 6F 66 74 77 61 72 65 5C 46 6C 79 53 6B 79 5C ascii "Software\FlySky\"
00401084 . 45 5C 49 6E 73 74 61 6C 6C 00 ascii "E\Install",0
0040108E /$ 55 push ebp
0040108F |. 8BEC mov ebp,esp
00401091 |. 81C4 F0FEFFFF add esp,-0x110
00401097 |. 8D85 FCFEFFFF lea eax,[local.65]
0040109D |. 50 push eax
0040109E |. E8 41010000 call 吾爱破解.004011E4
004010A3 |. 68 65104000 push 吾爱破解.00401065 ; /StringToAdd = "krnln.fnr"
004010A8 |. 8D85 FCFEFFFF lea eax,[local.65] ; |
004010AE |. 50 push eax ; |ConcatString = NULL
004010AF |. E8 24010000 call <jmp.&KERNEL32.lstrcatA> ; \lstrcatA

1.4 Delphi

Delphi特点:非常多的call指令,并且push addressretn结合相当于jmp address。比如:

1
2
3
4
push 004A52F4
……
retn
;相当于jmp 004A52F4

1.5 BC++

BC++6与BC++2010都差不多,入口处一样的机器码,接下来调用获取句柄的API函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
004014EC > $ /EB 10         jmp short 吾爱破解.004014FE
004014EE |66 db 66 ; CHAR 'f'
004014EF |62 db 62 ; CHAR 'b'
004014F0 |3A db 3A ; CHAR ':'
004014F1 |43 db 43 ; CHAR 'C'
004014F2 |2B db 2B ; CHAR '+'
004014F3 |2B db 2B ; CHAR '+'
004014F4 |48 db 48 ; CHAR 'H'
004014F5 |4F db 4F ; CHAR 'O'
004014F6 |4F db 4F ; CHAR 'O'
004014F7 |4B db 4B ; CHAR 'K'
004014F8 |90 nop
004014F9 |E9 db E9
004014FA . |ACB04C00 dd offset 吾爱破解.___CPPdebugHook
004014FE > \A1 9FB04C00 mov eax,dword ptr ds:[0x4CB09F]
00401503 . C1E0 02 shl eax,0x2
00401506 . A3 A3B04C00 mov dword ptr ds:[0x4CB0A3],eax
0040150B . 52 push edx ; 吾爱破解.<ModuleEntryPoint>
0040150C . 6A 00 push 0x0 ; /pModule = NULL
0040150E . E8 578F0C00 call <jmp.&KERNEL32.GetModuleHandleA> ; \GetModuleHandleA

1.6 ASM

1
2
0040108B >/$  6A 00            push 0x0                                 ; /pModule = NULL
0040108D |. E8 4A000000 call <jmp.&kernel32.GetModuleHandleA> ; \GetModuleHandleA

ASM特点:用汇编编写的程序都非常小,基本很少遇到,但它可用于改变自身代码,某些病毒就是利用多态和变形的特点隐藏自身。具体可见《OD使用教程》中的多态和变形。

1.7 .NET

.NET编译的程序放在OD调试会出现错误,推荐使用专门的net反编译工具。

1.8 AutoIt

在OD中文搜索引擎可以看到它是用AutoIt v3编译的。

1
2
3
4
中文搜索引擎, 条目 504
地址=0014A9F1
反汇编=push 吾爱破解.001E26A8
文本字符串=AutoIt v3 GUI

1.9 PB

PB可能也是跟易语言的独立编译一样,调用VC的链接程序编译,所以才显示VC6。但它还另加了两个库:libjcc.dllpbvm90.dll

1
2
3
4
5
Executable modules
基址 大小 入口 名称 文件版本 路径
009E0000 0007C000 00A10640 libjcc 4.2.5.06 F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\libjcc.dll
10000000 0000B000 10001B30 吾爱破解 1,0,0,1 F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\吾爱破解论坛学习脱壳实例_PB.exe
10B00000 003F3000 10DB9000 pbvm90 9.0.3.8784 F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\pbvm90.dll

1.10 QT

查壳可看到QT编译,并且OD的E模块也看到了有关QT的库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Executable modules
基址 大小 入口 名称 文件版本 路径
00400000 0000D000 004014C0 52pojie 0.0.0.0 F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\52破解脱壳实例\52pojie.exe
01090000 00633000 01091420 Qt5Widge 5.4.1.0 F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\52破解脱壳实例\Qt5Widgets.dll
016D0000 002AD000 016D1420 icuin53 F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\52破解脱壳实例\icuin53.dll
61940000 00505000 61941420 Qt5Gui 5.4.1.0 F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\52破解脱壳实例\Qt5Gui.dll
64940000 00014000 64941420 libwinpt 1, 0, 0, 0 F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\52破解脱壳实例\libwinpthread-1.dll
66500000 01499000 66501420 icudt53 F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\52破解脱壳实例\icudt53.dll
68430000 0003F000 68462D70 glu32 10.0.19041.1288 C:\Windows\SysWOW64\glu32.dll
68880000 004A6000 68881420 Qt5Core 5.4.1.0 F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\52破解脱壳实例\Qt5Core.dll
68F80000 001B3000 68F81420 icuuc53 F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\52破解脱壳实例\icuuc53.dll
694A0000 00103000 69573D60 opengl32 10.0.19041.1081 C:\Windows\SysWOW64\opengl32.dll
6C640000 0009F000 6C678870 apphelp 10.0.19041.1288 C:\Windows\SysWOW64\apphelp.dll
6E940000 00024000 6E941420 libgcc_s F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\52破解脱壳实例\libgcc_s_dw2-1.dll
6FC40000 00101000 6FC41420 libstdc+ F:\吾爱破解培训\吾爱破解培训第一课例子\无壳程序\52破解脱壳实例\libstdc++-6.dll

1.11 VB

1
2
3
0040129A  - FF25 AC104000   jmp dword ptr ds:[<&msvbvm60.#ThunRTMain_100>]            ; msvbvm60.ThunRTMain
004012A0 > 68 582E4000 push dumped_.00402E58
004012A5 E8 F0FFFFFF call <jmp.&msvbvm60.#ThunRTMain_100>

2. 如何判断是否加壳

  1. 通过查壳工具中内置各种壳的十六进制特征码进行对比查壳
  2. 通过程序入口特征与区段信息来判断

3. 程序加的是什么壳

未加壳、压缩壳、传统加密壳、代码虚拟化保护、.Net程序加密…

3.1 压缩壳

尽量减少可执行文件的大小。

3.1.1 ASPacK

1
00803001 >  60              pushad

看到m模块,除了最基本VS编译特点的区段,ASPacK壳还加了.aspack.adata模块,这两个模块都可以在加壳时自定义名字。

1
2
3
4
5
6
7
8
9
10
Memory map
地址 大小 属主 区段 包含 类型 访问 初始访问 已映射为
007B0000 00001000 吾爱破解 PE 文件头 Imag R RWE
007B1000 00031000 吾爱破解 .text 代码 Imag R RWE
007E2000 0000D000 吾爱破解 .rdata 数据 Imag R RWE
007EF000 00007000 吾爱破解 .data Imag R RWE
007F6000 00004000 吾爱破解 .rsrc 资源 Imag R RWE
007FA000 00009000 吾爱破解 .reloc Imag R RWE
00803000 00003000 吾爱破解 .aspack SFX,输入表 Imag R RWE
00806000 00001000 吾爱破解 .adata Imag R RWE

3.1.2 UPX

1
00287170 > $  60            pushad

3.2 加密壳

抵抗各类调试器和逆向,可能会加入大量干扰代码。

3.2.1 Themida

从查壳工具看到它有个区段没名字,并且最后两段是随机名称。而且它与其他加壳的入口处不一样,不是pushad而是三个push

1
2
3
4
5
00AC4000 >  56              push esi                                 ; 吾爱破解.<ModuleEntryPoint>
00AC4001 50 push eax
00AC4002 53 push ebx
00AC4003 E8 01000000 call 吾爱破解.00AC4009
00AC4008 CC int3

但F8运行一段后也是可以发现有pushad

1
2
3
4
008BC76D    89E8            mov eax,ebp
008BC76F 89E2 mov edx,esp
008BC771 60 pushad
008BC772 E8 00000000 call 吾爱破解.008BC777

3.2.2 VMProtect

可看VMProtect有很多个段,一些段的首字节显示ZERO SIZE。载入OD代码看起来也很乱

1
2
3
4
5
6
7
8
9
007433CC    E8 F6EFFCFF     call 吾爱破解.007123C7
007433D1 48 dec eax
007433D2 DE1F ficomp word ptr ds:[edi]
007433D4 ff5e 43 call far fword ptr ds:[esi+0x43]
007433D7 29F0 sub eax,esi
007433D9 3950 3F cmp dword ptr ds:[eax+0x3F],edx ; 吾爱破解.00610000
007433DC 16 push ss
007433DD 17 pop ss
007433DE F8 clc

进入第一个函数可以看到类似于加密的pushfd标志吧。

1
2
3
4
5
007123C7    9C              pushfd
007123C8 9C pushfd
007123C9 C74424 08 1F660>mov dword ptr ss:[esp+0x8],0xA00A661F
007123D1 9C pushfd
007123D2 E9 B58D0000 jmp 吾爱破解.0071B18C

3.2.3 Shielden

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0050B119 > $ /EB 08         jmp short 吾爱破解.0050B123
0050B11B |00 db 00
0050B11C |C8 db C8
0050B11D |10 db 10
0050B11E |00 db 00
0050B11F |00 db 00
0050B120 |00 db 00
0050B121 |00 db 00
0050B122 |00 db 00
0050B123 >^\E9 E5F2FFFF jmp 吾爱破解.0050A40D
0050B128 . 1978 5B sbb dword ptr ds:[eax+0x5B],edi ; 吾爱破解.<ModuleEntryPoint>
0050B12B > 0f93c3 setae bl
0050B12E . FEC1 inc cl
0050B130 . 8BDE mov ebx,esi ; 吾爱破解.<ModuleEntryPoint>
0050B132 . 60 pushad
0050B133 . F6DF neg bh

F8走几下,也能看到明显的Shielden加壳特征。

1
2
3
4
0050A40D   > /E8 1C000000   call 吾爱破解.0050A42E                       ;  PUSH ASCII "Safengine Shielden v2.3.6.0"
0050A412 . |53 61 66 65 6>ascii "Safengine Shield"
0050A422 . |65 6E 20 76 3>ascii "en v2.3.6.0",0
0050A42E > |9C pushfd

4. 脱壳详解

以下脱壳都是用吾爱专用虚拟机进行操作,因为Win7/8/10(以后仅称Win10)脱壳太不友好,导致各种各样的问题出现。比如Win10系统下LoadPE只显示系统进程、脱壳后的程序无法正常运行(原因是ASLR基地址随机化脱壳插件获取的地址不对),这些问题在XP系统都可以得到解决。

脱壳后的程序回到Win10运行不了的问题,也是因为ASLR基地址随机化。ASLR(Address space layout randomization)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。

在XP虚拟机用LoadPE和Scylla结合脱壳后的程序,移到Win10就打不开的情况(点击后加载了一下主程序却没出来)。OD载入看看

1
2
3
4
5
00F42CA0    8BFF            mov edi,edi                              ; dumped_S.<ModuleEntryPoint>
00F42CA2 55 push ebp
00F42CA3 8BEC mov ebp,esp
00F42CA5 83EC 10 sub esp,0x10
00F42CA8 A1 B0074400 mov eax,dword ptr ds:[0x4407B0]

下面是XP系统同一代码位置的情况

1
2
3
4
5
00422CA0    8BFF            mov edi,edi
00422CA2 55 push ebp
00422CA3 8BEC mov ebp,esp
00422CA5 83EC 10 sub esp,0x10
00422CA8 A1 B0074400 mov eax,dword ptr ds:[0x4407B0]

发现第5行都是4407B0,肯定不对,推测在Win10下这个偏移地址为F607B0(这个地址还会变,因为每次载入程序地址都会随机化)。为了让程序能在Win10正常运行,只需将文件或系统的ASLR取消即可。最好不要取消系统的ASLR,涉及安全问题。

取消文件的ASLR的两种方法:

  1. 打开LoadPE->PE编辑器->特征值后面三个点->将重定位已分离勾选->确定->保存->确定
  2. 打开CFF Explorer->载入文件->File Header->Click Here->将relocation info stripped from file勾选->OK

在XP中取消再移到Win10或直接在Win10取消都行。

4.1 UPX

做作业前推荐观看ximo脱壳基础——手脱UPX壳

用ExeinfoPE查壳可知它是UPX壳,并且可以知道区段的名字可以随意改变,所以用区段名字判断它是什么壳其实不准确。UPX脱壳非常简单,只需利用UPX脱壳工具即可。

1
2
3
4
5
6
7
8
9
10
D:\CTF\tools\upx>upx -d 吾爱破解培训第一课作业一.exe -o 1.exe
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2020
UPX 3.96w Markus Oberhumer, Laszlo Molnar & John Reiser Jan 23rd 2020

File size Ratio Format Name
-------------------- ------ ----------- -----------
upx: 吾爱破解培训第一课作业一.exe: CantUnpackException: file is modified/hacked/protected; take care!!!

Unpacked 0 files.

呃…脱壳工具好鸡肋,看了ximo的手脱UPX壳,现在4种方法都来试试吧。

4.1.1 单步跟踪法

一路F8,遇到向上跳转不实现,向下跳转忽略。

遇到向上跳转时,有两种方法饶过:

  1. 选中向上跳转指令的下一条指令F4(运行至光标处)
  2. 选中向上跳转指令的下一条指令F2->F9->F2(下断点->运行->取消断点)

如果向上跳转的下一条指令是跳转指令或是call指令,那就在下下一条指令进行操作,以此类推。

1
2
3
4
5
6
7
00F87757    61              popad
00F87758 8D4424 80 lea eax,dword ptr ss:[esp-0x80]
00F8775C 6A 00 push 0x0
00F8775E 39C4 cmp esp,eax
00F87760 ^ 75 FA jnz short 吾爱破解.00F8775C
00F87762 83EC 80 sub esp,-0x80
00F87765 - E9 4266FCFF jmp 吾爱破解.00F4DDAC

经过漫长的上述重复操作后,终于来到了第7行的大跳转处,大跳转在加壳程序中往往意味着OEP(注意第1行的popad,其它方法会用到)。

1
2
3
00F4DDAC    E8 EF4E0000     call 吾爱破解.00F52CA0
00F4DDB1 ^ E9 79FEFFFF jmp 吾爱破解.00F4DC2F
00F4DDB6 3B0D B007F700 cmp ecx,dword ptr ds:[0xF707B0]

以上就是真正程序的起始处,结合1所学的各种语言编译后代码的特点,猜测该程序是由VS编译的。

4.1.2 ESP定律

入口代码第1行是pushad,经过这一条指令后,在寄存器的ESP右键->数据窗口中跟随,来到数据窗口,选中第一个hex数据右键->断点->硬件访问->Word(或者在命令窗口输入hr esp回车),F9运行,取消硬件断点,之后就按照4.1.1的方法走,一直走到OEP。

4.1.3 2次内存镜像法

M模块查看内存,找到程序领空的.rsrc表,也就是资源表。虽然吾爱把表名改了,但还是可以在包含里看到“资源”字样,就在.cn处。这里注意,如果资源表是单独一个,就用2次内存镜像,如果与其他表合在一起,只用1次内存镜像即可。

1
2
3
4
5
6
Memory map
地址 大小 属主 区段 包含 类型 访问 初始访问 已映射为
00160000 00001000 吾爱破解 PE 文件头 Imag R RWE
00161000 00039000 吾爱破解 www. Imag R RWE
0019A000 0001E000 吾爱破解 52pojie SFX,代码 Imag R RWE
001B8000 00005000 吾爱破解 .cn 数据,输入表,资源 Imag R RWE

①如果.rsrc单独一个节,选中.rsrc右键->在访问上设置中断->F9。再找一次内存,在PE文件头的下一节地址,也就是161000处下断点,运行,删除断点。

②如果.rsrc并不是单独一个节,只需在PE文件头的下一节地址,也就是161000处下断点,运行,删除断点。

之后按照4.1.1的方法走。

4.1.4 一步直达法

绝大部分UPX壳和AsPack壳都可以用这种方法。载入OD后可直观看到pushad,既然有pushad那肯定有popad。右键->查找->命令,输入popad,不需要勾选整个块。之后与4.1.1同。

在虚拟机的OD找到OEP后,三种方法dump下来。

①用OllyDump插件,起始地址是E模块的第一行的00400000,大小为5D000,OEP的地址为0041DDAC,修正的是偏移地址,所以是0041DDAC-00400000=1DDAC,代码基址和数据基址可以在M模块看,代码基址是3A000,数据基址是58000。用方式1脱壳,保存为ODdump1.exe,可以运行,载入exeinfoPE说有不知名的壳在保护,但点一下插件Advanced Scan就可看到是由VS2008编译的。

在PEID可以直接看到是由VS2008编译的。

ODdump1.exe载入OD后还是显示有压缩代码的存在。

那UPX壳到底有没有脱干净呢?其实只要脱壳后的程序可以正常运行,OEP入口代码为无壳代码特征,IAT解密完资源没有被压缩即可。

②用OllyDump插件,与①完全相同,只是用方式2脱壳,保存为ODdump2.exe,不可运行。

③打开LoadPE,找到OD进程,右键->修复镜像大小->右键->完整转存,将正在调试的程序完整地转存下来,自动生成一个dumped.exe文件,不可运行,壳还没脱。注意,先别关掉OD。打开ImportREC,找到原本的程序(不是dumped.exe),将OEP改为1DDAC->自动查找IAT->获取输入表,没有无效函数,转储到文件,选中dumped.exe打开,会自动生成一个dumped_.exe。这时,dumped_.exe可以运行。

4.2 ASPacK

ASPacK壳都可以用以上4种方法。现在用单步跟踪法温故知新。F8运行到第2行主程序就出来了,也就是我们所说的“跑飞了”。所以要F7跟进去,以后同理。

1
2
3
00430001 >  60              pushad
00430002 E8 03000000 call QQ个性网.0043000A
00430007 - E9 EB045D45 jmp 45A004F7

单步跟踪法与F7、F8结合,运行至retn指令后出来的就是真正程序开始的地方。

在一步直达法查找popad时会遇到很多个popad可能都不是我们需要的,在不知道哪个是我们所要的popad情况下,最好不要用这种方法。

4.2.1 模拟跟踪法

模拟跟踪法包括SFX模拟跟踪都是让OD自动查找程序OEP。

M模块,在程序领空找到包含SFX的区段的地址。在命令窗口输入tc eip < 包含SFX的区段的地址回车,OD会自动跟踪OEP,但这个过程可能会有点漫长。跟踪完后OD会跳到C模块的某条指令上,这条指令可能是OEP,也可能是将要到OEP,需要单步跟踪到达OEP。

4.2.2 SFX模拟跟踪

选项->调试设置->SFX,选块方式跟踪或字节方式跟踪,视情况而定。重载后自动跳到OEP或将要到OEP处。

4.3 nsPack

nsPack壳是北斗的壳,可以用上面6种方法手动脱壳,过程与脱ASPacK壳一样。

nsPack壳作业用①方法用PEID查壳第一行Unknown,第二行显示VS2008,程序正常运行,载入OD也正常。用②方法查壳同①,程序不能正常运行,错误提示“应用程序正常初始化(0xc0000005)失败”,这时用importREC自动查找IAT就可正常运行,载入OD正常。用③方法全部同①。

4.4 FSG 2.0

4.4.1 单步跟踪法

单步跟踪到这三个连续跳转处,第1行代码向上跳,绕过。第2行代码跳到1D4地址处,刚好绕过第3行的大跨度跳转。

1
2
3
4
004001CD  ^\78 F3           js short qqspirit.004001C2
004001CF 75 03 jnz short qqspirit.004001D4
004001D1 - FF63 0C jmp dword ptr ds:[ebx+0xC] ; qqspirit.0040A86D
004001D4 50 push eax ; qqspirit.0042CBC6

选中1D1地址,F4运行至此处。F8就跳到了OEP。

4.4.2 ESP定律

在程序起始处执行到的第一个push指令后,用ESP定律法。

1
2
3
4
5
00400154 >  8725 04A24700   xchg dword ptr ds:[0x47A204],esp
0040015A 61 popad
0040015B 94 xchg eax,esp
0040015C 55 push ebp
0040015D A4 movs byte ptr es:[edi],byte ptr ds:[esi]

之后同单步跟踪。

4.4.3 特殊的ESP定律

FSG 2.0专用的ESP定律。

1
2
3
00400154 >  8725 04A24700   xchg dword ptr ds:[0x47A204],esp
0040015A 61 popad
0040015B 94 xchg eax,esp

执行到popad后(程序起始处就有popad而没有pushad),查看堆栈窗口。

1
2
3
4
0047A208   004001E8  qqspirit.004001E8
0047A20C 004001DC qqspirit.004001DC
0047A210 004001DE qqspirit.004001DE
0047A214 0040A86D qqspirit.0040A86D

顺数第4行就是OEP。选中右键->在反汇编窗口中跟随,发现在反汇编窗口全是空代码,右键->断点->硬件执行,F9运行(或Shift+F9)即可看到代码。如果反汇编窗口中还是看到类似于数据的东西,右键->分析->从模块中删除分析就可看到代码了。

用脱壳的三种方法脱壳后程序查壳都显示完美脱壳了,是由V6编译的。

然而三个都不能正常运行,那可能需要我们手动修复IAT。在真正程序段随便找一个call或mov系统函数的偏移地址,比如425210,它的函数名kernel32.GetVersion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0040A86D  /.  55            push ebp                                 ;  oleaut32.770F0000
0040A86E |. 8BEC mov ebp,esp
0040A870 |. 6A FF push -0x1
0040A872 |. 68 78794200 push qqspirit.00427978
0040A877 |. 68 F4E14000 push qqspirit.0040E1F4 ; SE 处理程序安装
0040A87C |. 64:A1 0000000>mov eax,dword ptr fs:[0]
0040A882 |. 50 push eax
0040A883 |. 64:8925 00000>mov dword ptr fs:[0],esp
0040A88A |. 83EC 58 sub esp,0x58
0040A88D |. 53 push ebx ; qqspirit.0047A208
0040A88E |. 56 push esi ; qqspirit.0042C0C8
0040A88F |. 57 push edi ; qqspirit.004252AC
0040A890 |. 8965 E8 mov [local.6],esp
0040A893 |. FF15 10524200 call dword ptr ds:[0x425210] ; kernel32.GetVersion

可以在importREC中填好OEP->自动查找IAT->获取输入表,验证虚拟地址为25210处是否为GetVersion函数。

在OD的命令窗口输入d 425210回车,数据窗口会刷新数据,右键->长型->地址,程序中所有函数全都显示出来了。往上滑动记下第一个函数地址425000(再往上全是0),往下滑动记下最后一个kernel32系列函数的下一个地址425284

回到importREC填入RVA25000,大小425284-425000=284(比较偷懒的做法就是将大小写成1000或1500,但会产生很多垃圾指针),获取输入表->显示无效函数,无效函数少的话可以直接选中无效函数右键->剪切指针,转储到文件。这时的dumped_.exe可以正常运行了。(无效函数多的话右键->跟踪级别1或跟踪级别3跟一下,或者右键->插件跟踪)

由于OllyDump是很久远的插件了,所以对现在的脱壳不太友好,最好是用③方法脱壳。而在③方法中importREC最好换成Scylla用作IAT修复,因为ImpREC对一些新系统的API支持不好。

Scylla用法跟importREC差不多,但是OEP要填40A86D,VA填425000,大小与importREC一样,同样操作,修复转储后文件。

4.5 PEcompact

PEcompact壳可以用单步跟踪、ESP定律、2次内存镜像法。

使用单步跟踪时,与ASPacK一样都会跑飞,只需F7进入函数即可。

使用ESP定律时,执行完两个push指令后再进行ESP。

1
2
3
4
0040A86D >  B8 74DE4500     mov eax,qqspirit.0045DE74
0040A872 50 push eax ; qqspirit.0045DE74
0040A873 64:FF35 0000000>push dword ptr fs:[0]
0040A87A 64:8925 0000000>mov dword ptr fs:[0],esp

使用2次内存镜像法时,程序跳到此处:

1
2
3
4
5
6
7
0045DE89    C602 E9         mov byte ptr ds:[edx],0xE9
0045DE8C 83C2 05 add edx,0x5
0045DE8F 2BCA sub ecx,edx ; qqspirit.0040A883
0045DE91 894A FC mov dword ptr ds:[edx-0x4],ecx ; qqspirit.0045DE97
0045DE94 33C0 xor eax,eax
0045DE96 C3 retn
0045DE97 B8 F9CB45F0 mov eax,0xF045CBF9

如果执行retn再单步跟踪,程序跑飞。所以要在retn指令的下一条指令下断点运行,再单步跟踪到OEP。

4.5.1 使用VirtualFree

方法一:在命令窗口输入bp VirtualFree回车,运行。取消断点,Alt+F9返回到用户代码处。右键->查找->命令,输入push 8000(特征码),取消勾选整个块,F4运行至此处,单步跟踪。

方法二:在命令窗口输入bp VirtualFree回车,两次F9运行。取消断点,Alt+F9返回到用户代码处。单步跟踪。

4.5.2 使用VirtualAlloc

在命令窗口输入bp VirtualAlloc回车,运行。取消断点,Alt+F9返回到用户代码处。单步跟踪。

4.5.3 使用GetVersion

在命令窗口输入at GetVersion回车,反汇编窗口跳到相关代码处,F2->F9->F2。

1
2
3
4
5
6
7
8
9
10
11
12
7C81127A >  64:A1 18000000  mov eax,dword ptr fs:[0x18]
7C811280 8B48 30 mov ecx,dword ptr ds:[eax+0x30]
7C811283 8B81 B0000000 mov eax,dword ptr ds:[ecx+0xB0]
7C811289 0FB791 AC000000 movzx edx,word ptr ds:[ecx+0xAC]
7C811290 83F0 FE xor eax,-0x2
7C811293 C1E0 0E shl eax,0xE
7C811296 0BC2 or eax,edx ; ntdll.KiFastSystemCallRet
7C811298 C1E0 08 shl eax,0x8
7C81129B 0B81 A8000000 or eax,dword ptr ds:[ecx+0xA8]
7C8112A1 C1E0 08 shl eax,0x8
7C8112A4 0B81 A4000000 or eax,dword ptr ds:[ecx+0xA4]
7C8112AA C3 retn

一直单步到retn处返回父函数。往上滑动发现OEP,父函数就是真正代码处。

4.5.4 最后一次异常法

选项->调试设置->异常,将所有选项取消勾选。插件->StrongOD->skip some exceptions取消勾选。重新载入,按Shift+F9几次,直至出现主程序。

比如在ximo教程中是按了Shift+F9两次后跑飞,所以可知主程序在第一次Shift+F9到第二次Shift+F9的代码之间。重新载入,去到倒数第二次的Shift+F9处,在堆栈窗口查看标有SE处理程序(一般是第二行)的数值0045DE74,在反汇编窗口Ctrl+G查找0045DE74,下断点,Shift+F9运行,取消断点。这就来到了与2次镜像内存法一样的地址处,单步跟踪,也要绕过retn指令。

4.5.5 PEcompact的第一条指令

1
0040A86D >  B8 74DE4500     mov eax,qqspirit.0045DE74

加壳程序的第一条指令就是将偏移地址0045DE74赋值给eax。在命令窗口输入bp 0045DE74回车->运行->取消断点。后面步骤与2次镜像内存法一样的地址处,单步跟踪,也要绕过retn指令。

4.6 EZIP

这个可以用单步跟踪和ESP定律。这次主要讲脱壳后程序崩溃问题。用③方法脱壳没有问题,而用①和②方法都出现了找不到dll的问题。

此时,只要打开LoadPE分别将oddump1.exeoddump2.exe重建PE即可。

4.7 tElock 0.98b1

这个壳可以用最后一次异常法、模拟跟踪法、2次内存镜像法。

这个程序中很多的花指令以及SEH暗桩。

花指令是,由设计者特别构思,希望使反汇编的时候出错,让破解者无法清楚正确地反汇编程序的内容,迷失方向。经典的是,目标位置是另一条指令的中间,这样在反汇编的时候便会出现混乱。花指令有可能利用各种指令:jmp, call, ret的一些堆栈技巧,位置运算,等等。

花指令是程序中的无用代码,程序对它没影响,少了它也能正常运行。加花指令后,杀毒软件对木马静态反汇编时,木马的代码就不会正常显示出来,加大杀毒软件的查杀难度。

SEH,结构化异常处理,是WINDOWS中异常处理的机制,简单地说,就是操作系统维护一个用来处理异常的函数指针的链表,如果发生异常,系统就会顺着这个链表去调用其中的函数,直到某个异常处理函数将异常处理完了,或者所有的函数都不处理异常。

使用SEH的机制来进制反调试,就是SEH暗桩。

程序可以将特定的代码注册成为异常处理函数,然后故意产生一些异常。当程序被调试时,所有的异常事件,都会先发由调试器进行处理,调试器可以决定是否处理,以及如何处理;如果调试器决定不处理,程序自己注册的异常处理入口才会得到执行。对于调试器,并不能保证所有的异常都正确的识别并恰当地处理(因此OD中的很多插件都有anti-anti功能),如果因为调试器对异常情况识别有误,那么程序就会执行完全不同的一条执行路径,这样程序就可以知道自己正在被调试了。

4.7.1 最后一次异常法

选项->调试设置->异常,将所有选项取消勾选。插件->StrongOD->skip some exceptions取消勾选。重新载入,按17次Shift+F9出现主程序。

重载,按16次Shift+F9后,观察堆栈窗口的SE处理程序数值为0042D7FD。在反汇编窗口下断点Shift+F9运行,取消断点。之后用单步跟踪。

4.7.2 模拟跟踪法

使用模拟跟踪法的前提是没有SEH暗桩。而这个程序有很多SEH暗桩。我们知道,按16次Shift+F9没有出现主程序,而在第17次出现主程序,所以第16次到第17次的这一段是没有SEH暗桩的。所以连续按16次Shift+F9再使用模拟跟踪法。

4.7.3 2次内存镜像法

2次内存镜像法即可到达OEP。

用①方法脱壳出现问题:

可以用importREC修复。打开原加壳程序(因为用OD进程会卡死),填好OEP->自动查找IAT->获取输入表,显示无效函数,发现有非常多的无效函数,选中右键->插件跟踪->tElock 0.98,让它自动修复无效函数,修复完后还有5个无效函数,右键->剪切指针。转储到文件,程序正常运行。

用②③方法直接崩溃不出现弹窗。用importREC进行上述修复后同样可以正常运行。

4.8 exe32pack

可以用ESP定律。

4.8.1 使用IsDebuggerPresent

在命令窗口输入bp IsDebuggerPresent回车,运行。取消断点,Alt+F9返回到用户代码处。

1
2
0040ED24    8BBD 013C4000   mov edi,dword ptr ss:[ebp+0x403C01]
0040ED2A 03BD 273C4000 add edi,dword ptr ss:[ebp+0x403C27] ; sticker.00400000

F8执行到下一条指令,查看信息窗口:

1
2
ss:[0040A167]=00400000 (sticker.00400000), ASCII "MZ0"
edi=0000535F

基址为400000,偏移地址为535F,第二条指令就是要它俩相加,结果就是OEP=40535F

Ctrl+G去到40535F,F4运行至光标处,光标处即OEP。

4.9 WinUpack

用单步跟踪法跟踪到这一步,发现这是个大跳转,然而这个跳转却没有实现。

1
0043E635  - 0F84 E3BDFCFF   je 跑跑排行.0040A41E

不能直接改为jmp,因为如果这样改的话在Scylla获取输入表时一个函数都没有。

要让这个跳转实现,即eax要等于0。

1
2
0043E633    85C0            test eax,eax                             ; 跑跑排行.0040C034
0043E635 - 0F84 E3BDFCFF je 跑跑排行.0040A41E

运行到跳转指令处,右键->断点->条件,输入eax==0。F9运行,取消断点,F8跳转至OEP。之后用③方法dump即可。

4.10 脱壳的基本思路及小结

还有非常多的我们没见过的壳,比如KByS、RLPack、PEpack、JDPack、PEncrypt等。但基本都可以用以下几种办法解决:

  1. 单步跟踪
  2. ESP定律
  3. 2次内存镜像
  4. 最后一次异常法
  5. 模拟跟踪法
  6. SFX模拟跟踪

压缩壳修不修正无所谓,但加密壳一定要修正镜像大小。

如果脱壳后的程序无法运行,可能是IAT重定位的问题,无效指针的问题,需要重建PE的问题,包括5的疑难杂症等等。这需要慢慢探索吧。

4.11 ASPack变形壳

使用单步跟踪法、2次内存镜像法可以,但ESP定律不行。

另外,对任何壳,短距离call要跟进去,远距离call可以不跟进去。

4.11.1 利用脚本进行脱壳

载入加壳程序后,插件->ODdgScript->运行脚本->打开对应的壳脚本(后缀可以为txt/osc),自动找到程序OEP。

脱壳脚本要自己收集,如果遇到一些变形壳,脱壳脚本可能就太不管用了,在运行了脱壳脚本的基础上再单步跟踪到OEP。

4.12 ACProtect

4.12.1 ACProtect 1.32

ACProtect 1.32是没有Stolen Code的,脱壳相对简单。

Stolen Code的意思是把被保护程序要运行的代码移走,以进行进一步的保护处理。增加分析难度,脱壳难度、增加反跟踪难度等等。

运行一下程序,发现会弹一个ACProtect壳的NAG窗口,再进入到主程序。

载入OD,选项->调试设置->异常->将非法访问内存取消勾选,插件->SrtongOD->options->将skip some exceptions取消勾选。用最后一次异常法,发现它在第2次Shift+F9跑飞了。所以在第1次Shift+F9(如果第一次就跑飞的话用F9)后,选中堆栈窗口的SE处理程序右键->数据窗口中跟随,在数据窗口中的第一个字节下一个内存访问断点,Shift+F9运行。在反汇编窗口下断点,Shift+F9,再次下断点,Shift+F9。在B模块取消软件断点。如何删除内存访问断点呢?直接右键->断点->删除内存断点即可。去到M模块,选中程序领空的.text区段右键->在访问上设置中断,Shift+F9,弹出NAG窗口,点确定后直达OEP。

用importREC修复IAT,Scylla会加载出非常多无效指针。

4.12.2 Ultraprotect 1.x(stolen code)

查壳显示Ultraprotect壳,这是ACProtect以前的名字,EP区段都显示.perplex

这次运行没有NAG窗口。与上述操作一致,到达“OEP”。

1
2
004010D2    56              push esi
004010D3 FF15 E4634000 call dword ptr ds:[0x4063E4] ; notepad9.0040D1BA

这“OEP”长得非常奇怪,与我们之前遇到的语言入口特征都不一样。考虑到ACProtect会有偷代码的习惯,所以猜测入口特征被移到了某处。

入口特征最常见的是push ebp,先用这个试一下。

同样,删除完3个断点后,F4运行至目前函数的retn处。调试->设置条件(Ctrl+T),勾选“命令是一个”,在输入框填写“push ebp”。调试->跟踪步入(Ctrl+F11),跟踪需要亿点时间,跟踪完后会跳到某个指令处,刚好就是被偷的入口特征代码。

1
2
3
4
5
004254C9    55              push ebp
004254CA 8BEC mov ebp,esp
004254CC 83EC 44 sub esp,0x44
004254CF 60 pushad
004254D0 60 pushad

后面几个是出栈指令,所以被偷的代码应该是前3条。选中右键->二进制->二进制复制。同样在M模块的.text下断点,Shift+F9运行至“OEP”。由于前3条指令占6个字节,所以要粘贴在“OEP”前6个字节处(粘贴前要把粘贴的地方的指令先nop掉)。覆盖后代码如下:

1
2
3
4
5
6
004010CA    90              nop
004010CB 90 nop
004010CC 55 push ebp
004010CD 8BEC mov ebp,esp
004010CF 83EC 44 sub esp,0x44
004010D2 56 push esi

选中真正的OEP右键->此处为新EIP。接下来就是脱壳了。如果遇到很多无效函数,可以用插件跟踪->ACProtect,也可以用跟踪级别跟踪(注意OD进程跟踪会卡死,所以打开加壳程序跟踪)。

4.12.3 Ultraprotect 1.x进阶版

用4.12.2的方法到了跟踪那一步会报错,“无法处理调试异常”。这时,将选项->调试设置->异常->将INT3中断取消勾选,其余勾选。重载,同样用最后一次异常法,取消3个断点后,在命令窗口输入d 12ffc0回车,下硬件访问断点,Shift+F9。程序找到Stolen Code。

1
2
3
4
004C9B30    61              popad
004C9B31 55 push ebp
004C9B32 8BEC mov ebp,esp
004C9B34 6A FF push -0x1

复制,找到“OEP”往上滑,却发现代码全乱了。

1
2
004431F9    68 D8B24400     push NetClean.0044B2D8
004431FE 68 B4334400 push NetClean.004433B4
1
2
3
004431F7   /2e:79 68        bhnt jns short 00443262
004431FA |D8B2 440068B4 fdiv dword ptr ds:[edx-0x4B97FFBC]
00443200 |334400 64 xor eax,dword ptr ds:[eax+eax+0x64]

去一下别的模块再回来,发现又好了,就是不能向上滑动。右键->分析->分析代码。这时往上滑正常。将被偷的代码粘贴上去,修改EIP。

脱壳,运行程序异常。这是因为某些ACProtect 1.x壳有自我保护功能,即入口点如果不是原本那个地址可能就运行不了了。加壳程序入口点4AC000。将已脱壳程序载入OD,定位到4AC000,将被偷代码粘贴,再跳到OEP。

1
2
3
4
5
004AC000    55              push ebp
004AC001 8BEC mov ebp,esp
004AC003 6A FF push -0x1
004AC005 - E9 EA71B9FF jmp 004431F4
;jmp 4431F4也可改为push 4431F4, retn

复制到可执行文件。再用LoadPE编辑入口点为AC000,程序正常执行。

4.12.4 ACProtect 2.0.x

4.12.4.1 VB类

用4.12.1方法直接到达“OEP”4012CE

1
2
3
4
5
004012CE  - FF25 78104000   jmp dword ptr ds:[0x401078]              ; msvbvm60.ThunRTMain
004012D4 29BA A821B144 sub dword ptr ds:[edx+0x44B121A8],edi
004012DA BC 4DB59200 mov esp,0x92B54D
004012DF 0000 add byte ptr ds:[eax],al
004012E1 0000 add byte ptr ds:[eax],al

根据语言的编译特点,发现它是VB程序。以下就是VB程序的典型例子。第一条指令push,第二条指令call,双击call指令就可知道call的地址call 00401044,刚好是OEP的上面一条jmp 系统函数指令。运行到这条jmp指令时,堆栈窗口的第二条就是push的地址。

1
2
3
4
00401044   $- FF25 18104000 jmp dword ptr ds:[<&MSVBVM60.#ThunRTMain_100>]     ;  msvbvm60.ThunRTMain
0040104A ? 0000 add byte ptr ds:[eax],al
0040104C > $ 68 3C1F4000 push 吾爱破解.00401F3C ; ASCII "VB5!#vb6chs.dll" OEP
00401051 . E8 EEFFFFFF call <jmp.&MSVBVM60.#ThunRTMain_100>
1
2
0012FFBC   00401056  返回到 吾爱破解.00401056 来自 <jmp.&MSVBVM60.#ThunRTMain_100>
0012FFC0 00401F3C ASCII "VB5!#vb6chs.dll"

模仿典型例子修改代码:

1
2
3
004012CE  - FF25 78104000   jmp dword ptr ds:[0x401078]              ; msvbvm60.ThunRTMain
004012D4 68 54474000 push QQ个性签.00404754
004012D9 E8 F0FFFFFF call QQ个性签.004012CE ; jmp 到 msvbvm60.ThunRTMain

在真正OEP4012D4处右键->此处为新EIP,脱壳。

4.12.4.2 Delphi

这次的壳发现取消勾选“非内存访问”或“INT3”或两个都取消,都直接跑飞。换方法。

在命令窗口输入bp GetCurrentProcessId回车,运行,取消断点。用LoadPE查看程序的PID为8C8(每次都会变)。

1
2
3
7C8099C0 >  64:A1 18000000  mov eax,dword ptr fs:[0x18]
7C8099C6 8B40 20 mov eax,dword ptr ds:[eax+0x20] ; comctl_1.<ModuleEntryPoint>
7C8099C9 C3 retn

修改为:

1
2
3
4
5
6
7C8099C0 >  B8 64050000     mov eax,0x564
7C8099C5 90 nop
7C8099C6 90 nop
7C8099C7 90 nop
7C8099C8 90 nop
7C8099C9 C3 retn

光标处修改为PID,下面指令都nop掉。

在命令窗口输入bp GetModuleHandleA回车,运行,取消断点。去到M模块,在CODE设置访问中断(F2),F9运行,出现NAG窗口,点确定后,来到OEP。

4.12.5 另类方法解ACProtect

ACProtect 1.4x

取消勾选“非法内存访问”和“int3中断”都跑飞。用4.12.4.2方法找到“OEP”。用4.12.2寻找Stolen Code跟踪时间太过漫长,可以用另一种办法。

1
2
3
0040A54C    50              push eax                                 ; 跑跑排行.00400000
0040A54D E8 1B030000 call 跑跑排行.0040A86D
0040A552 8945 98 mov dword ptr ss:[ebp-0x68],eax ; 跑跑排行.00400000

重载,看到有pushad,试一下用ESP定律,硬件断点先别删除,再用最后一次异常法。Shift+F9连续13次到达最佳脱壳地点。

1
2
3
00441E4D    64:A1 00000000  mov eax,dword ptr fs:[0]
00441E53 8905 39CA4200 mov dword ptr ds:[0x42CA39],eax
00441E59 FF35 39CA4200 push dword ptr ds:[0x42CA39]

但是脱壳却说这个位置没有函数。在importREC先填写OEP为A54C,获取输入表修复IAT后,再将OEP改为41E4D,转储到文件。

程序正常运行,查壳说还有壳,但已经可以进行破解美化DIY了。

4.12.6 补区段

ACProtect壳会对MessageBoxAReigisterHotKey进行处理,所以我们要在它进行处理的地方nop掉不让它处理。

将所有忽略异常勾选,在M模块的.rdata处下断运行(没有.rdata.idata)。单步跟踪看注释窗口出现这两个API函数,把它下面的跳转指令nop掉。

1
2
3
4
5
6
7
8
0043396D    3B85 9CE24100   cmp eax,dword ptr ss:[ebp+0x41E29C]      ; user32.MessageBoxA
00433973 74 20 je short NgaMy.00433995;nop
00433975 90 nop
00433976 90 nop
00433977 90 nop
00433978 90 nop
00433979 3B85 9D014100 cmp eax,dword ptr ss:[ebp+0x41019D] ; user32.RegisterHotKey
0043397F 74 09 je short NgaMy.0043398A;nop

再继续往下有magic跳,改JMP。

1
2
004339C0    80BD D2594100 0>cmp byte ptr ss:[ebp+0x4159D2],0x0
004339C7 74 57 je short NgaMy.00433A20;jmp

M模块的.text下访问中断,Shift+F9运行到此处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
00403D38    68 8C3D4000     push NgaMy.00403D8C
00403D3D 64:A1 00000000 mov eax,dword ptr fs:[0]
00403D43 50 push eax
00403D44 8B4424 10 mov eax,dword ptr ss:[esp+0x10]
00403D48 896C24 10 mov dword ptr ss:[esp+0x10],ebp
00403D4C 8D6C24 10 lea ebp,dword ptr ss:[esp+0x10]
00403D50 2BE0 sub esp,eax
00403D52 53 push ebx
00403D53 56 push esi
00403D54 57 push edi
00403D55 8B45 F8 mov eax,dword ptr ss:[ebp-0x8] ; kernel32.7C817080
00403D58 8965 E8 mov dword ptr ss:[ebp-0x18],esp
00403D5B 50 push eax
00403D5C 8B45 FC mov eax,dword ptr ss:[ebp-0x4]
00403D5F C745 FC FFFFFFF>mov dword ptr ss:[ebp-0x4],-0x1
00403D66 8945 F8 mov dword ptr ss:[ebp-0x8],eax
00403D69 8D45 F0 lea eax,dword ptr ss:[ebp-0x10]
00403D6C 64:A3 00000000 mov dword ptr fs:[0],eax
00403D72 C3 retn

F4去到retn处,F8返回父函数。再去到M模块.text设置内存访问断点运行,F4去到retn处,F8返回父函数。再去到M模块.text设置内存访问断点运行,F4去到retn处,F8返回父函数。到达“OEP”。

1
2
3
4
0040305C    83F9 02         cmp ecx,0x2
0040305F 74 0C je short NgaMy.0040306D
00403061 81CE 00800000 or esi,0x8000
00403067 8935 B0DE4000 mov dword ptr ds:[0x40DEB0],esi

用Scylla脱壳(因为用PEtools和LoadPE脱都显示错误),在importREC输入偏移地址,自动获取输入表没有函数,自己手动查找IAT,起始地址A000,大小174。

下一步,加壳程序重载,用ESP定律。F9运行5次来到这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
004365F4    8915 F5FD4100   mov dword ptr ds:[0x41FDF5],edx          ; ntdll.KiFastSystemCallRet
004365FA FF35 F5FD4100 push dword ptr ds:[0x41FDF5]
00436600 8F05 2DFE4100 pop dword ptr ds:[0x41FE2D] ; kernel32.7C817077
00436606 FF35 2DFE4100 push dword ptr ds:[0x41FE2D]
0043660C C70424 60000000 mov dword ptr ss:[esp],0x60
00436613 56 push esi
00436614 890C24 mov dword ptr ss:[esp],ecx
00436617 68 8DFD4100 push NgaMy.0041FD8D
0043661C 59 pop ecx ; kernel32.7C817077
0043661D 8919 mov dword ptr ds:[ecx],ebx
0043661F 8B0C24 mov ecx,dword ptr ss:[esp] ; kernel32.7C817077
00436622 8F05 ADFE4100 pop dword ptr ds:[0x41FEAD] ; kernel32.7C817077
00436628 FF35 8DFD4100 push dword ptr ds:[0x41FD8D]
0043662E C70424 48A24000 mov dword ptr ss:[esp],NgaMy.0040A248
00436635 8905 B9FD4100 mov dword ptr ds:[0x41FDB9],eax
0043663B FF35 B9FD4100 push dword ptr ds:[0x41FDB9]
00436641 90 nop
00436642 90 nop
00436643 60 pushad
00436644 E8 01000000 call NgaMy.0043664A

pushad前的代码二进制复制下来:

1
2
3
89 15 F5 FD 41 00 FF 35 F5 FD 41 00 8F 05 2D FE 41 00 FF 35 2D FE 41 00 C7 04 24 60 00 00 00 56
89 0C 24 68 8D FD 41 00 59 89 19 8B 0C 24 8F 05 AD FE 41 00 FF 35 8D FD 41 00 C7 04 24 48 A2 40
00 89 05 B9 FD 41 00 FF 35 B9 FD 41 00

取消硬件断点,F4去到pushad的下一条指令,在寄存器窗口找ESP,继续用ESP定律。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
00436F16    68 1DFD4100     push NgaMy.0041FD1D
00436F1B 58 pop eax
00436F1C 8930 mov dword ptr ds:[eax],esi ; NgaMy.00403D38
00436F1E 8F05 79FC4100 pop dword ptr ds:[0x41FC79]
00436F24 8B05 79FC4100 mov eax,dword ptr ds:[0x41FC79]
00436F2A FF35 1DFD4100 push dword ptr ds:[0x41FD1D]
00436F30 56 push esi ; NgaMy.00403D38
00436F31 891C24 mov dword ptr ss:[esp],ebx
00436F34 C70424 383D4000 mov dword ptr ss:[esp],NgaMy.00403D38
00436F3B 8B3424 mov esi,dword ptr ss:[esp]
00436F3E 8F05 A5FE4100 pop dword ptr ds:[0x41FEA5] ; NgaMy.00403D38
00436F44 8905 01FF4100 mov dword ptr ds:[0x41FF01],eax
00436F4A FF35 01FF4100 push dword ptr ds:[0x41FF01]
00436F50 891C24 mov dword ptr ss:[esp],ebx
00436F53 56 push esi ; NgaMy.00403D38
00436F54 C70424 45FE4100 mov dword ptr ss:[esp],NgaMy.0041FE45
00436F5B 8F05 31FE4100 pop dword ptr ds:[0x41FE31] ; NgaMy.0041FE45
00436F61 90 nop
00436F62 90 nop
00436F63 60 pushad
00436F64 E8 01000000 call NgaMy.00436F6A
1
2
3
68 1D FD 41 00 58 89 30 8F 05 79 FC 41 00 8B 05 79 FC 41 00 FF 35 1D FD 41 00 56 89 1C 24 C7 04
24 38 3D 40 00 8B 34 24 8F 05 A5 FE 41 00 89 05 01 FF 41 00 FF 35 01 FF 41 00 89 1C 24 56 C7 04
24 45 FE 41 00 8F 05 31 FE 41 00

重复操作n次,到达OEP。

ESP有:12FFA4、12FF98、12FF94、12FF24、12FF1C、12FF20、12FF1C、12FF1C、12FE8C、12FE8C、12FE90。

在12FE90处下断点后运行到达jmp指令,F8去到一个大跳转,跳到“OEP”。

1
0043BE77   /EB 01               jmp short NgaMy.0043BE7A  
1
0043BE7A  - FF25 BCBE4300   jmp dword ptr ds:[0x43BEBC]              ; NgaMy.0040305C
1
2
0040305C    83F9 02         cmp ecx,0x2
0040305F 74 0C je short NgaMy.0040306D

二进制代码汇总:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
89 15 F5 FD 41 00 FF 35 F5 FD 41 00 8F 05 2D FE 41 00 FF 35 2D FE 41 00 C7 04 24 60 00 00 00 56
89 0C 24 68 8D FD 41 00 59 89 19 8B 0C 24 8F 05 AD FE 41 00 FF 35 8D FD 41 00 C7 04 24 48 A2 40
00 89 05 B9 FD 41 00 FF 35 B9 FD 41 00 68 1D FD 41 00 58 89 30 8F 05 79 FC 41 00 8B 05 79 FC 41
00 FF 35 1D FD 41 00 56 89 1C 24 C7 04 24 38 3D 40 00 8B 34 24 8F 05 A5 FE 41 00 89 05 01 FF 41
00 FF 35 01 FF 41 00 89 1C 24 56 C7 04 24 45 FE 41 00 8F 05 31 FE 41 00 8B 1D 31 FE 41 00 89 33
8F 05 39 FC 41 00 FF 35 39 FC 41 00 5B 8F 05 09 FE 41 00 89 1D 21 FC 41 00 FF 35 21 FC 41 00 C7
05 19 FC 41 00 09 FE 41 00 8B 1D 19 FC 41 00 8B 33 8F 05 FD FB 41 00 8B 1D FD FB 41 00 FF 15 45
FE 41 00 89 0D B1 FD 41 00 FF 35 B1 FD 41 00 8F 05 B5 FC 41 00 FF 35 B5 FC 41 00 56 BE FD FC 41
00 89 3E 5E FF 35 FD FC 41 00 68 94 00 00 00 8F 05 E5 FC 41 00 FF 35 E5 FC 41 00 5F 89 3D 3D FE
41 00 FF 35 3D FE 41 00 8B 0C 24 8F 05 7D FE 41 00 8B 3C 24 8F 05 79 FD 41 00 89 35 25 FC 41 00
FF 35 25 FC 41 00 89 0C 24 8B 3C 24 8F 05 B9 FC 41 00 8F 05 19 FE 41 00 89 05 89 FD 41 00 FF 35
89 FD 41 00 57 BF 19 FE 41 00 8B C7 5F 8B 08 8F 05 95 FC 41 00 8B 05 95 FC 41 00 53 8F 05 5D FE
41 00 FF 35 5D FE 41 00 89 0C 24 89 3D 91 FE 41 00 FF 35 91 FE 41 00 8F 05 81 FC 41 00 89 1D 89
FE 41 00 FF 35 89 FE 41 00 68 81 FC 41 00 5B 8B 0B 8F 05 C9 FC 41 00 8B 1D C9 FC 41 00 57 89 04
24 89 0C 24 8B 04 24 8F 05 D5 FD 41 00 8B 0C 24 8F 05 4D FC 41 00 50 89 14 24 8F 05 BD FE 41 00
FF 35 BD FE 41 00 51 B9 DD FD 41 00 89 39 59 FF 35 DD FD 41 00 C7 05 A9 FE 41 00 60 55 40 00 FF
35 A9 FE 41 00 8B 3C 24 8F 05 95 FD 41 00 89 1D 29 FD 41 00 FF 35 29 FD 41 00 8B DF 8B D3 5B 8F
05 E9 FE 41 00 8B 3D E9 FE 41 00 52 89 1C 24 68 9D FE 41 00 5B 89 13 8B 1C 24 8F 05 49 FE 41 00
8B 14 24 8F 05 69 FD 41 00 FF 15 9D FE 41 00 89 65 E8 89 25 C5 FD 41 00 89 1D 21 FD 41 00 FF 35
21 FD 41 00 68 C5 FD 41 00 5B 8B 33 8B 1C 24 8F 05 A9 FC 41 00 89 3E 57 8F 05 F5 FE 41 00 FF 35
F5 FE 41 00 89 34 24 FF 15 BC A0 40 00 8B 4E 10 50 B8 F9 FB 41 00 89 10 58 FF 35 F9 FB 41 00 56
C7 04 24 AC DE 40 00 8B 14 24 8F 05 AD FD 41 00 89 0A 8F 05 29 FE 41 00 FF 35 29 FE 41 00 5A 8B
46 04 A3 B8 DE 40 00 8B 56 08 52 8F 05 3D FD 41 00 FF 35 3D FD 41 00 8F 05 BC DE 40 00 8B 76 0C
81 E6 FF 7F 00 00 53 BB 35 FE 41 00 89 33 5B FF 35 35 FE 41 00 8F 05 B0 DE 40 00

用topo工具在脱壳修复后的程序申请一个新的区段,数以上字节数或直接输入1000,记下起始的地址:0043E000。OD打开脱完壳后的程序,找到0043E000,粘贴入代码,记住,后面得加跳向假OEP的代码!!保存。

1
JMP 0040305C 

用LoadPE修正入口点为3E000。

4.12.7 AC脱壳出现的问题

  1. 用LoadPE无法抓取进程内存。用Scylla填入OEP,获取输入表,不用管无效函数,转储到文件。再用importREC或Scylla重建IAT。
  2. 用importREC填入OEP无法获取输入表。用Scylla重建IAT,但剪切指针时可能麻烦一点。

4.13 ASProtect

4.13.1 ASProtect 1.2

选项->调试设置->异常,将所有异常取消勾选。用最后一次异常法,Shift+F9运行19次。在retn处F2->Shift+F9->F2。去到M模块,在401000处下断运行,直接去到OEP。

1
2
3
4
5
0040A41E    55              push ebp
0040A41F 8BEC mov ebp,esp
0040A421 6A FF push -0x1
0040A423 68 C8CB4000 push 跑跑赛道.0040CBC8
0040A428 68 A4A54000 push 跑跑赛道.0040A5A4

4.13.2 ASProtect 1.23 RC1

取消勾选“非法内存访问”,最后一次异常法。Shift+F9运行16次。用4.13.1的方法找到OEP。

也可以用另一种方法。运行到retn后,查看堆栈窗口的400000下的第二行的数值12FFA4。在命令窗口下硬件断点hr 12FFA4,运行,F8经过一个大跳转也可去到OEP。

1
2
3
4
0012FF5C   00A0381C
0012FF60 00400000 跑跑排行.00400000
0012FF64 3D375BA5
0012FF68 0012FFA4

脱壳正常运行。

也可以用asprdbgr脱壳辅助工具打开加壳程序,让它自动修复IAT,打开importREC发现只有3个无效指针了,直接剪切掉。同样可以运行。

4.13.3 ASProtect 1.23 RC4

判断ASProtect版本:

  1. ASProtect 1.23 RC4 按shift+f9键26次后来到典型异常,在最近处的retn处设断,跳过异常,f8步跟就会来到fake oep。
  1. ASProtect 1.31 04.27 按shift+f9键36次后来到典型异常,在最近处的retn处设断,跳过异常,f8步跟就会来到foep。

  2. ASProtect 1.31 05.18 按shift+f9键40次后来到典型异常,在最近处的retn处设断,跳过异常,f8步跟就会来到foep。

  3. ASProtect 1.31 06.14 按shift+f9键38次后来到典型异常,在最近处的retn处设断,跳过异常,f8步跟就会来到foep。

用最后一次异常法可能判断不准确,也可以用PEID的VerA插件区分版本。

[1]表示上述第一个版本RC4。

取消勾选“非法访问内存”,Shift+F9运行26次,在retn处F2->Shift+F9->F2。去到M模块,在401000处下断运行,直接去到FOEP。

1
2
3
4
004F27CF    FF15 9CC25200   call dword ptr ds:[0x52C29C]
004F27D5 33D2 xor edx,edx ; ntdll.KiFastSystemCallRet
004F27D7 8AD4 mov dl,ah
004F27D9 8915 34306900 mov dword ptr ds:[0x693034],edx ; ntdll.KiFastSystemCallRet

找回被偷代码,取消勾选“非法访问内存”,Shift+F9运行26次,在retn处F2->Shift+F9->F2。查看堆栈窗口的400000往下数第二行数值为12FFA4,在命令窗口输入hr 12FFA4下硬件断点,运行。

1
2
3
4
0012FF5C   00C86804
0012FF60 00400000 SoWorker.00400000
0012FF64 E3DE7228
0012FF68 0012FFA4

一路F8跟到以下,call跟进去。

1
2
00C8544B    BD 5154C800     mov ebp,0xC85451
00C85450 FF55 03 call dword ptr ss:[ebp+0x3]

发现类似于OEP开头:

1
2
3
4
5
6
7
00C8547A    55              push ebp
00C8547B 8BEC mov ebp,esp
00C8547D 6A FF push -0x1
00C8547F 68 78E35300 push 0x53E378
00C85484 68 407B4F00 push 0x4F7B40
00C85489 64:A1 00000000 mov eax,dword ptr fs:[0]
00C8548F /EB 01 jmp short 00C85492

将jmp以上(不包含jmp)的代码二进制复制

1
55 8B EC 6A FF 68 78 E3 53 00 68 40 7B 4F 00 64 A1 00 00 00 00

执行jmp到以下代码,再复制

1
2
3
4
00C85492    50              push eax
00C85493 64:8925 0000000>mov dword ptr fs:[0],esp
00C8549A 83EC 58 sub esp,0x58
00C8549D EB 01 jmp short 00C854A0
1
50 64 89 25 00 00 00 00 83 EC 58

以此类推

1
00C854A0    53              push ebx
1
53
1
00C854A4    56              push esi
1
56
1
2
3
00C854A8    57              push edi                                 ; SoWorker.006CF040
00C854A9 8965 E8 mov dword ptr ss:[ebp-0x18],esp
00C854AC 26:eb 01 jmp short 00c854b0
1
57 89 65 E8

汇总一下代码:

1
55 8B EC 6A FF 68 78 E3 53 00 68 40 7B 4F 00 64 A1 00 00 00 00 50 64 89 25 00 00 00 00 83 EC 58 53 56 57 89 65 E8

一路F7到这里,再一路F8去到retn,返回父函数。

1
2
3
4
5
6
7
8
9
10
11
00C85378    51              push ecx
00C85379 57 push edi ; SoWorker.006CF040
00C8537A 9C pushfd
00C8537B FC cld
00C8537C BF B953C800 mov edi,0xC853B9
00C85381 B9 5E140000 mov ecx,0x145E
00C85386 F3:AA rep stos byte ptr es:[edi]
00C85388 9D popfd
00C85389 5F pop edi ; SoWorker.004F27CF
00C8538A 59 pop ecx ; SoWorker.004F27CF
00C8538B C3 retn

直接来到FOEP,上面全为0是因为真正的OEP被移到了别处,刚才所做的操作就是把OEP拼接起来。

1
2
3
4
004F27C9    0000            add byte ptr ds:[eax],al
004F27CB 0000 add byte ptr ds:[eax],al
004F27CD 0000 add byte ptr ds:[eax],al
004F27CF FF15 9CC25200 call dword ptr ds:[0x52C29C]

将刚才复制的代码粘贴在它的上方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
004F27A9    55              push ebp
004F27AA 8BEC mov ebp,esp
004F27AC 6A FF push -0x1
004F27AE 68 78E35300 push SoWorker.0053E378
004F27B3 68 407B4F00 push SoWorker.004F7B40
004F27B8 64:A1 00000000 mov eax,dword ptr fs:[0]
004F27BE 50 push eax
004F27BF 64:8925 0000000>mov dword ptr fs:[0],esp
004F27C6 83EC 58 sub esp,0x58
004F27C9 53 push ebx
004F27CA 56 push esi
004F27CB 57 push edi ; SoWorker.006CF040
004F27CC 8965 E8 mov dword ptr ss:[ebp-0x18],esp
004F27CF FF15 9CC25200 call dword ptr ds:[0x52C29C]

设置EIP指向第一条指令。脱壳。

4.13.4 以壳解壳

Stolen Code太多,可以用以壳解壳的方法。以壳解壳就是将Stolen Code及Stolen Code所在区段的壳一并放在主程序里。这样会导致比无壳程序多了很多无用代码,文件大小比无壳程序大很多。

与4.13.3操作几乎一致。由于它有ASLR,所以地址与4.13.3会有所不同。FOEP为4F27CF,OEP为C871DE

打开LoadPE,修正镜像大小,先完整转存一个,再区域转存OEP所在的区段。在PE编辑器打开脱壳程序,区段->选中最后一个区段右键->从磁盘载入区段,将我们刚才保存的区段载入,右键->编辑区段,程序基址为400000,所在区段起始地址为C80000,所以虚拟地址应为C80000-400000=880000。保存->确定。

选项->重建->只选“使PE有效”。重建PE->载入脱壳程序。

用importREC修复IAT,OEP先填写FOEP,修复好指针后再修改OEP为真正的OEP。程序正常运行。

5. 疑难杂症

5.1 附加数据的处理方法

PEID查壳是NSPacK壳,后面写着[overlay],也就是这个程序有附加数据。

overlay真正的意思是取消打开功能,将这些需要读取的数据放到pe文件的后面,让程序自动运行打开的功能。比如mp3文件需要播放器打开,那mp3文件除了音乐数据还需要附加数据,让播放器读到能通过播放器运行。

如果单纯用NSPacK脱壳方法还不足够,会发现脱壳后的程序运行不了。正确方法是将加壳程序后面的附加数据粘贴到脱壳程序的后面。

用PEID查看区段信息,重点关注最后一个区段的R偏移和R大小。

8800h+400h=8C00h

用Win HEX打开加壳程序,Alt+G搜索偏移8C00,选第一个字节右键->选块开始,选文件最后一个字节右键->选块结束,右键->编辑->复制选块->正常。打开脱壳后的程序,选中最后一个字节,右键->编辑->剪贴板数据->粘贴,保存。程序正常运行。

5.2 自校验的去除方法

用了以上各种方法修复程序都运行不了,可以考虑可能是程序有自校验。

自校验原理:程序会检查自己有没有被修改,如果发现被修改的话,便会离开或进行其它动作。基本的校检方法包括checksum,检查大小,检查跳转代码,等等。

将脱壳后的程序也用OD载入,在两个OD的命令窗口下断点bp CreateFileA回车,Alt+F9执行到用户代码,取消断点。接下来对比两个程序执行的不同之处,主要看条件跳转指令。

1
2
3
4
0040120C   /75 07           jnz short 例子.00401215
0040120E |B8 01000000 mov eax,0x1
00401213 |EB 02 jmp short 例子.00401217
00401215 \33C0 xor eax,eax

加壳程序这个跳转不实现,而脱壳程序的这个跳转实现了。只要将脱壳程序的跳转指令修改即可。

1
jnz short 例子.00401215	=>	jz short 例子.00401215

保存后程序正常运行。

6. 脱壳后的简单应用

程序的破解、汉化、美化都要先脱壳才能进行。

6.1 软件汉化及DIY

6.1.1 VB类

VB程序最好用GetVBRes工具。

6.1.2 VC++/VS类

VC++/VS程序可以用Resource Hacker、PEexplorer、xnresource工具。

脱完壳后加载进Resource Hacker,还是显示“有非标准资源”错误提示,说明我们手工脱壳没脱干净,可能在重建IAT时大小设置为1000,导致有很多垃圾指针。可以用fixres修复一下脱壳后的程序。

6.1.3 BC++类/Delphi

BC++/Delphi类最好用PEexplorer工具。

7. 脱壳练习

7.1 kkrunchy 0.23

单步到push后用ESP定律,继续F8往下,看到一个大跳转,却没有实现。

1
003FFD5B   /0F84 9F190000   je UnPackMe.00401700

选中右键->跟随,是一大片空代码。右键->断点->硬件执行->F9,原本空代码的地方已经出现了OEP,删除硬件断点。

1
2
3
4
5
00401700    55              push ebp                                 ; UnPackMe.003F2A08
00401701 8BEC mov ebp,esp
00401703 6A FF push -0x1
00401705 68 00254000 push UnPackMe.00402500
0040170A 68 86184000 push UnPackMe.00401886

用单步跟踪、内存镜像法都是要在大跳转那里同样操作才可到达OEP。

脱壳。注意它的基址是3F0000,所以OEP的偏移地址为11700

7.2 AT4RE Protector

PEID显示yoda’s Protector v1.02

载入OD用2次内存镜像法,在.rsrc资源段设置内存访问断点(A),设置访问中断(F2)断不下来,因为会去到DLL领空,记得要取消内存断点。再在PE文件头下一行下访问中断,运行,到达以下:

1
2
3
4
5
6
7
00407101    8A1C06          mov bl,byte ptr ds:[esi+eax]
00407104 80EB FF sub bl,0xFF
00407107 881C06 mov byte ptr ds:[esi+eax],bl
0040710A 46 inc esi
0040710B 83FE 32 cmp esi,0x32
0040710E ^ 75 F1 jnz short UnPackMe.00407101
00407110 - FFE0 jmp eax ; UnPackMe.00401700

单步跟到jmp eax去到OEP。

7.3 ORiEN v2.11 - 2.12

开始用ESP定律,再单步几下就到了OEP。

1
004A1AF1  - FFE0            jmp eax                                  ; UnPackMe.0045159C
1
2
3
4
0045159C    55              push ebp
0045159D 8BEC mov ebp,esp
0045159F 83C4 F0 add esp,-0x10
004515A2 B8 BC134500 mov eax,UnPackMe.004513BC ; UNICODE ";"

修复时直接删除无效指针,剪切指针程序会出错。

7.4 MoleBox V2.6.5

单步跟,跑飞的函数跟进去,就可跟到OEP。(ESP定律也行)

1
2
3
4
0045159C    55              push ebp
0045159D 8BEC mov ebp,esp
0045159F 83C4 F0 add esp,-0x10
004515A2 B8 BC134500 mov eax,UnPackMe.004513BC ; UNICODE ";"

脱壳,重建输入表时,有很多无效函数,用跟踪1全部修复。

PEID显示无壳,但是程序不能运行。重新操作一次回到无效函数那里,看其中一个无效函数的RVA55170,在OD命令窗口输入d 455170

1
00455170  00477189  UnPackMe.00477189

发现它是程序函数,而不是系统函数,重建表中应该全是系统函数才对。猜测它是被壳加密了。

在OD重载一下程序,再次d 455170,这时这个地址什么都没有。

1
00455170  965AB0EB

右键->断点->硬件访问。Shift+F9直至这个地址出现系统函数。此时处于未加密状态。而这个函数与跟踪1修复函数的名字完全不一样,所以跟踪修复不一定正确。

1
00455170  7C801D53  kernel32.LoadLibraryExA

现在单步跟踪观察是哪一步让它变成程序函数。执行完下面这一步时,函数改变。也就是说这一步导致IAT重建时RVA为55170地址的函数无效。

1
00471682    8901            mov dword ptr ds:[ecx],eax               ; UnPackMe.00477189

为了不让它改变,往上找能绕过这个指令的跳转指令,这个跳转指令称为magic jump

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
00471658   /74 45           je short UnPackMe.0047169F
0047165A |8D55 F0 lea edx,dword ptr ss:[ebp-0x10]
0047165D |52 push edx ; UnPackMe.0047D378
0047165E |6A 04 push 0x4
00471660 |6A 04 push 0x4
00471662 |8B45 08 mov eax,dword ptr ss:[ebp+0x8] ; UnPackMe.00455170
00471665 |50 push eax ; UnPackMe.00477189
00471666 |FF15 40D84700 call dword ptr ds:[0x47D840] ; kernel32.VirtualProtect
0047166C |85C0 test eax,eax ; UnPackMe.00477189
0047166E |75 0A jnz short UnPackMe.0047167A
00471670 |B9 0B0000EF mov ecx,0xEF00000B
00471675 |E8 9D2F0000 call UnPackMe.00474617
0047167A |8B4D 08 mov ecx,dword ptr ss:[ebp+0x8] ; UnPackMe.00455170
0047167D |8B55 F8 mov edx,dword ptr ss:[ebp-0x8] ; UnPackMe.0047D378
00471680 |8B02 mov eax,dword ptr ds:[edx] ; UnPackMe.00477189
00471682 |8901 mov dword ptr ds:[ecx],eax ; UnPackMe.00477189
00471684 |8D4D F4 lea ecx,dword ptr ss:[ebp-0xC]
00471687 |51 push ecx ; UnPackMe.00455170
00471688 |8B55 F0 mov edx,dword ptr ss:[ebp-0x10]
0047168B |52 push edx ; UnPackMe.0047D378
0047168C |6A 04 push 0x4
0047168E |8B45 08 mov eax,dword ptr ss:[ebp+0x8] ; UnPackMe.00455170
00471691 |50 push eax ; UnPackMe.00477189
00471692 |FF15 40D84700 call dword ptr ds:[0x47D840] ; kernel32.VirtualProtect
00471698 |C745 FC 0100000>mov dword ptr ss:[ebp-0x4],0x1
0047169F \8B45 FC mov eax,dword ptr ss:[ebp-0x4]

重载,同样操作走一次,将je指令修改为jmp指令。Ctrl+G去到OEP,下断点运行,发现信息窗口的这个地址还是系统函数。

1
00455170  7C801D53  kernel32.LoadLibraryExA

重建IAT时已经所有函数都有效了。脱壳,正常运行。

7.5 PESpin 1.32(Stolen Code)

ESP定律,Shift+F9运行。单步跟踪发现破碎OEP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
00408D09    55              push ebp
00408D0A /EB 01 jmp short UnPackMe.00408D0D
00408D0C |3D 8BECEB01 cmp eax,0x1EBEC8B
00408D11 2F das
00408D12 6A FF push -0x1
00408D14 EB 01 jmp short UnPackMe.00408D17
00408D16 0C 68 or al,0x68
00408D18 8890 BF01812C mov byte ptr ds:[eax+0x2C8101BF],dl
00408D1E 24 88 and al,0x88
00408D20 6B7F 01 68 imul edi,dword ptr ds:[edi+0x1],0x68
00408D24 ed in eax,dx
00408D25 8824EE mov byte ptr ds:[esi+ebp*8],ah
00408D28 810424 998F1B12 add dword ptr ss:[esp],0x121B8F99
00408D2F 64:A1 00000000 mov eax,dword ptr fs:[0]
00408D35 EB 01 jmp short UnPackMe.00408D38
00408D37 CE into
00408D38 50 push eax
00408D39 EB 01 jmp short UnPackMe.00408D3C

汇总一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
00408D09    55              	push ebp
00408D0D 8BEC mov ebp,esp
00408D12 6A FF push -0x1
00408D17 68 8890BF01 push 0x1BF9088
;00408D1C 812C24 886B7F01 sub dword ptr ss:[esp],0x17F6B88 这个在其他OEP好像没有,去掉(因为OEP不够内存)
00408D23 68 ED8824EE push 0xEE2488ED
;00408D28 810424 998F1B12 add dword ptr ss:[esp],0x121B8F99 这个在其他OEP好像没有,去掉
00408D2F 64:A1 00000000 mov eax,dword ptr fs:[0]
00408D38 50 push eax
00408D3C 64:8925 00000000 mov dword ptr fs:[0],esp
00408D46 83EC 68 sub esp,0x68
00408D4C 53 push ebx
00408D50 56 push esi
00408D54 57 push edi
00408D58 8965 E8 mov dword ptr ss:[ebp-0x18],esp
00408D5E 33DB xor ebx,ebx
00408D63 895D FC mov dword ptr ss:[ebp-0x4],ebx
00408D69 6A 02 push 0x2
00408D6E FF15 90214000 call dword ptr ds:[0x402190] ; msvcrt.__set_app_type
00408D77 59 pop ecx
00408D7B 830D 2C314000 FF or dword ptr ds:[0x40312C],-0x1
00408D85 830D 30314000 FF or dword ptr ds:[0x403130],-0x1
00408D8F FF15 8C214000 call dword ptr ds:[0x40218C] ; msvcrt.__p__fmode

二进制代码汇总:

1
55 8B EC 6A FF 68 88 90 BF 01 68 ED 88 24 EE 64 A1 00 00 00 00 50 64 89 25 00 00 00 00 83 EC 68 53 56 57 89 65 E8 33 DB 89 5D FC 6A 02 FF 15 90 21 40 00 59 83 0D 2C 31 40 00 FF 83 0D 30 31 40 00 FF FF 15 8C 21 40 00

终于到达大跳转:

1
00408D98  - E9 AB89FFFF          jmp UnPackMe.00401748

FOEP:

1
2
3
00401748    8B0D 20314000        mov ecx,dword ptr ds:[0x403120]
0040174E 8908 mov dword ptr ds:[eax],ecx
00401750 FF15 88214000 call dword ptr ds:[0x402188] ; msvcrt.__p__commode

二进制粘贴到FOEP上方,将401700作为新EIP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
00401700      55                 push ebp
00401701 8BEC mov ebp,esp
00401703 6A FF push -0x1
00401705 68 8890BF01 push 0x1BF9088
0040170A 68 ED8824EE push 0xEE2488ED
0040170F 64:A1 00000000 mov eax,dword ptr fs:[0]
00401715 50 push eax ; msvcrt._fmode
00401716 64:8925 00000000 mov dword ptr fs:[0],esp
0040171D 83EC 68 sub esp,0x68
00401720 53 push ebx
00401721 56 push esi
00401722 57 push edi
00401723 8965 E8 mov dword ptr ss:[ebp-0x18],esp
00401726 33DB xor ebx,ebx
00401728 895D FC mov dword ptr ss:[ebp-0x4],ebx
0040172B 6A 02 push 0x2
0040172D FF15 90214000 call dword ptr ds:[0x402190] ; msvcrt.__set_app_type
00401733 59 pop ecx ; 005BFFD0
00401734 830D 2C314000 FF or dword ptr ds:[0x40312C],-0x1
0040173B 830D 30314000 FF or dword ptr ds:[0x403130],-0x1
00401742 FF15 8C214000 call dword ptr ds:[0x40218C] ; msvcrt.__p__fmode
00401748 . 8B0D 20314000 mov ecx,dword ptr ds:[0x403120]

重建IAT用importREC不行,但用Scylla脱壳成功,程序正常运行。

7.6 eXPressor 1.3.0(VB自校验处理)

单步去到大跳转。

1
00407D00  |.- FFE0          jmp eax                                  ;  UnPackMe.004012A0

去到OEP。

1
2
3
0040129A  - FF25 AC104000   jmp dword ptr ds:[<&msvbvm60.#ThunRTMain_100>]     ; msvbvm60.ThunRTMain
004012A0 > 68 582E4000 push dumped_.00402E58
004012A5 E8 F0FFFFFF call <jmp.&msvbvm60.#ThunRTMain_100>

脱壳后还是运行不了,因为本来就是要考自校验的问题。

将脱壳后的程序载入OD,在命令窗口输入bpx papa去到调用模块。

方法一:找到目标为__vbaNew2__vbaNew2的功能是调用程序的子窗口或子函数。根据分析(?),它调用了退出函数。

右键->在每个调用到__vbaNew2上设置断点。一个一个看。往上拉紧挨的cmp与跳转指令要格外关注,F4运行到cmp指令后,观察跳转指令是否绕过退出函数。绕过则不管它,没绕过将跳转实现。

1
2
3
4
5
6
00403D2A    391D E0524000   cmp dword ptr ds:[0x4052E0],ebx
00403D30 75 10 jnz short dumped_.00403D42;跳转实现,不管
00403D32 68 E0524000 push dumped_.004052E0 ; ASCII "d窝"
00403D37 68 2C384000 push dumped_.0040382C
00403D3C FF15 8C104000 call dword ptr ds:[<&msvbvm60.#__vbaNew2_340>] ; msvbvm60.__vbaNew2
00403D42 8B35 E0524000 mov esi,dword ptr ds:[0x4052E0]

看到有两个cmpjnz组合,先F4、F8走一下看它是怎么运行的。

1
2
3
4
5
6
7
00403F35    66:395D E4      cmp word ptr ss:[ebp-0x1C],bx
00403F39 75 56 jnz short dumped_.00403F91;不实现
00403F3B 391D E0524000 cmp dword ptr ds:[0x4052E0],ebx
00403F41 75 10 jnz short dumped_.00403F53;实现
00403F43 68 E0524000 push dumped_.004052E0 ; ASCII "d窝"
00403F48 68 2C384000 push dumped_.0040382C
00403F4D FF15 8C104000 call dword ptr ds:[<&msvbvm60.#__vbaNew2_340>] ; msvbvm60.__vbaNew2

如果jnz不修改,F9出现异常。如果jnz修改为jmp,F9程序正常终止。所以第一个jnz应该修改为jmp

又来两个cmp与跳转指令结合

1
2
3
4
5
6
7
00404295    817D E4 C05D000>cmp dword ptr ss:[ebp-0x1C],0x5DC0
0040429C 7E 54 jle short dumped_.004042F2;不实现
0040429E 391D E0524000 cmp dword ptr ds:[0x4052E0],ebx
004042A4 75 10 jnz short dumped_.004042B6;实现
004042A6 68 E0524000 push dumped_.004052E0 ; ASCII "d窝"
004042AB 68 2C384000 push dumped_.0040382C
004042B0 FF15 8C104000 call dword ptr ds:[<&msvbvm60.#__vbaNew2_340>] ; msvbvm60.__vbaNew2

如果jnz不修改,F9出现正常终止。如果jnz修改为jmp,F9程序弹出主程序。所以jle应该修改为jmp

因为已经可以出现主程序了,所以后面的调用可以不管它。保存两处修改,程序可以跑起来。

7.7 delphi自效验的处理

是FSG 2.0的壳,上面有说,不再赘述。在修复时,会发现只有3个指针,这种情况是不可能有的,所以要手动查找IAT指针。

1
0045273F    A1 E03F4500     mov eax,dword ptr ds:[0x453FE0]

在窗口命令输入d 453FE0,发现全是程序函数还有ASCII码,不是我们要找的IAT指针。

1
2
3
4
00453FE0  00455BB0  UnPackMe.00455BB0
00453FE4 00453014 UnPackMe.00453014
00453FE8 00407138 ASCII "dVE"
00453FEC 0041ADD0 ASCII "dVE"

往下滑啊滑啊(Zzz…),终于来到IAT,起始地址45612C,末尾456738,大小456738-45612C=60C

1
2
3
00456128  00000000
0045612C 7C93135A ntdll.RtlDeleteCriticalSection
00456130 7C9210E0 ntdll.RtlLeaveCriticalSection

脱壳后的程序没有弹出错误提示,双击“运行不了”的情况,大多数是程序有自校验的问题,它是运行了又退出了。

与7.6一样,在命令窗口输入bp FindFirstFileA(dll领空)下断避免后续程序跑飞。在命令窗口输入bpx papa,找到EixtProcess函数下断(因为7.6找不到这个函数,经过分析得出__vbaNew2调用退出函数)。

F9,运行到dll领空Alt+F9返回。再F9运行到达某个退出函数断点处。这是switch语句执行default时的代码。

1
2
3
4
5
6
7
8
9
10
004523A6   > \50            push eax                                 ;  Default case of switch 00451FB8
004523A7 . 89D8 mov eax,ebx
004523A9 . 29D8 sub eax,ebx
004523AB . 89C3 mov ebx,eax
004523AD . 89D8 mov eax,ebx
004523AF . 01D8 add eax,ebx
004523B1 . 89C3 mov ebx,eax
004523B3 . 58 pop eax
004523B4 . 6A 00 push 0x0 ; /ExitCode = 0x0
004523B6 . E8 F540FBFF call <jmp.&kernel32.#ExitProcess_183> ; \ExitProcess

上下找一下发现只有case 2E4E9没有退出函数。

1
004523BB   > \33C0          xor eax,eax                              ;  Case 2E4E9 of switch 00451FB8

这个地址跳转来自很多,但只有一个是条件跳转0045208C,往上看在00452085下断点,重载运行几下运行发现可以停在这里。jg不要实现,因为我们想在je跳转。

1
2
3
00452085   > \3D E9E40200   cmp eax,0x2E4E9
0045208A . 7F 6A jg short dumped_.004520F6
0045208C . 0F84 29030000 je dumped_.004523BB

修改一下

1
2
3
0045208A      90            nop
0045208B 90 nop
0045208C E9 2A030000 jmp dumped_.004523BB

一路F8,在这又遇到了2E4E9,运行到jnz实现了,按道理它实现就不让它实现,nop掉。

1
2
00452775  |.  3D E9E40200   cmp eax,0x2E4E9
0045277A |. 75 0C jnz short dumped_.00452788

一路F8,又遇到了2E4E9,je要改为jmp,要不然就退出了。

1
2
3
4
5
00452419  |.  3D E9E40200   cmp eax,0x2E4E9
0045241E |. 74 07 je short dumped_.00452427
00452420 |. 6A 00 push 0x0 ; /ExitCode = 0x0
00452422 |. E8 8940FBFF call <jmp.&kernel32.#ExitProcess_183> ; \ExitProcess
00452427 |> 33C0 xor eax,eax

修改完这3处,程序正常运行。结果OD不能选择所有修改(我的原因),那就修改一处保存一次重载新程序一次。

7.8 GHF Protector V1.0

脱壳的最佳时机:手动脱壳理想的最佳dump时机是指壳已经把程序代码包括资源等数据全部解密、输入表等数据还原但未填充系统函数地址、dll还没重定位,此时dump出来的文件只需修正OEP、ImportTableRVA等信息即可正常运行完成脱壳。

PEID查壳说是ASProtect 1.32的壳,但载入OD的壳特征明显不是ASProtect,并且程序的图标也被隐藏了。题目给了这是GHF Protector V1.0的壳。(可能不同程序入口代码不同)

1
2
3
4
5
6
00511709 >  50              push eax
0051170A 7C 05 jl short UnPackMe.00511711
0051170C 52 push edx ; ntdll.KiFastSystemCallRet
0051170D c1c4 80 rol esp,0x80
00511710 5A pop edx ; kernel32.7C817077
00511711 58 pop eax ; kernel32.7C817077

7.8.1 最佳脱壳时机

M模块,.idata下断,Shift+F9运行,F7、F8走到这,这里是最佳的脱壳时机。算是这个壳的一个特征,三个跳转指令,且有0x800000000x7FFFFFFF

1
2
3
4
5
005114A2   /74 2A           je short UnPackMe.005114CE
005114A4 |F7C2 00000080 test edx,0x80000000
005114AA |74 08 je short UnPackMe.005114B4
005114AC |81E2 FFFFFF7F and edx,0x7FFFFFFF
005114B2 |EB 04 jmp short UnPackMe.005114B8

用PEtools半脱壳,设置PEtools选项:

或者用LoadPE半脱壳,在选项设置一下:

不要修正镜像大小,否则不能抓取进程。程序正常运行,但壳还没脱干净。

再将半脱壳后的程序载入OD,用ESP定律找到OEP。LoadPE脱壳,还原默认设置。

importREC重建IAT,脱壳成功。

7.8.2 使用LoadLibraryA

在命令窗口输入bp LoadLibraryA回车,Shift+F9运行8次后,Alt+F9返回,取消断点,单步跟踪。(不知道为什么是8次)

到达这里,edx=00401700,是一个大跳转。

1
0040729F  ^\FFE2            jmp edx

F8到达OEP。

用LoadPE,按7.8.1设置PE选项,程序可以运行,但壳还没脱干净,重建IAT即可。如果按PE默认,PEID显示这不是一个有效的PE文件。

或者用PEtools,按如下设置,重建IAT就可成功运行。

7.8.3 使用GlobalFree

在命令窗口输入bp GlobalFree回车,Shift+F9,再Alt+F9,取消断点,单步跟踪也能去到OEP。

7.9 Armadillo(穿山甲) 6.04

穿山甲需要处理Magic jump,都有IAT加密。

7.9.1 使用GetModuleHandleA+9/+5

忽略所有异常,在StrongOD中勾选“跳过某些异常”。在命令窗口输入bp GetModuleHandleA+9下断,Shift+F9运行N次,直到堆栈窗口出现:

1
2
3
4
5
6
001293E4 /0012EB40
001293E8 |00B88683 RETURN to 00B88683 from kernel32.GetModuleHandleA
001293EC |00BB514C ASCII "kernel32.dll"
001293F0 |00BB6D64 ASCII "VirtualFree"
001293F4 |0EF6D207
001293F8 |00454380 UnPackMe.00454380

再Shift+F9一次,Alt+F9返回,单步到Magic Jump,将它nop掉。(我也不知道为什么这里是Magic Jump)

1
00B6AA13   /75 05           jnz short 00B6AA1A;nop

往下拉到这里,F4运行到此处,再撤销刚才对Magic Jump的修改。因为穿山甲会检测某种保护下壳的完整性。

1
00B6AC9A   /EB 03           jmp short 00B6AC9F

接着再下第二个断点bp CreateThread,Shift+F9运行。再Alt+F9返回。

1
2
3
00B7614C    50              push eax
00B7614D FF15 9032BB00 call dword ptr ds:[0xBB3290] ; kernel32.CloseHandle
00B76153 5E pop esi ; UnPackMe.00454380

单步跟踪到这,F7进入即可到达OEP。

1
00B930E5    FFD2            call edx                                 ; UnPackMe.00401700

或者不下第二个断点,在M模块的.text下断运行,单步跟踪到上面指令再F7进入OEP。

1
2
3
4
5
00401700    55              push ebp
00401701 8BEC mov ebp,esp
00401703 6A FF push -0x1
00401705 68 00254000 push UnPackMe.00402500
0040170A 68 86184000 push UnPackMe.00401886

常规脱壳即可。

7.9.2 使用VirtualProtect

在命令窗口输入bp VirtualProtect,处理IAT加密。Shift+F9,注意寄存器ecx。当运行到ecx=00401000时,Alt+F9返回,取消断点。右键->查找->命令(Ctrl+F),输入push 100,勾选“整个块”来到这里。(不勾选会找错地址,血的教训!)

1
2
3
4
5
6
7
00B42EC0    55              push ebp;修改为retn
00B42EC1 8BEC mov ebp,esp
00B42EC3 83EC 2C sub esp,0x2C
00B42EC6 833D 20F6BB00 0>cmp dword ptr ds:[0xBBF620],0x0
00B42ECD 75 59 jnz short 00B42F28
00B42ECF C745 EC 13004BB>mov dword ptr ss:[ebp-0x14],0xBA4B0013
00B42ED6 68 00010000 push 0x100

将第1行代码修改为retn,因为下面的代码是执行加密。

之后再下断点bp CreateThread或者去到M模块.text下断运行,单步跟踪同样去到OEP。

7.10 Armadillo 4.40

脱壳方法同7.9.1,LoadLibraryA下面的跳转就是magic jump?将它修改为jmp(什么时候修改为nop什么时候修改为nop?看它是否跳转?)

1
2
3
4
5
6
00AB5FE3    FF15 BC62AD00   call dword ptr ds:[0xAD62BC]             ; kernel32.LoadLibraryA
00AB5FE9 8B0D AC40AE00 mov ecx,dword ptr ds:[0xAE40AC]
00AB5FEF 89040E mov dword ptr ds:[esi+ecx],eax
00AB5FF2 A1 AC40AE00 mov eax,dword ptr ds:[0xAE40AC]
00AB5FF7 391C06 cmp dword ptr ds:[esi+eax],ebx
00AB5FFA 0F84 2F010000 je 00AB612F

之后同7.9.1

用7.9.2方法好像不太可。

7.11 PEBundle 2.0b5 - 3.0x

用ESP定律找到OEP。脱壳后运行不了,但查壳没壳。考虑是importREC重建IAT时那些被我们剪切掉的指针是有用的,壳将它们加密了让我们以为那些指针没用。

随便找一个无效指针偏移地址为5517C,在OD重载,命令窗口输入d 45517C,在它上面一行(有效指针)下硬件访问断点。F8几下跑飞,但被我们下的硬件断点截住了。断点上面的地址已经出现系统函数。

1
2
3
4
5
0045516C  7C8101B1  kernel32.lstrcpynA
00455170 7C801D53 kernel32.LoadLibraryExA
00455174 7C80A4B5 kernel32.GetThreadLocale
00455178 0005589E
0045517C 000558B0

F8走几下,发现这里是一个循环,并且45517C被加密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
00471E14    8B19            mov ebx,dword ptr ds:[ecx]
00471E16 83C1 04 add ecx,0x4
00471E19 85DB test ebx,ebx ; UnPackMe.004558C4
00471E1B 74 33 je short UnPackMe.00471E50
00471E1D 8BC3 mov eax,ebx ; UnPackMe.004558C4
00471E1F F7C3 00000080 test ebx,0x80000000
00471E25 74 08 je short UnPackMe.00471E2F
00471E27 81E3 FFFF0000 and ebx,0xFFFF
00471E2D EB 04 jmp short UnPackMe.00471E33
00471E2F 43 inc ebx ; UnPackMe.004558C4
00471E30 43 inc ebx ; UnPackMe.004558C4
00471E31 03DA add ebx,edx ; UnPackMe.00400000
00471E33 51 push ecx ; UnPackMe.00455184
00471E34 52 push edx ; UnPackMe.00400000
00471E35 899D C2214000 mov dword ptr ss:[ebp+0x4021C2],ebx ; UnPackMe.004558C4
00471E3B 53 push ebx ; UnPackMe.004558C4
00471E3C FFB5 BA214000 push dword ptr ss:[ebp+0x4021BA] ; kernel32.7C800000
00471E42 E8 32010000 call UnPackMe.00471F79
00471E47 5A pop edx ; UnPackMe.00471DBC
00471E48 59 pop ecx ; UnPackMe.00471DBC
00471E49 85C0 test eax,eax ; UnPackMe.0047214A
00471E4B 74 05 je short UnPackMe.00471E52
00471E4D AB stos dword ptr es:[edi]
00471E4E ^ EB C4 jmp short UnPackMe.00471E14
1
2
00455178  7C801EF2  kernel32.GetStartupInfoA
0045517C 00471F79 UnPackMe.00471F79

再准确一点,程序运行完下面这条指令,45517C显示被加密。

1
00471E4D    AB              stos dword ptr es:[edi]

STOS指令:是将AL/AX/EAX的值存储到[EDI]指定的内存单元中。

往上看最近的eax就是call产生的。所以这个call很可能是加密函数。跟进去,着重看寄存器eax。我们已知455178的函数没有被加密,程序运行到这一步,eax显示函数。

1
2
00471FBF    85C0            test eax,eax                                       ; kernel32.GetStartupInfoA
00471FC1 /74 25 je short UnPackMe.00471FE8;没有实现

到这一步eax显示为0。

1
2
00471FDF    85C0            test eax,eax
00471FE1 /74 02 je short UnPackMe.00471FE5;跳转实现

一路到stos,函数出现在数据窗口。第二次循环就是我们要找的45517C的函数。进去到471FBF处,这已经显示了45517C未加密的系统函数kernel32.GetProcAddress,可以直接在import REC上修改。但我们需要将所有被加密的函数还原,不可能一个一个这样找,太麻烦了。

1
2
00471FBF    85C0            test eax,eax                                       ; kernel32.GetProcAddress
00471FC1 /74 25 je short UnPackMe.00471FE8;没有实现

继续对比两者异同,到这一步eax显示的是加密函数UnPackMe.00471F79

1
2
00471FDF    85C0            test eax,eax                                       ; UnPackMe.00471F79
00471FE1 74 02 je short UnPackMe.00471FE5;没有实现

471FE1处,未加密函数跳转实现而加密函数跳转未实现,所以应该将471FE1处的跳转改为jmp。继续单步观察数据窗口,验证修改是否正确。

1
2
00455178  7C801EF2  kernel32.GetStartupInfoA
0045517C 7C80AE40 kernel32.GetProcAddress

复制到可执行文件,将新的程序载入OD寻找OEP,脱壳,没有无效指针,程序成功运行。

7.12 PUNiSHER 1.5

这个壳比较特殊,下面用原版OD进行破解。首先忽略所有异常,隐藏好OD。Options -> Debugging options -> Exceptions,全选。

载入OD,程序停在此处。在命令行下断bp LoadLibraryA+5

1
2
3
00408061 > /EB 04           jmp short UnPackMe.00408067
00408063 |83A4BC CE60EB04>and dword ptr ss:[esp+edi*4+0x4EB60CE],0>
0040806B BC 0411E800 mov esp,0xE81104

Shift + F9运行程序,查看堆栈变化,第一次:

1
2
3
4
0012FF98   FFFEBDA9
0012FF9C 004083EC RETURN to UnPackMe.004083EC
0012FFA0 0040821F ASCII "USER32.DLL"
0012FFA4 005E5918

Shift + F9第二次,已经看到LoadLibraryA了:

1
2
3
4
0012E9B4  /0012EA5C
0012E9B8 |73FBE2BF RETURN to usp10.73FBE2BF from kernel32.LoadLibraryA
0012E9BC |73FA1840 ASCII "gdi32.dll"
0012E9C0 |73FBE4B9 usp10.<ModuleEntryPoint>

再Shift + F9一次,此时为我们的最佳返回时机。Alt + F9返回:

1
2
3
4
0012FF98   FFFA31E5
0012FF9C 003C0470 RETURN to 003C0470
0012FFA0 003C00B7 ASCII "USER32.DLL"
0012FFA4 005E5918

取消断点,F8单步,遇到向上跳转时忽略,继续往下。直到去到003C08C5处。

1
2
3
4
003C08C5    8D85 ADD64100   lea eax,dword ptr ss:[ebp+0x41D6AD]
003C08CB 870424 xchg dword ptr ss:[esp],eax ; UnPackMe.00402494
003C08CE FF95 E1CE4100 call dword ptr ss:[ebp+0x41CEE1] ; kernel32.OutputDebugStringA
003C08D4 80BD E9CE4100 0>cmp byte ptr ss:[ebp+0x41CEE9],0x0

此时,信息窗口显示:

1
2
Address=003C0892, (ASCII "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s")
eax=00402494 (UnPackMe.00402494)

把上面的汇编代码前两行NOP掉,避免产生溢出错误。继续往下,来到两个连续的jmp指令。

1
2
003C0B1C  ^\EB BF           jmp short 003C0ADD	;不跳转
003C0B1E ^ E9 36FEFFFF jmp 003C0959 ;跳转

第一个jmp指令不跳,第二个jmp指令跳转。跳转后继续单步,看到大跳转了,跳过去就是Stolen Code的开始处。

1
2
3
4
5
6
7
8
9
10
11
003C0959    8B85 EACE4100   mov eax,dword ptr ss:[ebp+0x41CEEA]
003C095F 8D9D FEDC4100 lea ebx,dword ptr ss:[ebp+0x41DCFE]
003C0965 50 push eax
003C0966 E8 0E000000 call 003C0979
003C096B 50 push eax
003C096C 53 push ebx ; UnPackMe.00400000
003C096D E8 8EF7FFFF call 003C0100
003C0972 83C4 08 add esp,0x8
003C0975 - FF6424 FC jmp dword ptr ss:[esp-0x4]
003C0979 60 pushad
003C097A EB 04 jmp short 003C0980

跳过去是jmp指令,继续F8找到熟悉的VC程序入口,遇到近call要F7跟进去。

1
2
3
4
009F0000   /EB 04           jmp short 009F0006
009F0002 |8182 8241D9EE E>add dword ptr ds:[edx+0xEED94182],0x8C83>
009F000C 8C82 DB5C24FC mov word ptr ds:[edx+0xFC245CDB],es
009F0012 EB 04 jmp short 009F0018

来到这儿时,终于看到被抽取代码的了,VC程序入口:

1
2
3
4
5
6
7
8
9
10
11
009F022D    90              nop
009F022E 64:A1 00000000 mov eax,dword ptr fs:[0]
009F0234 50 push eax
009F0235 64:8925 0000000>mov dword ptr fs:[0],esp
009F023C 83EC 68 sub esp,0x68
009F023F 53 push ebx
009F0240 56 push esi ; UnPackMe.00404038
009F0241 57 push edi ; UnPackMe.00407B90
009F0242 8965 E8 mov dword ptr ss:[ebp-0x18],esp
009F0245 33DB xor ebx,ebx
009F0247 895D FC mov dword ptr ss:[ebp-0x4],ebx

但是前面还缺少了55开头的OEP,此时堆栈前2行就是第4、第5行push的地址。自行补上:

1
2
3
4
5
push ebp
mov ebp esp
push -1
push 004023D0
push 00401616

继续找被抽取的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
009F02B7    90              nop
009F02B8 FF15 7C214000 call dword ptr ds:[0x40217C] ; msvcrt.__set_app_type
009F02BE 59 pop ecx
009F02BF 830D 3C314000 F>or dword ptr ds:[0x40313C],0xFFFFFFFF
009F02C6 830D 40314000 F>or dword ptr ds:[0x403140],0xFFFFFFFF
009F02CD FF15 78214000 call dword ptr ds:[0x402178] ; msvcrt.__p__fmode
009F02D3 8B0D 30314000 mov ecx,dword ptr ds:[0x403130]
009F02D9 8908 mov dword ptr ds:[eax],ecx
009F02DB FF15 74214000 call dword ptr ds:[0x402174] ; msvcrt.__p__commode
009F02E1 8B0D 2C314000 mov ecx,dword ptr ds:[0x40312C]
009F02E7 8908 mov dword ptr ds:[eax],ecx
009F02E9 A1 70214000 mov eax,dword ptr ds:[0x402170]
009F02EE 8B00 mov eax,dword ptr ds:[eax]
009F02F0 A3 38314000 mov dword ptr ds:[0x403138],eax

遇到下面这种垃圾语句一定要将它改为NOP,否则程序会出错。

1
009F037C    0F31            rdtsc

接下来遇到这一语句,注释和信息窗口都显示0040开头,是我们的用户代码段,这里把指令转换,还原我们的真实地址。

1
009F03DA    8B6D 00         mov ebp,dword ptr ss:[ebp]               ; UnPackMe.00401615

即在被抽取代码段中添加call 00401615

运行到这不要retn,F4运行到jmp处,继续F8单步走。

1
2
009F0402    C3              retn
009F0403 EB 04 jmp short 009F0409

提取最后一句被抽取代码,retn去到伪OEP401504处。

1
2
3
4
009F0409    90              nop
009F040A 391D 50304000 cmp dword ptr ds:[0x403050],ebx;最后一句被抽取的代码
009F0410 68 04154000 push 0x401504
009F0415 C3 retn

总结一下被抽取的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
push ebp
mov ebp esp
push -1
push 004023D0
push 00401616
009F022E 64:A1 00000000 mov eax,dword ptr fs:[0]
009F0234 50 push eax
009F0235 64:8925 0000000>mov dword ptr fs:[0],esp
009F023C 83EC 68 sub esp,0x68
009F023F 53 push ebx
009F0240 56 push esi ; UnPackMe.00404038
009F0241 57 push edi ; UnPackMe.00407B90
009F0242 8965 E8 mov dword ptr ss:[ebp-0x18],esp
009F0245 33DB xor ebx,ebx
009F0247 895D FC mov dword ptr ss:[ebp-0x4],ebx
call 00401615
009F040A 391D 50304000 cmp dword ptr ds:[0x403050],ebx

去到伪OEP,如果看到全是字节,右键 -> Analysis -> remove analysis from module 。这样就可以看到汇编代码了。

1
2
3
4
5
00401504   /75 0C           jnz short UnPackMe.00401512
00401506 |68 12164000 push UnPackMe.00401612
0040150B |FF15 6C214000 call dword ptr ds:[0x40216C] ; msvcrt.__setusermatherr
00401511 |59 pop ecx ; UnPackMe.00407B90
00401512 \E8 E9000000 call UnPackMe.00401600

往上拉发现代码被混淆,先不管。拉到retn下面的一句,也就是4014A7处,往下NOP到伪代码处。(后来发现4014A7不够,4014A6也需要NOP掉)

1
2
3
4
5
6
004014A1    c2 2513         retn 0x1325
004014A4 ^ E3 A5 jecxz short UnPackMe.0040144B
004014A6 cb retf
004014A7 7B D5 jpo short UnPackMe.0040147E
004014A9 64:03d4 add edx,esp
004014AC 3F aas

发现401504也被NOP掉了,选中被NOP掉的前3行右键 -> Undo selection ,还原代码。

1
2
3
4
5
00401504    90              nop
00401505 90 nop
00401506 90 nop
00401507 1216 adc dl,byte ptr ds:[esi]
00401509 40 inc eax

接下来我们去段首可以选择二进制粘贴的形式,也可以采取逐一汇编的方式进行修改。

二进制代码如下:

1
2
3
4
55 8B EC 6A FF 68 D0 23 40 00 68 16 16 40 00 64 A1 00 00 00 00 50 64 89 25 00 00 00 00 83 EC 68
53 56 57 89 65 E8 33 DB 89 5D FC 6A 02 FF 15 7C 21 40 00 59 83 0D 3C 31 40 00 FF 83 0D 40 31 40
00 FF FF 15 78 21 40 00 8B 0D 30 31 40 00 89 08 FF 15 74 21 40 00 8B 0D 2C 31 40 00 89 08 A1 70
21 40 00 8B 00 A3 38 31 40 00 E8 17 01 00 00 39 1D 50 30 40 00

选中所有NOP二进制粘贴,新建EIP,运行loadPE和importREC,修正指针,脱壳成功。

7.13 未知壳

7.13.1 PolyBox

这个是个捆绑壳,意思是伪装成其他类别的壳,加大脱壳的难度。

PEID查壳显示FSG壳,这种壳用了FSG的外衣,把要加壳的程序当成一种资源加密起来。

用FSG专用的ESP定律找到第2层壳的OEP。

1
2
3
4
00402F18    55              push ebp
00402F19 8BEC mov ebp,esp
00402F1B 83C4 F0 add esp,-0x10
00402F1E B8 C02E4000 mov eax,UnPackMe.00402EC0 ; ASCII "\n"

在命令窗口输入bp WriteProcessMemoryWriteProcessMemory是读取程序的大小,也可以解释为处理写入的数据。F9运行,程序停在dll领空,查看堆栈窗口,它给程序写入了起始地址为3D0000的程序。

1
2
3
4
5
6
7
0012FDE4   0040201B  /CALL 到 WriteProcessMemory 来自 UnPackMe.00402016
0012FDE8 00000048 |hProcess = 00000048 (window)
0012FDEC 00400000 |Address = 0x400000
0012FDF0 003D0000 |Buffer = 003D0000
0012FDF4 00001000 |BytesToWrite = 1000 (4096.)
0012FDF8 0012FF48 \pBytesWritten = 0012FF48
0012FDFC 0012FE08 指向下一个 SEH 记录的指针

用LoadPE找到进程,右键->区域转存,找到起始地址为3D0000的区域转存,后缀名改为exe即可运行。

7.13.2 UPX(Stolen Code)

单步和ESP定律都不太行,用两次内存镜像到达一串jmp指令的地方。

1
2
004014E4  - FF25 1C204000   jmp dword ptr ds:[<&MFC42.#CWinApp::InitApplication_3922>]                ; mfc42.#CWinApp::InitApplication_3922
004014EA - FF25 20204000 jmp dword ptr ds:[<&MFC42.#CWinApp::AddToRecentFileList_1089>] ; mfc42.#CWinApp::AddToRecentFileList_1089

很明显这是由MFC写的程序,拿一个完整的MFC程序做对比。

1
2
3
4
5
6
7
8
9
10
11
12
0040163D     C3                        RETN
0040163E - FF25 94214000 JMP DWORD PTR DS:[<&msvcrt.__CxxFrameHandler>]
00401644 CC INT3
00401645 CC INT3
0040164F CC INT3
00401650 - FF25 90214000 JMP DWORD PTR DS:[<&msvcrt._except_handler3>]
00401656 55 PUSH EBP;这里是OEP
00401657 8BEC MOV EBP,ESP
00401659 6A FF PUSH -1
0040165B 68 E0234000 PUSH 004023E0
00401660 68 50164000 PUSH <JMP.&msvcrt._except_handler3>
00401665 64:A1 00000000 MOV EAX,DWORD PTR FS:[0]

被偷代码的程序:

1
2
3
4
5
6
7
8
9
10
11
004016F5    C3              retn
004016F6 - FF25 A4214000 jmp dword ptr ds:[<&MSVCRT.__CxxFrameHandler>] ; msvcrt.__CxxFrameHandler
004016FC CC int3
004016FD CC int3
004016FE CC int3
004016FF CC int3
00401700 98 cwde
00401701 e5 27 in eax,0x27
00401703 69AB ED2F71B3 0>imul ebp,dword ptr ds:[ebx-0x4C8ED013],0xC6844200
0040170D 084A 64 or byte ptr ds:[edx+0x64],cl
00401710 A1 00000000 mov eax,dword ptr ds:[0]

OEP前面的代码都是一样的,所以我们要找<&msvcrt._except_handler3>函数的地址。查看与它同模块的<&MSVCRT.__CxxFrameHandler>的地址(双击即可查看)为4021A4,在命令窗口输入d 4021A4回车。

1
2
3
4
5
00402194 >77C05C94  msvcrt._except_handler3
00402198 >77C1EE4F msvcrt._controlfp
0040219C >77C04DF8 msvcrt._onexit
004021A0 >77C04E51 msvcrt.__dllonexit
004021A4 >77BF27FA msvcrt.__CxxFrameHandler

很容易找到<&msvcrt._except_handler3>的地址为402194,然后是入口代码,都一样的,只有两个下面的push指令不一样。因为第二个push也刚好是<&msvcrt._except_handler3>函数地址,所以我们只需找第一个push的地址。

把堆栈窗口的滚动条往下拉到底(因为最先压栈的在最底部),从下往上看,找到第一个压入的程序函数,就是我们第一个push进去的地址。

1
2
3
4
5
6
7
8
9
10
11
12
0012FFD0   0012FFE0  指向下一个 SEH 记录的指针
0012FFD4 00401886 SE处理程序
0012FFD8 00402500 UnPackMe.00402500;程序函数,所以是这个
0012FFDC 00000000
0012FFE0 0012CFA0 指向下一个 SEH 记录的指针
0012FFE4 7C839AD8 SE处理程序
0012FFE8 7C817080 返回到 kernel32.7C817080;这个是系统函数,不是
0012FFEC 00000000
0012FFF0 00000000
0012FFF4 00000000
0012FFF8 00407000 UnPackMe.<ModuleEntryPoint>;这个是入口点
0012FFFC 00000000

还有最后一句,正常程序的代码为64:A1 00000000,而加壳程序的代码与上面一句混淆了,所以也要将它修改为64:A1 00000000

将代码拼凑起来,由于中间的代码不够用,所以把第1行jmp去掉,因为其它代码都是入口点必须的。

1
2
3
4
5
6
7
jmp 402194;去掉
push ebp
mov ebp,esp
push -1
push 402500
push 402194
mov eax,dword ptr fs:[0]
1
2
3
4
5
6
00401700    55              push ebp;将此处设置为新EIP
00401701 8BEC mov ebp,esp
00401703 6A FF push -0x1
00401705 68 00254000 push UnPackMe.00402500
0040170A 68 94214000 push <&MSVCRT._except_handler3>
0040170F 64:A1 00000000 mov eax,dword ptr fs:[0]

这时用ODdump说“不能创建程序”,用LoadPE也不能抓取进程,是因为这个程序由反调试功能。用任务管理器将程序进程结束,再在OD用ODdump即可。

7.13.3 yoda’s Protector 1.03.3

PEID说是这个壳,但也不知道是不是。OD载入

1
2
3
0040A6ED >  E8 03000000     call UnPackMe.0040A6F5
0040A6F2 EB 01 jmp short UnPackMe.0040A6F5
0040A6F4 E9 BB550000 jmp UnPackMe.0040FCB4

忽略除非法内存访问的所有异常,在StrongOD中取消勾选“跳过某些异常”。重载,F9运行。程序停在空白代码处。

方法一:此时堆栈窗口显示

1
2
0012EBE0   0012EC70  指向下一个 SEH 记录的指针
0012EBE4 0040CA2C SE处理程序

在反汇编窗口Ctrl+G,跟随SE句柄。F2下断,Shift+F9运行,取消断点。

1
2
3
0040CA2C    55              push ebp
0040CA2D 8BEC mov ebp,esp
0040CA2F 57 push edi

F8往下,走到此处EDI的值就是OEP。

1
0040CA55    3E:89B8 B800000>mov dword ptr ds:[eax+0xB8],edi          ; UnPackMe.00401700

Ctrl+G跟随401700到达OEP。脱壳,一定要修正镜像大小。这个程序是有涉及到锁键盘、锁任务栏的,但可以用这种方法巧妙绕过。

方法二:在M模块.text处下断,Shift+F9也能运行到

1
2
3
0040CA2C    55              push ebp
0040CA2D 8BEC mov ebp,esp
0040CA2F 57 push edi

往后操作一样。