国外网站大全·环游世界互联网

当前位置:首页 > 汉化宝典 > >

内容详情

收录时间:2010-06-07

标题

脱壳经验谈之基础知识进阶

已阅0
投稿须知

网站介绍

egouz提示:查看第一篇基础篇

关于UPX:

开篇还是说点儿轻松的吧~ UPX是个非常简单但是又非常强大的压缩壳,很多脱壳的基础教程上都是先拿这个壳开刀。的确,这个壳脱起来确实不难,我这里想说的也不是脱壳的问题。先来看个实例:exeinfope 0.0.1.9B版,查一下壳,是UPX的。正常的手续是直接OD载入,然后代码向下翻,找到跳到OEP的跳转就可以了。大家可以尝试用这种方法脱下壳。从内存抓的文件居然有65M之大。我第一次弄的时候也被吓了一跳。为什么会这样呢?难道我脱错了么? 先别慌,我们来看看程序的区段信息。
\
既然程序是从内存中抓出来的,那么我们这里关心的就是虚拟地址和虚拟大小了。看下程序的VSize着实会让人吓一跳,有0412E000这么大。如果没有什么概念的话,我们进行一下简单的计算:

0412E000H=68345856D,单位是字节

68345856/1024/1024=65.1796875MB 这个就是我们从内存中抓的文件的大小。

那么文件为什么那么大的原因也找到了:是因为加壳程序把程序的虚拟大小设成非常大,从而导致前面的现象。

OK,下面总结一下从这个实例中学到了什么呢?

首先,我对虚拟地址和虚拟大小这两个概念的理解加深了。所谓虚拟地址就是程序映射到虚拟内存中相对基址的偏移。而虚拟大小就是该区段所占的虚拟内存大小,与文件实际尺寸无关。

其次,我有了一个经验:对于UPX这样的壳,最好的办法还是使用UPX自带的-d命令来脱壳的好。因为UPX是目前极少数支持自身脱壳并能完全还原加壳前程序状态的壳。对于破解来说,程序是否能还原成原状态没有什么关系,只要代码能还原即可。但是这对软件的汉化来说,使用-d命令是很重要的,因为汉化更多的是考虑资源,而资源段同其他段的相对位置关系往往会影响汉化的成败。

关于API:

好了,上面说了些轻松的话题,现在开始进入比较技术的领域。

API,英文全称是Application Programming Interface,直译过来称为应用程序编程接口,是Windows提供的调用系统功能的接口。所有运行在Windows平台上的程序,都要或多或少的调用这些API,壳也不例外。

一个加壳程序,会对程序的代码,数据,资源,输入表等进行处理,然后再把程序入口点指向壳的代码,并重新为壳自身建立一个输入表---这一点是很重要的。随便打开加过壳的程序,我们可以看到程序输入表中调用的函数少的可怜,但是,对比过几个程序后,我们会发现一些规律,即这些壳一般都会引用某些特定的API,这些就是这里要讲的-----几个同壳密切相关的API函数。

1、GetModuleHandle.先了解一下这个函数的意思:

Get 获取:

Module 模块,当PE文件被映射到内存中后,我们就把它叫做一个模块。

Handle 句柄,是一个数值,对于PE可执行文件而言,句柄的意义就是文件被载入(映射)到内存时的基地址的。EXE文件一般是00400000,而DLL文件的句柄是变化的。

这样这个函数的意义就明了了,即获取一个可执行模块的句柄,即可执行模块的载入基地址。

2、LoadLibraryA 这个函数的意思是显而易见的,就是载入一个库文件,可以是DLL文件或者其他类型的PE文件。入栈参数即使该文件的完整名称。

3、GetProcAddress 这个函数的全称是 Get Procedure Address, 意思是获取(子)程序地址,也就是用来获取DLL文件输出函数的真实物理地址,由于DLL文件映射到内存中的基地址是变化的,所以这个函数显得格外重要。

4、VirtualAlloc 全名 Virtual Allocate,意思是分配虚拟内存,即向系统申请一块虚拟内存。

函数原型如下:

LPVOID VirtualAlloc
(LPVOID lpAddress, // 请求或保存区域地址
SIZE_T dwSize, // 区域大小
DWORD flAllocationType, // 分配类型
DWORD flProtect // 访问保护类型);

5、VirtualFree 这个函数是用来是释放申请的虚拟内存的,函数原型如下:

BOOL VirtualFree
(LPVOID lpAddress, // 区域地址
SIZE_T dwSize, // 区域大小
DWORD dwFreeType // 类型);

6、VirtualProtect 这个函数是用来修改虚拟内存的属性的,函数原型如下:

BOOL VirtualProtect(
    PVOID pvAddress,
    DWORD dwSize,
    DWORD flNewProtect,
    PDWORD pflOldProtect
    );
   是不是有点儿晕呢?没关系,先放一下,了解一下别的东西后再回过头来看这些,因为下面的内容需要这些函数的知识,等介绍后面的知识之后再进一步讨论。

关于输入表:

   要想把脱壳学好,不了解PE文件的格式是不可能。而输入表又是加密壳处理的重点关注对象,所以对于输入表结构了解是必不可少的。但是,这里不打算详细介绍输入表的结构,而是想深入挖掘和脱壳有关系的信息。

   输入表结构中有两个重要的结构,一个是输入名称表(Import Name Table, INT),另一个是输入地址表(Import Address Table, IAT)(是不是看这个名字很熟呢?),分别由IID表中的OriginThunk和Thunk项指向。本质上这两个数据结构是一样的,而且在没有加壳的文件中,这个两个结构中数据是一模一样的,都保存了导入函数的RVA值。但是为什么要用两个相同的结构来表示呢?这要从PE文件的加载过程说起。

   PE文件中的输入表中仅保存导入函数的名称(或编号)以及所在的DLL文件。但是程序在执行的时候是需要这些函数在本系统的真实地址,而这个工程就是由PE加载器根据输入表来完成的。

PE加载器通过PE头部的结构找到输入表的位置,并读取IID结构中的OriginThunk项,并根据OriginThunkINT找到迭代搜索数组中的每个函数名称指针,并进一步找到引入函数的名称,然后使用LoadLibrary载入函数所在DLL文件,通过GetProcAddress函数获取这些函数的真实地址,并把这些地址填写到IAT结构中。此后程序运行只要依靠这张的IAT表就可以运行了。 换句话说,IAT表是要被PE加载器修改填写函数地址的,理解这一点对于我们后面要讨论的内容很重要。

   填写完成后的IAT就是我们脱壳时通过ImportREC获取的IAT了。

回想一下,我们在脱壳的时候要找到一张完整的,没有加密的IAT。然后用ImportREC来抓取,重新构造一份一份输入表。重建输入表的过程其实就是PE加载器填写IAT的逆过程:通过IAT的每个真实物理地址逆推出函数名称,并用这些物理地址所在的RVA(下图,红线区域)构建INT,再把函数名(下图,蓝线区域)填写到相应的RVA。这样就重建了输入表。
\
值得一提的是,IID结构中是没有项指出INT中的引入函数个数的,INT结构中的最后一个项是NULL,也就是一个Dword的0。由于IAT和INT是一样的,这也就解释了为什么IAT中每两个DLL文件中函数的地址是用Dword的0隔开的。这个可以作为区别无效指针的一个方法。

   这部分的内容有些难懂,希望大家能多读几遍。慢慢体会,了解输入表的结构可以帮助理解。

关于壳的行为:

   有了上面的基础,我们就可以来讨论一下壳的行为了。简单的理解,壳是加在可执行程序外面,先于可执行文件运行的一段代码。除了一些anti以外,壳一般处理代码和输入表,但不管怎么处理,最终的结果都是要让程序能够正常运行(废话!)。

那么,根据上面提到的,程序正常运行的条件是解密的代码,以及填写好的IAT,只要这两个就够了。但是由于程序是壳解密的,IAT的填写的重担也就自然落到壳的身上。壳在把运行权交给程序之前一定要填写好IAT供程序使用。这就是为什么说上面提到的LoadLibrary,GetProcAddress是重要的API函数了。

    至于GetModuleHandle.函数在壳运行过程的重要性也是很容易理解的。壳在解码代码和数据过程中,光靠它自身输入表中的函数是绝对不够的,壳需要动态加载一些dll文件并获得相应的函数地址。当填写IAT时,有些dll其实已经加载过了,这个时候就需要GetModuleHandle.获取这些dll的句柄,再通过GetProcAddress获得函数地址。

 VirtualAlloc,VirtualFree和VirtualProtect这三个函数在壳运行过程中有两个用途:

    一是申请虚拟内存放代码或IAT。有些壳会把解密代码的一部分放在自己申请的内存地址中,这样即使脱壳了,由于缺少代码也不能运行,像穿山甲的Code Splicing功能以及ASPR中让人抓狂的补区段都是要通过这些函数来完成的。VirtualProtect是用来修改内存属性的,作用是修改区段属性,然后向内存中写入解码后的数据;某些壳HookAPI的时候也会使用这个函数来修改系统dll的属性,以便修改系统dll,达到Hook的目的。

   第二个就是同花指令的处理有关系。不过说实话,这个我也没太搞明白。以后再补充。

关于Break:

   先休息下,回想一下上面的内容。由于上面提到的那些函数在几乎所有的壳里面都会用得到,所以这些函数被频繁的用在了脱壳上。有的时候是为定位代码,有的则是根据函数的意义来下的断点。

关于输入表和IAT的加密:

   休息完回来接着看。了解上面的输入表和IAT之间的关系,想来大家应该对下面的讲解不会感到糊涂了!对输入表和IAT进行加密现在已经成为所有加密壳的必须要做的事情了。因为这样,就涌现了一大堆的新名词,搞的人头昏脑胀的。

   一个典型的代表就是Magic Jump了。这是什么意思呢?一般壳中都有一段代码来判断当前函数是否要加密。代码中的跳转就被称为Magic Jump,因为只要让它跳了,IAT就不加密了。

   继续进行之前,先弄清楚一个问题:壳是怎么处理输入表的?一般来说,壳对输入表的处理有以下几种。

   1、完全保留输入表,加载时没有对IAT进行加密。注意,这里的说法,前面是输入表,后面是IAT。即,壳在调到OEP之前,通过读取原程序的输入表结构,模拟PE加载器来填充IAT表。一般来说,壳在填充完IAT以后就会清除原程序的输入表。这种情况下的修复比较简单,可以不用ImportREC重建输入表,只要跳过清除输入表的代码,适时抓出内存镜像,然后找到输入表的位置,修正OEP就OK了~ 记得黑鹰的教程中貌似有个不用ImportREC来脱ACProtect的,大家可以参考下。UnpackCN的一篇,不用ImportREC脱ASPack的文章就是使用的这样的原理。

   2、完全保留输入表,加载时对IAT进行部分加密。这种情况有两种方法修复,一种是像上面提到的,找到输入表,然后修改输入表目录就可以了;另一种方法就是找到Magic Jump跳过IAT加密,然后用ImportREC抓出IAT,重建一个输入表。如果跳过IAT加密的方式不可行的话,可以试试找找输入表的位置。

   3、完全擦除输入表,并更改输入表的保存方式。这种情况,由于PE文件的输入表结构被转换了保存形式,只有壳的加载器才能识别,所以这个时候只能通过一份未加密的IAT,利用ImportREC重建了。

   4、上面三种形式都没有多IAT表的位置进行处理,而第四种方式就是修改所有输入表调用,填写IAT的时候把IAT结构放在壳自己申请的虚拟内存中。这种处理方式在穿山甲中体现的比较明显,穿山甲中的Import Elimnation就是这样的保护方式,相信脱过穿山甲的朋友应该有感受哦。这种方式首先要把IAT结构移动回程序内部才能Dump。

   上面的问题清楚了,就要进一步了解另一个概念:什么是IAT加密?是不是觉得有点诡异呢? 但是,问下你自己,你是不是真的知道什么叫IAT加密? 壳在加密的时候是怎么处理的?

  加壳程序要运行,在填充完IAT以后,输入表就没有用了。对于API的调用完全是依赖于IAT了。那么从逻辑上讲,一个IAT表无论进行怎么样的变形最后一定要跟不变形的时候达到同样的效果。因为IAT中保存的是API函数的地址,那么进一步的推论就是,无论怎么加密、变形,最后一定要执行到这个API的代码中或者跟其相同功能的代码之中。有了上面的逻辑基础,下面就介绍一下IAT的加密。

  IAT的加密分为主要分为四种方式:

     1、Hook型。这种方式就是就是把API函数的地址替换成其他的地址,并在这些地址中加入一些连接代码,最后转到API函数执行。相当于hook了这个API函数 然后无法知道是什么函数,也就无法从IAT重建输入表了。多数加密壳都是这种方式的。不过为了加强加密强度,这些连接代码往往有很多垃圾代码和无用的API调用,来干扰真实API的还原。一般来说,壳不会对所有的API地址进行处理,总有一些API函数是没有处理的。这样也就存在了一个判断是否加密的跳转,这个跳转就是我们常说的Magic Jump。

     2、模拟型。这种方式说起来其实很简单,就是模拟API的功能,然后把这段功能代码所在的地址填充到IAT中相应的位置。这种方式比较麻烦,因为最后没有跳转到真正的API地址,这样不太容易分辨出到底是哪个函数。好在现在使用这种方式的并不多,而且即便使用了模拟API的加密,也只是针对一些比较特殊、敏感的函数。最极端的例子就是ZProtect的Anti-Hook功能了。这个功能把程序使用的dll文件的代码全部复制到申请的内存中去,然后把这是的API地址全部替换成这块内存中代码的地址。不过修复起来还是很容易的。

     3、搬运型。这种加密方式是把IAT表从程序领空移走,并擦除原始IAT。对IAT的调用我完全指向壳申请的一段内存。一个极端的例子的就是PeSpin 的APIRedirection功能。意思是API的重定位。这个壳对于IAT的处理有别于其他壳的是:它把需要加密的IAT搬走,而不需要加密的IAT则不进行处理。这样对于IAT的处理就形成的两个流程。而不是一个简单的Magic Jump就可以完成的。

低版本的可以通过修改跳转流程来处理,但是对于高版本这样的方法就不适用了,要完整分析出对IAT处理的过程才可以修复。这个壳的另一个特点就是它对加密的IAT表进行了不规则处理:平时我们遇到的IAT表都是API地址都是挨着的,中间没有空隙(就算有也是4个字节,用来间隔dll的)。

但是Pespin在处理的时候虽然把加密的API地址放在了一起,但是API地址直接是用随机的1-2个00隔开的,这样,即使把加密的IAT完全解密,ImportREC识别也是个问题,这就需要一些技巧。

     4、擦除型。这里面“擦除”的意思是指把IAT表完全擦除,而且不把API集中放置,并修改对IAT调用代码,改成普通的jmp和call,这样修复起来就很麻烦,需要用户自己识别IAT调用代码,或者在壳代码中patch掉这样处理的代码。但是如果,加壳程序在加壳之前就进行了这样的处理的话,就需要到达OEP以后手动识别并处理了。这是一种很耗费精力的加密方式。目前NoobyProtect,VMP,Themida/Winlicense,ZProtect,Private Exe Protector中都有看到这样的保护方式。

关于花指令:

   作为干扰调试的方式,很多壳都使用了花指令。广义的来讲,花指令大致分为三种:

   1、数据型。这种是最传统的花指令形式,即在指令中间插入无意义的字节,不参与指令,但是可以干扰反编译,给静态分析带来很大障碍。因为这些字节的数据是没有意义的,也不是参与执行的指令,所以称之为数据型花指令。这种花被称之为junk,OD的去花插件也是针对这样的花指令进行去花的。

   2、废代码型。这种花指令指的是一组代码,执行这组代码的最终结果,在逻辑上为0.举个例子:xchg eax,ebx,xchg eax,ebx。这两条指令执行完以后,等于nop指令,因为什么都没做。实际的应用的中会有很多种变形。取个学术点儿的名字叫:“空逻辑展开”。使用代码膨胀引擎在代码中随机插入一些这样的花指令,可以增加分析壳流程的难度。

   3、代码变形型。从广义的角度来看,这样的指令可以成为花指令,因为它是对原始指令的变形,但是要达到同样的效果,也就是说这些代码是有实际意义的代码。举个例子:

   jmp指令 可以变形为:push xxxxxxxx,retn; 或者编程 jnz xxxxxxxx,je xxxxxxxx。这样两种代码都可以实现jmp指令的功能。 这样的代码或许现在看来没有什么,但是,当量多的时候着实让人头疼。

关于ESP定律:
   上面的东西,大家想必看的头晕了,说点儿简单的吧。ESP定律想必大家都听说过,而且很多人经常会用,只是问下你自己:你真的明白其中的道理了么?ESP定律归根结底就是一句话:堆栈平衡原理。我今天要说的不是ESP定律内容,而是其本质。考虑下面一段子程序:
\
\

在地址0047E99B处有一个call,调用子程序sub_00433487.调试过程序的人都知道,如果我们选择步过0047E99B这条指令的话,程序会执行完这个call以后停在0047E9A0这里;同理,如果选择步进这个call,走完整个call,出来以后也会停在这里。下面就要问两个问题:

1、为什么会停在这里?

2、为什么会返回到这里?

      好了 先说说call这个指令的作用:指令call指令的效果相当于
Push NextEip
Jmp xxxxx
即,这条指令会把下一条指令的地址先压入堆栈,然后跳转的子程序中执行。理解到这一点很重要。后面再说。执行完以后,esp-4.

然后我们来看这个子程序。熟悉编程的朋友应该都知道局部变量这个概念,就是只在子程序中使用的变量。那么局部变量在汇编中是怎么表现的呢? 是通过堆栈分配堆栈空间来实现局部变量的。
\
相信大家对Push ebp,mov ebp,esp这两个指令应该不陌生吧。这里就来说一下这两条指令的含义。Push ebp就是把ebp的值压入堆栈,配合子程序末尾的pop ebp 可以理解,这时为了保存当前ebp的值,在子程序结束以后恢复这个值。

Mov ebp,esp这条指令是把当前的esp指针给ebp。那么现在ebp就指向了堆栈顶端了,我们来看下,现在esp指针比call进来之前小了8,一个dword是下一条指令的地址,一个dword是保存ebp的值。下面的指令add esp,-34就是把当前的esp再减去0x34。

这样操作以后,新的ESP和旧的ESP之前就用了一段可以使用堆栈空间了。这就是为局部变量分配的空间。我们知道,局部变量仅在子函数中使用,当退出子函数以后,这些变量将被消除。体现在汇编代码上就是,我们可以找到一对对称的指令:add esp,-34和add esp,34这样的指令。就是清理内局部变量的指令。
\
同样,我们也可以找到与Push ebp相对应的pop ebp指令。执行完pop ebp以后,我们注意到这个时候的堆栈栈顶中的内容是之前call指令push进去的下一条指令的地址。执行完retn以后我们就到了这条指令上 而且堆栈指针会+4. 分析了这么多,我们知道了在经过一个call以后 堆栈指针是不变的,也就是说在一个正常的call里面,堆栈是平衡的。

        说了这么多,我们来说一下ESP定律。ESP定律的意义在于把壳代码的看做是一个call。那么我们知道在这个call执行之前和执行之后,堆栈环境应该是一样的。这就是ESP定律的本质所在。因为壳代码在处理程序代码以后并跳转到OEP的时候,要恢复堆栈及寄存器环境。那么也就是说壳代码要在代码执行之前保存这些数值。所以就有很多壳会在刚开始不久就使用pushad这样的指令了~ 所以呢,对于ESP定律的应用不要见红就用,要自己分析一下的!
        
关于PEiD:

   PEiD应该是最为常用的查壳工具了。由于其具有很好的数据库扩展性,可以很方便的侦测很多壳。这里想说点儿关于PEiD使用上的问题。现在很多壳都有具有很强的伪装性,而且实现入口点代码的随机性其实并不难,所以通过检测入口点的特征来判断壳的类型已经逐渐变得很不可靠了。

有些脱壳经验的朋友应该见过PEiD把VC++程序错误识别为Armadillo 1.x-2.x。原因很简单:因为Armadillo1.x-2.x版本的入口点跟VC++的入口是一模一样的。如果仅凭这个来判断的话,当让会出错了。而且,个人观点是,当你的水平达到一定程度的时候,其实可以完全摆脱PEiD等编译检测软件了,而且对于壳具体版本其实没有必要知道的那么详细了。

下面给出两种其他判断壳类型的方法:

   1、看区段特征。拿ZProtect为例,默认情况下,加出来的文件的区段特征是:第一个区段名为.textbbs,其他区段名称为text,idata,rsce,rdata。如果一个加壳文件是这样的特征,基本可以判断为ZProtect加壳的了。虽然现在ZProtect有了随机区段名功能,但是还是有方法判断:因为ZProtect对程序处理的方式是把原始文件的数据全部恢复到第一个区段去,这样的话,对于一个较大的程序来说,第一个区段的虚拟大小就会显得太别大。如果是这样特征的程序话,就可以考虑是ZProtect加壳的。而WL/TMD加壳的程序恰恰跟ZProtect相反,因为它会把虚拟机和相关数据都放在程序的最后一个区段,这样的话,对于一个小一些程序,最后一个区段的大小会显得很大(因为这个区段的大小是相对不变的。)同样,RlPack,NoobyProtect,VMProtect,Armadillo,TTProtect都有其区段的特征性。大家可以自己总结一下。

   2、看代码特征。这里提到的特征其实是种模糊的概念,因为这需要你自己有个感性的认识,例如:
\
这就是ZP的特征,它使用类似这样的代码变形实现跳转。而NoobyProtect
\
这就是ZP的特征,它使用类似这样的代码变形实现跳转。而NoobyProtect。
分享给小伙伴们:
站长头像赫赫无敌:探索互联网世界,收集和分享实用互联网资源,推荐国内和国外知名、实用、创新、科技、优质的站点资源!互联无极限,探索无止境;分享求真知,网络无国界!
更多>>

同类站点推荐

评论

关于我们|联系方式|版权声明|关于图片|友情链接|

分享互联网优秀资源-国外网站推荐

Copyright ◎ 2014 egouz.com, All Rights Reserved.| 目前收录国外网站 个!

国外网站大全 版权所有 冀ICP备11014106号-