逆向脱壳

基本脱壳及修复

基础知识

加壳程序

  • 1、壳是什么?
  • 加壳是可执行程序资源压缩,是保护文件的常用手段。加壳过的程序可以直接运行,但是不能查看源代码,要经过脱壳才可以查看源代码。
  • 2、加壳的原理是什么?壳是怎么运作的?
  • 加壳是利用特殊的算法,对EXE、DLL文件里的资源进行压缩、加密。类似WINZIP 的效果,只不过这个压缩之后的文件,可以独立运行,解压过程完全隐蔽,都在内存中完成。它们附加在原程序上通过Windows加载器载入内存后,先于原始程序执行,得到控制权,执行过程中对原始程序进行解密、还原,还原完成后再把控制权交还给原始程序,执行原来的代码部分。加上外壳后,原始程序代码在磁盘文件中一般是以加密后的形式存在的,只在执行时在内存中还原,这样就可以比较有效地防止破解者对程序文件的非法修改,同时也可以防止程序被静态反编译。
  • 简单来说,壳可以完成代码的压缩和实时解压执行,加壳程序可以正常执行,但是看不到原来的代码,反编译的时候看不懂。
  • 3、壳的分类
  • 壳的类型通常分为压缩壳和加密壳两类。
  • 压缩壳的特点是减小软件体积大小,加密保护不是重点。
  • 加密壳种类比较多,不同的壳侧重点不同,一些壳单纯保护程序,另一些壳提供额外的功能,如提供注册机制、使用次数、时间限制等。

脱壳

  • 1、对特定的壳可以用专门的脱壳软件进行脱壳。
  • 2、也可以手动脱壳

OD

反汇编工具OD=OllyDebug,OllyDbg 是一种具有可视化界面的 32 位汇编-分析调试器。它的特别之处在于可以在没有源代码时解决问题,并且可以处理其它编译器无法解决的难题。

手动脱壳的基本思路

  • 1、用软件查壳,可用PEID或者ExeinfoPE查壳。
  • 2、用进行OD动态反汇编,找到程序真正的入口。
  • 3、用OD插件或者loadPE脱壳。

OEP

  • OEP:(Original Entry Point),程序的入口点。软件加壳一般隐藏了程序真实的OEP(或者用了假的OEP), 我们需要寻找程序真正的OEP,才可以完成脱壳。
    一般加壳程序在使用Ollydbg等动态调试工具时,会停在壳的预处理块。即处在对于程序原始代码块的解压或解密操作之前,在运行完程序自脱壳模块后,会停留在程序加壳之前的OEP位置,此时是dump程序的最佳时期。脱壳时在真实OEP处下int3断点,就可以捕捉到程序代码段完全恢复的状态。因此,寻找加壳程序的正确OEP,也成了手动脱壳时的第一要务。

OPE的基本特征

VC6

  • 区段特征

  • 入口特征

  • EBP为基指针(Base Pointer)寄存器,用它可直接存取堆栈中的数据;

  • ESP为堆栈指针(Stack Pointer)寄存器,用它只可访问栈顶。

  • push ebp;保存ebp内容以便调用完后恢复;此时esp <- esp - 4

  • mov ebp esp; 保存调用前栈顶地址以便调用完后恢复,用ebp来保存当前esp。

VS2008和VS2013

  • 入口特征

  • 第一行是call XXXX

  • 第二行是 jmp XXXX

  • call里面是push ebp mov ebp esp

常见语言的入口点:

VB:

004012D4 > 68 54474000 push QQ个性网.00404754
004012D9 E8 F0FFFFFF call <jmp.&MSVBVM60.#100>
004012DE 0000 add byte ptr ds:[eax],al
004012E0 0000 add byte ptr ds:[eax],al
004012E2 0000 add byte ptr ds:[eax],al
004012E4 3000 xor byte ptr ds:[eax],al
004012E6 0000 add byte ptr ds:[eax],al
004012E8 48 dec eax

delphi:

004A5C54 > 55 push ebp
004A5C55 8BEC mov ebp,esp
004A5C57 83C4 F0 add esp,-10
004A5C5A B8 EC594A00 mov eax,openpro.004A59EC

BC++:

00401678 > /EB 10 jmp short btengine.0040168A
0040167A |66:623A bound di,dword ptr ds:[edx]
0040167D |43 inc ebx
0040167E |2B2B sub ebp,dword ptr ds:[ebx]
00401680 |48 dec eax
00401681 |4F dec edi
00401682 |4F dec edi
00401683 |4B dec ebx
00401684 |90 nop
00401685 -|E9 98005400 jmp 00941722
0040168A \A1 8B005400 mov eax,dword ptr ds:[54008B]
0040168F C1E0 02 shl eax,2
00401692 A3 8F005400 mov dword ptr ds:[54008F],eax
00401697 52 push edx
00401698 6A 00 push 0
0040169A E8 99D01300 call <jmp.&KERNEL32.GetModuleHandleA>
0040169F 8BD0 mov edx,eax

VC++:

0040A41E > 55 push ebp
0040A41F 8BEC mov ebp,esp
0040A421 6A FF push -1
0040A423 68 C8CB4000 push 跑跑排行.0040CBC8
0040A428 68 A4A54000 push <jmp.&MSVCRT._except_handler3>
0040A42D 64:A1 00000000 mov eax,dword ptr fs:[0]
0040A433 50 push eax
0040A434 64:8925 0000000>mov dword ptr fs:[0],esp
0040A43B 83EC 68 sub esp,68
0040A43E 53 push ebx
0040A43F 56 push esi
0040A440 57 push edi

MASM(汇编):

004035C9 > 6A 00 push 0
004035CB E8 A20A0000 call <jmp.&kernel32.GetModuleHandleA>
004035D0 A3 5B704000 mov dword ptr ds:[40705B],eax
004035D5 68 80000000 push 80
004035DA 68 2C754000 push 11.0040752C
004035DF FF35 5B704000 push dword ptr ds:[40705B]
004035E5 E8 820A0000 call <jmp.&kernel32.GetModuleFileNameA>
004035EA E8 87070000 call 11.00403D76
004035EF 6A 00 push 0
004035F1 68 0B364000 push 11.0040360B
004035F6 6A 00 push 0
004035F8 6A 64 push 64
004035FA FF35 5B704000 push dword ptr ds:[40705B]

手动脱壳方法

  • 方法1:单步跟踪

  • 方法2:ESP定律法

  • 方法3:2次内存镜像法

  • 方法4:一步直达法

常用汇编

1.MOV 传送字或字节.
2.MOVSX 先符号扩展,再传送.
3.MOVZX 先零扩展,再传送.
4.PUSH 把字压入堆栈.
5.POP 把字弹出堆栈.
6.PUSHA 把AX,CX,DX,BX,SP,BP,SI,DI依次压入堆栈.
7.POPA 把DI,SI,BP,SP,BX,DX,CX,AX依次弹出堆栈.
8.JMP 无条件转移指令
9.CALL 过程调用
10.RET/RETF过程返回.
11.2>条件转移指令 (短转移,-128到+127的距离内)
( 当且仅当(SF XOR OF)=1时,OP1<OP2 )
12.JA/JNBE 不小于或不等于时转移.
13.JAE/JNB 大于或等于转移.
14.JB/JNAE 小于转移.
15.JBE/JNA 小于或等于转移.
以上四条,测试无符号整数运算的结果(标志C和Z).
16.JG/JNLE 大于转移.
17.JGE/JNL 大于或等于转移.
18.JL/JNGE 小于转移.
19.JLE/JNG 小于或等于转移.
以上四条,测试带符号整数运算的结果(标志S,O和Z).
20.JE/JZ 等于转移.
21.JNE/JNZ 不等于时转移.
22.JC 有进位时转移.
23.JNC 无进位时转移.
24JNO 不溢出时转移.
25.JNP/JPO 奇偶性为奇数时转移.
26.JNS 符号位为 "0" 时转移.
27.JO 溢出转移.
28.JP/JPE 奇偶性为偶数时转移.
29.JS 符号位为 "1" 时转移.
30.3>循环控制指令(短转移)
31.LOOP CX不为零时循环.
32.LOOPE/LOOPZ CX不为零且标志Z=1时循环.
33.LOOPNE/LOOPNZ CX不为零且标志Z=0时循环.
34.JCXZ CX为零时转移.
35.JECXZ ECX为零时转移.

寄存器相关知识

  • EAX : 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。

  • ECX : 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。

  • EDX : 总是被用来放整数除法产生的余数。

  • EBX : 是"基地址"(base)寄存器, 在内存寻址时存放基地址。

  • ESP : 专门用作堆栈指针,被形象地称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,ESP也就越来越小。在32位平台上,ESP每次减少4字节。

  • EBP : 是"基址指针"(BASE POINTER), 它最经常被用作高级语言函数调用的"框架指针"(frame pointer).

push ebp ;保存当前ebp
mov ebp,esp ;EBP设为当前堆栈指针
sub esp, xxx ;预留xxx字节给函数临时变量.
  • ESI/EDI:分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.

  • EIP: 寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。

  • ES:附加段寄存器

  • CS:代码段寄存器

  • SS:堆栈段寄存器

  • DS:数据段寄存器

  • FS:标志段寄存器

  • GS:全局寄存器

UPX (the Ultimate Packer for eXecutables)

是一款先进的可执行程序文件压缩器,压缩过的可执行文件体积缩小50%-70% ,这样减少了磁盘占用空间、网络上传下载的时间和其它分布以及存储费用。 通过 UPX 压缩过的程序和程序库完全没有功能损失和压缩之前一样可正常地运行,对于支持的大多数格式没有运行时间或内存的不利后果。 UPX 支持许多不同的可执行文件格式 包含 Windows 95/98/ME/NT/2000/XP/CE 程序和动态链接库、DOS 程序、 Linux 可执行文件和核心。(来自百度百科)

下面,我们来给它脱壳~

1、单步运行法

  • 原理:单步跟踪法的原理就是通过Ollydbg的单步(F8)、单步进入(F7)和运行到(F4)功能,完整走过程序的自脱壳过程,跳过一些循环恢复代码的片段,并用单步进入确保程序不会略过OEP。这样可以在软件自动脱壳模块运行完毕后,到达OEP,并dump程序。
  • 为什么要一直往下执行呢?
  • 压缩壳的作用是代码的压缩和实时解压执行,一般加壳程序会在开头有一段代码执行解压缩,然后再运行到原来的程序,运行完开头壳的代码部分,自然就完成了解压缩,运行到了OPE。
  • 步骤:
  • 向上的跳转不实现,在跳转的下一步F4,运行到选定位置;向下的跳转实现,直接单步运行。
  • 一直单步运行找到一个大跳转,大的跳转之后一般就是ope。
  • 然后用OD插件或者loadPE脱壳即可得到无壳的程序。
    单步运行后找到大跳转:

找到OEP:
Push ebp
Mov ebp,esp
将上一个函数的基址入栈,从当前esp开始作为新基址.
EBP是当前函数的存取指针,即存储或者读取数时的指针基地址;ESP就是当前函数的栈顶指针。每一次发生函数的调用(主函数调用子函数)时,在被调用函数初始时,都会把当前函数(主函数)的EBP压栈,以便从子函数返回到主函数时可以获取EBP。

遇到问题:

原因是ASLR基地址随机化脱壳插件获取的地址不对,不能用win7win8脱壳,换XP虚拟机。

2、ESP定律法

  • ESP定律法是脱壳的利器,是应用频率最高的脱壳方法之一。
  • 原理:ESP定律的原理在于程序中堆栈平衡的合理利用。由于在程序自解密或者自解压过程中,不少壳会先将当前寄存器内容压栈,如使用pushad,在解压结束后,会将之前的寄存器值出栈,如使用popad。因此在寄存器出栈时,往往程序代码被自动恢复,此时硬件断点触发。然后在程序当前位置,只需要少许单步跟踪,就很容易到达正确的OEP位置。
  • PUSHAD指令压入32位寄存器,其入栈顺是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI .即把所有的寄存器都压栈。
  • “堆栈平衡”原理:call命令是访问子程序的一个汇编基本指令,它先向堆栈中压入下一行程序的地址,再JMP到call的子程序地址处。与call对应的就是RETN,将当前的ESP中指向的地址出栈;JMP到这个地址。这里有一个前提:如果我们要返回父程序,则当我们在堆栈中进行堆栈的操作的时候,一定要保证在RETN这条指令之前,ESP指向的是我们压入栈中的地址。Call和retn的时候地址是对应的,这就是“堆栈平衡”。
    ###esp定律适用范围:几乎全部的压缩壳,部分加密壳。

脱壳步骤:

  • 1、单步运行,右侧寄存器ESP为红色,说明esp值改变。当前ESP为pushad压栈当前所有寄存器以后的栈顶位置。根据“堆栈平衡”原理。脱壳以后,还会在这个位置出栈。

  • 脱壳前寄存器值:

  • 2、所以,我们右键esp,选择在数据窗口跟随

  • 3、在数据窗口,我们选一个位置,右键“断点”->“硬件访问”->“Byte”设置硬件断点,然后“调试”->“硬件断点”,查看断点。
  • 为什么要在这里设置断点(⊙o⊙)?因为我们可以把壳看成一个子程序,解压缩或者解密完了,它要把控制权交还给主程序。一开始pushad入栈,返回程序本身的时候要在同样的位置出栈,ESP是栈顶指针,出栈时栈顶指针和入栈时相同,而出栈时,壳的部分运行完成了,也就到了OPE附近。

  • 4、运行程序到断点处,删除断点。根据ESP定律,程序入口在断点附近。

  • 此时的寄存器,可以看到,除了EIP,其他和pushad时完全一样。

  • 5、单步运行,找到OPE

  • 6、用OD插件脱壳成功

2次内存镜像法

步骤

  • 1、Alt+M查看内存

  • 2、找到程序段的第一个.rsrc ,设置中断访问并运行。

  • 3、再设一次断点,运行

  • 4、单步运行,又到了popad

  • 5、找到OPE,脱壳

遇到的问题

  • 问题:出错,第二次设断运行后,没有删除内存断点,再单步时出错。
  • 解决:出错是因为没有删除访问中断,删除访问中断就可以了。

原理

  • 断点:让程序在运行时暂停到特定的代码段,下断点是调试器的功能之一,可以让程序中断在需要的地方,从而方便其分析。
  • 常用的断点包括:INT3断点、硬件断点、内存断点、消息断点、条件断点等。
  • 内存访问断点分为,内存访问断点和内存写入断点。
  • 内存访问:代码段的代码访问内存数据。
  • 内存写入:代码段的代码执行完成后把数据写入内存
  • 内存访问断点:代码段的代码访问内存数据的时候,程序暂停。
  • 内存写入断点:代码段的代码写入内存数据的时候,程序暂停。
  • 如果给内存设内存访问断点,那么,程序就会在读取该内存数据的代码上中断。
  • 比如:
    寄存器间接寻址,操作数在以该寄存器中的内容为偏移地址的内存单元中,把以“ecx”值为地址的内存单元的值赋予dl,如果在地址为“ecx”值的内存下断点,那么,程序运行到这一条指令时就会中断。
  • 壳在解压缩时,需要在内存读取压缩过的源代码,进行解压缩,然后再进行内存写入。
  • 第一次内存断点设在资源段,假设解压顺序是先代码段,数据段,再资源段,那么。资源段发生写入中断时,程序就已经完成了代码段和数据段的解压。然后,我们在解压完成的基础上,在代码段下第二个断点,程序再次中断时,就运行到了代码段的OPE附近。
  • 在第二次中断后,经过几次单步运行即可到达OPE。

一步直达法

  • 这个非常简单,但是有适用范围,适用于USP和aspack
  • 原理:开头有pushad入栈,那么一定会有popad出栈,出栈时就说解压缩完成后,到达了OPE附近。
  • 步骤:
  • 直接搜索POPAD,然后F4,运行到POPAD,即可找到OPE

- 注意:不要勾选整个块
- 如图,jump过去以后就说OPE

脱壳后的修复

IAT修复

IAT

导入地址表(IAT):Import Address Table 由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL 中.当PE 文件被装入内存的时候,Windows 装载器才将DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成.

为什么要手动修复IAT?

我们已经掌握了如何寻找OEP和脱壳,有的时候,Dump出来的时候不能正常运行,是因为还有一个输入表没有进行处理,这时候,我们可以尝试手动修复IAT。

原理

程序的IAT是连续的排列的,所以我们只需要找到IAT的起始位置和末位置,就可以确定IAT的地址和大小。在压缩壳中,我们只要找一个调用系统的API的Call的地址,然后在数据窗口中查找,确定IAT起始和结束地址。然后在OD中手动修复。

思路

  • 1.查壳
  • 2.找到程序入口
  • 3.OD插件脱壳-->失败
  • 4.LoadPE脱壳-->失败
  • 5.用ImportREC自动修复-->失败
  • 6.手动查找IAT,用ImportREC进行修复-->成功

实验过程

  • 查壳,显示FSG壳

  • 我们用ESP定律法脱壳,关键跳转后,没有到OPE,右键分析,从模块中删除分析。

  • 到达OPE

  • 用OD的脱壳插件脱壳,这个插件是可以自动修复ITA的,如图勾择重建输入表。

  • 用OD自带的插件脱壳以后,查壳显示无壳,但不能运行。

  • 换一种方法脱壳试试~用LoadPE脱壳

  • 修正镜像大小

  • 还是不能运行

  • 用ImportREC进行输入表重建,尝试修复程序,输入刚刚调试时找到的OPE地址,点击自动查找IAT

  • 点击获取输入表,工具自动帮你填充RVA,这里的RVA是IAT起始的偏移量。RAV是偏移地址的意思,如果说Windows装载器将一个PE文件装入 00400000h处的内存中,而某个节中的某个数据被装入0040xxxxh处,那么这个数据的RVA就是(0040xxxxh- 00400000h)=xxxxh。

- 转储到文件,尝试运行,壳已经没有了,但是还是不行,不能运行……

- 那我们只有试一下手动修复了

  • 随便找一个调用系统的API的Call,如果这个函数的调用地址在IAT中,那么我们根据这个地址就能找到IAT的地址范围。

- 在输入表(IAT)中找到该地址

  • 记录地址0x425210,在命令下输入 d 425210

  • 到达数据窗口的这一段,这是IAT地址,名称与输入表重建工具中的函数对比,内容一致,可以确定这个CALL的调用函数地址在IAT中。

  • 因为IAT段是连续的,因此向上找到开头,向下找到结尾

  • IAT段开始:

  • IAT段结束

- 获取IAT信息
起始:00425000
结束:00425510
RAV:0x00425000-0x00400000=0x25000
Size:0x0042410-0x00425000=0x510

- 进行修复,输入IAT信息,RVA和SIZE,获取输入表。

- 转存,运行成功~脱壳完成~

posted @ 2017-06-06 21:05  君知らない  阅读(1872)  评论(0编辑  收藏  举报