脱壳基础知识以及简单应用

发布者:Seclusion
发布于:2019-06-17 17:38
闲谈: 最近因为需要,需要完成几个脱壳的小例子,之前只是单纯的了解过linux平台下的漏洞利用,对于脱壳其实并不十分熟悉,拿到手的资料也只是流程性的操作,并不知道其中的原理,就学习了一下《加密与解密》里面的脱壳部分,记录下来,最近看到一个比较完整的脱壳教程,很不错,推荐给大家。使用OllyDbg从零开始Cracking--安于此生

一 、壳概述

1 . 壳的概念

正常的软件开发者不对开发的软件进行保护的话,会导致开发的软件源代码很容易的被心怀不轨的人窃取到,而程序的壳便是抵抗第一过程的手段,就如自然界中的昆虫壳的作用一样,程序壳便是保护程序不被非法分子获取甚至破坏的。

2 . 壳的工作过程

加壳过程是用加壳程序对源程序进行压缩、加密、转换指令等操作,然后一般是在程序的开头加上一段壳程序。当加壳程序运行时,在程序开头的壳程序会对加壳程序进行解压缩、解密代码或者数据、解释执行相应代码等过程,壳程序运行完之后一般会将加壳程序恢复成源程序,加壳程序便依旧可以执行相应的功能。

3 . 壳的几种分类

压缩壳:UPX 、 ASPack

压缩壳主要是为了让程序文件占用更小的体积,对于程序的加密保护方面侧重性不是很强。

加密壳 : ASProtect 、Armadillo 、EXECryptor 、Themida

加密壳的功能更加侧重安全性,对于加壳后程序的体积大小并不是十分关注,它能够将程序的重要代码、数据等进行加密,甚至可以提供额外的功能,比如注册机制、使用次数、时间限制等。有的加密壳也涉及到了虚拟机保护技术。

虚拟机保护软件: VMProtect

虚拟机保护技术最近经常听到,但是也是刚刚才弄明白是什么意思,简单来说也是软件保护技术的一种,他可以将我们的程序代码转换成字节码,然后跳转到虚拟机解释执行,这样代码保护程序就是相当高的了。《加密与解密》里面的一句话形容的我觉得十分恰当而且有趣,“就好比把一篇文章从英文翻译成中文后,发现文章里的很多段落是用孟加拉语写的一样。”当然这样的破解成本也是相当的高了。但是虚拟机保护技术的运用会使得程序代码的效率降低,对此一般只会对于特定的关键部分代码进行虚拟机保护处理,这是对安全和效率的权衡。

4 . 其他

虽然壳的保护效果很好,但是目前大多的商业软件并不会通过壳来保护他们的软件。首先因为加壳程序的兼容性问题,某些程序加壳后可能因为兼容性不能在某些平台上使用;其次也因为目前大多数的杀毒软件会对程序软件进行安全监测,其可以自动识别并脱去公开的商业壳,然后分析程序的安全性,但是如果是杀毒软件都能脱去的壳那么对于软件的保护也就是微乎其微了,如果使用杀毒软件不能识别的自己开发的个人的壳,杀毒软件出于安全考虑会自动识别软件为病毒,因为对于程序加壳似乎并不能实现程序的推广。

因此目前大多数厂商会设计包含复杂算法的序列号、设置多个“暗桩”等技术来实现程序保护。

其实照我说,开源就完了,你开源我开源大家都开源,那样就省的破解了。


二、脱壳技术


1 . 壳的加载过程

1. 保存入口参数(pushad/popad pushfd/popfd)

2. 获取壳本身使用的API地址

    关键函数:

    LoadLibraryA(W)、LoadLibraryExA(W) : 将制定的DLL文件映射进内存,返回模块的句柄

    GetmoduleHandle: 获取已经映射进内存的dll文件的句柄,返回值句柄

    GetProcAddress : 获取dll内制定的函数地址,返回函数地址

3.解密源程序的各个区块的数据

4. IAT的初始化

5. 重定位项的处理(DLL文件脱壳)

6. HOOK API

7. 跳转到OEP


2 . 脱壳三部曲

1. 寻找EOP

2. 抓取内存映像

3. 重建PE文件

3 . 脱壳一般工具


4 . 寻找OEP


4.1 . 按照跨段指令寻找OEP

加壳后


加壳前


程序开头

00413000 >  60              pushad  # 保存寄存器的值
00413001    E8 C2000000     call    004130C8
00413006    2E:3001         xor     byte ptr cs:[ecx], al
00413009    0000            add     byte ptr [eax], al
0041300B    0000            add     byte ptr [eax], al
0041300D    0000            add     byte ptr [eax], al
0041300F    0000            add     byte ptr [eax], al
00413011    003E            add     byte ptr [esi], bh
00413013    3001            xor     byte ptr [ecx], al
00413015    002E            add     byte ptr [esi], ch
00413017    3001            xor     byte ptr [ecx], al
00413019    0000            add     byte ptr [eax], al
0041301B    0000            add     byte ptr [eax], al
0041301D    0000            add     byte ptr [eax], al
0041301F    0000            add     byte ptr [eax], al

申请内存

004130FE    50              push    eax
004130FF    FF55 2E         call    dword ptr [ebp+2E]
00413102    8985 B8000000   mov     dword ptr [ebp+B8], eax
00413108    6A 04           push    4
0041310A    68 00100000     push    1000
0041310F    FFB5 8F000000   push    dword ptr [ebp+8F]
00413115    6A 00           push    0
00413117    FF95 B8000000   call    dword ptr [ebp+B8]               ; KERNEL32.VirtualAlloc
0041311D    50              push    eax
0041311E    8985 C4000000   mov     dword ptr [ebp+C4], eax
00413124    8B9D 8B000000   mov     ebx, dword ptr [ebp+8B]

跳转到壳的第二部分

0041310A    68 00100000     push    1000
0041310F    FFB5 8F000000   push    dword ptr [ebp+8F]
00413115    6A 00           push    0
00413117    FF95 B8000000   call    dword ptr [ebp+B8]
0041311D    50              push    eax
0041311E    8985 C4000000   mov     dword ptr [ebp+C4], eax
00413124    8B9D 8B000000   mov     ebx, dword ptr [ebp+8B]
0041312A    03DD            add     ebx, ebp
0041312C    50              push    eax
0041312D    53              push    ebx
0041312E    E8 04000000     call    00413137
00413133    5A              pop     edx
00413134    55              push    ebp
00413135  - FFE2            jmp     edx

获取oep并跳转

00020263    C742 50 0010000>mov     dword ptr [edx+50], 1000
0002026A    FF85 59030000   inc     dword ptr [ebp+359]
00020270    8B85 89020000   mov     eax, dword ptr [ebp+289]
00020276    0385 51030000   add     eax, dword ptr [ebp+351]
0002027C    0185 84020000   add     dword ptr [ebp+284], eax
00020282    61              popad
00020283    68 30114000     push    401130
00020288    C3              retn

跳转到oep开启正常程序逻辑

00401130  /.  55            push    ebp
00401131  |.  8BEC          mov     ebp, esp
00401133  |.  6A FF         push    -1
00401135  |.  68 B8504000   push    004050B8
0040113A  |.  68 FC1D4000   push    00401DFC                         ;  SE 处理程序安装
0040113F  |.  64:A1 0000000>mov     eax, dword ptr fs:[0]
00401145  |.  50            push    eax
00401146  |.  64:8925 00000>mov     dword ptr fs:[0], esp
0040114D  |.  83EC 58       sub     esp, 58
00401150  |.  53            push    ebx

查看程序的整个section信息位于.text内。

Memory map
地址       大小       属主       区段       包含          类型   访问      初始访问  已映射为
00010000   00010000                                       Map    RW        RW
00020000   00001000                                       Priv   RW        RW
00030000   00001000                                       Priv   RW        RW
00040000   00016000                                       Map    R         R
00095000   0000B000                                       Priv   RW  保护    RW
0019C000   00002000                                       Priv   RW  保护    RW
0019E000   00002000                         堆栈 于  主?      Priv   RW  保护    RW
001A0000   00004000                                       Map    R         R
001B0000   00002000                                       Priv   RW        RW
001F5000   0000B000                                       Priv   RW  保护    RW
00274000   00004000                                       Priv   RW        RW
00278000   00003000                         数据块 于  ?      Priv   RW        RW
0027B000   00003000                         数据块 于  ?      Priv   RW        RW
0027E000   00001000                         数据块 于  ?      Priv   RW        RW
00400000   00001000   RebPE                 PE 文件头        Imag   R         RWE
00401000   00004000   RebPE      .text      代码            Imag   R         RWE
00405000   00001000   RebPE      .rdata                   Imag   R         RWE
00406000   00003000   RebPE      .data      数据            Imag   R         RWE
00409000   0000A000   RebPE      .rsrc      资源            Imag   R         RWE
00413000   00007000   RebPE      .pediy     SFX,输入表       Imag   R         RWE


4.2 用内存访问断点寻找oep

OD可以对程序设置内存访问断点,这样当程序开始读取或者执行相应代码时,程序会中断,并且把断点清除,利用这个办法我们可以寻找oep。

因为普通的压缩、加密壳等是按区段处理数据的,一般的顺序是.text 、 .rdate、.data 、 .rsrc的顺序处理数据,我们可以现在.rsrc处设置按F2设置内存访问断点,当程序断下后可表明.text段已经处理完毕,那么我们可以在接着对.text段设置内存访问断点,这样当程序执行.text代码时便会中断在oep处。

对data端设置内存访问断点


然后F9执行到中断处

00413145    A4              movs    byte ptr es:[edi], byte ptr [esi>
00413146    B3 02           mov     bl, 2
00413148    E8 6D000000     call    004131BA
0041314D  ^ 73 F6           jnb     short 00413145
0041314F    33C9            xor     ecx, ecx
00413151    E8 64000000     call    004131BA
00413156    73 1C           jnb     short 00413174
00413158    33C0            xor     eax, eax
0041315A    E8 5B000000     call    004131BA
0041315F    73 23           jnb     short 00413184
00413161    B3 02           mov     bl, 2
00413163    41              inc     ecx

在对text端设置内存访问断点


按F9执行到中断处,会停在oep的位置。

00401130  /.  55            push    ebp
00401131  |.  8BEC          mov     ebp, esp
00401133  |.  6A FF         push    -1
00401135  |.  68 B8504000   push    004050B8
0040113A  |.  68 FC1D4000   push    00401DFC                         ;  SE 处理程序安装
0040113F  |.  64:A1 0000000>mov     eax, dword ptr fs:[0]
00401145  |.  50            push    eax
00401146  |.  64:8925 00000>mov     dword ptr fs:[0], esp
0040114D  |.  83EC 58       sub     esp, 58
00401150  |.  53            push    ebx
00401151  |.  56            push    esi
00401152  |.  57            push    edi
00401153  |.  8965 E8       mov     dword ptr [ebp-18], esp
00401156  |.  FF15 28504000 call    dword ptr [405028]               ;  KERNEL32.GetVersion


4.3 根据栈平衡原理寻找OEP

方法一 :壳程序在运行前,会将程序的寄存器的值利用push指令压入栈内,那么我可以对压入的栈地址设置硬件访问断点,当访问这个地址时,说明壳程序正在恢复寄存器的值,也就是说即将跳转到OEP处。

首先运行壳程序的push指令,使得寄存器现场压入栈内。

00413000 >  60              pushad
00413001    E8 C2000000     call    004130C8
00413006    2E:3001         xor     byte ptr cs:[ecx], al
00413009    0000            add     byte ptr [eax], al
0041300B    0000            add     byte ptr [eax], al

此时栈内的信息如下

0019FF64   00413000  ASCII "`杪"
0019FF68   00413000  ASCII "`杪"
0019FF6C   0019FF94
0019FF70   0019FF84
0019FF74   003A9000
0019FF78   00413000  ASCII "`杪"
0019FF7C   00413000  ASCII "`杪"

对0x19ff64设置硬件访问断点


然后F9

0002027C    0185 84020000   add     dword ptr [ebp+284], eax
00020282    61              popad
00020283    68 30114000     push    401130
00020288    C3              retn

程序在执行完popad后停下,可以看到已经来到了OEP附近。


方法二 : 我们已经知道壳程序运行前后的esp地址是不变的,而且正常程序的第一条指令通常是push ebp, 那么我们可以在壳程序刚开始时记录stack地址,然后对于stack-4地址处设置硬件写入断点,这样程序运行到 中断时,便会在OEP处停下。

程序刚开始运行时的stack地址如下

0019FF84   747762C4  返回到 KERNEL32.747762C4
0019FF88   00205000
0019FF8C   747762A0  KERNEL32.BaseThreadInitThunk
0019FF90   F648F892
0019FF94  /0019FFDC

我们队0x19ff80地址设置硬件写入断点。


然后F9程序便会停在oep处。

00401130  /.  55            push    ebp
00401131  |.  8BEC          mov     ebp, esp
00401133  |.  6A FF         push    -1
00401135  |.  68 B8504000   push    004050B8
0040113A  |.  68 FC1D4000   push    00401DFC                         ;  SE 处理程序安装

程序直接停在了 mov ebp , esp处。


4.4 根据编译语言特点寻找OEP

各类语言编译的文件入口点都有自己的特点。

比如在VC6的启动部分有GetCommandLineA(W)、GetVersion函数等。

我们对GetVersion函数设置断点,中断两次后,就可以回到OEP附近。

4.5 最后一次异常法

程序在解密或者解压缩时,会产生许多次异常,那么最后一次异常的后面往往不远处就会跳转到OEP。 

5 . 抓取内存映像

也称dump,指的是把指定内存地址的映像转存起来。

工具 : LordPE , 将内存中的数据与磁盘中的文件PE头链接起来。

基本用法 :

图中红框框出来的部分要勾选,表示链接磁盘文件中的文件头


然后准备dump的时候 , 选择dump full选项。


针对一些Anti-dump技术:

纠正SizeOfImage

我们可以使用LordPE的corrct ImageSize功能,其功能室直接在PE文件头中的SizeOfImage来实现的。


修改内存属性

某些Anti-dump技术对于文件的PE文件头等做了权限限制,比如不允许访问之类的,这样LordPE便不能正常dump程序,那么我们就可以利用OD加载程序,然后切换到Alt + M窗口, 对于没有指定权限区段赋予指定权限。


然后再利用LordPE正常进行dump即可。

6 . 重建输入表

一般做法是指,跟踪加壳程序对IAT的处理过程,修改相关指令,阻止外壳加密API,获得未加密的IAT。

6.1 确定IAT的地址以及大小

随便查看一个call 函数 , 查看[00405020] = 0x7477cfc0 , 相当于elf文件的got表。

然后我们查看数据栏,搜索0x405020 , IAT的结尾处是一个DOWRD的\x00。

所以我们确定IAT的大小是0xB8。


6.2 根据IAT重建输入表(手动)

有点繁琐,没有动手去试,但是原理还是要闹清楚的吗,建议大家去看一下相关的资料,但是要注意的是 , 最好不要把改变FirstThunk指向的IAT的地址。


6.3 用ImportREC重建输入表

前提条件: 目标程序已经被dump,目标程序正在运行,已知OEP或者IAT的偏移量以及大小。

首先我们让程序运行到OEP,然后dump内存映像,记得要修正imageSize。

然后用ImportREC打开程序,输入OEP使得程序自动获取IAT。

然后选择我们dump出来进程进行修复。


6.4 处理不连续的IAT

有些程序的IAT有可能被断成几份,不连续。

例子 : TestWin.exe

查壳

这一个没壳,直接不用脱,直接dump就可以直接用,也没有dump的必要。就是介绍一下不连续的IAT怎么办。

查找IAT的起始地址与size

因为有多个不连续IAT,这里只能找到最开始的IAT,最好的办法就是将size改的足够大,然后让Im.portREC自动分析。

然后点击show invalid , 分析无效信息 , 在无效信息处点击右键cut chunk ,得到正确的IAT表信息,然后选择dump进行fix。


6.5 修复函数

有时会出现函数名称识别出错的问题,利用ImportICE可以修改。




声明:该文观点仅代表作者本人,转载请注明来自看雪