0%

加壳知识

脱壳存根

​ 脱壳存根是一个类似于一个exe加载器,如果静态分析某个加壳的exe,实际上分析的是脱壳存根。主要执行如下:

  • 将原始程序脱壳到内存
  • 解析原始可执行文件的所有导入函数
  • 将可执行程序转移到原始的程序入口点(EOP)

解析函数导入表

  • 导入Loadlibrary和GetProcAddress

  • 保持原始导入表的完整,让windows加载器加载所有dll以及导入函数

  • 为原始导入表种的每个dll保留一个导入函数

  • 不导入任何函数,在不用函数的前提下,自己从库中查找所有需要的函数或者首先找到LoadLibrary和GetProcAddress,然后定位其他的库

    第二种和第三种静态分析都会很容易看到导入库。

尾部跳转

​ 完成脱壳后必须使用尾部跳转指令跳转到OEP执行,jmp是最简单的指令,所以有些使用ret或者call来隐藏。有时还会使用操作系统转移控制的函数来掩盖(譬如NtContinue、ZwContinue)。

脱壳过程

image-20211129202544768

image-20211129202522905

脱壳分类

自动脱壳

​ 自动静态脱壳解压缩或者解密一个可执行文件,但是仅仅只针对某个壳。PE Explorer提供了一些默认安装的静态脱壳插件,支持NSPack、UPack、UPX。

​ 自动动态脱壳程序运行可执行文件,并让脱壳存根脱出原始的可执行文件。自动的脱壳程序必须确定脱壳存根的结束位置,原始文件的开始位置。但是目前并没有非常好用的公开的自动动态脱壳程序。

手动脱壳

  • 找到加壳算法,然后编写程序逆向运行它。

  • 运行脱壳程序,让脱壳存根帮你工作。(让它从内存中转储出进程,然后再手动修正PE头部)

    用UPX的壳尝试一下OllyDbg脱壳
    重构导入表

    ​ 当OllyDump重构导入表失败时,可以使用导入重构器(ImpRec)。

    操作系统如何填充IAT:

    ​ 1:定位导入表

    ​ 2:解析第一个IID(IMAGE_IMPORT_DESCRIPTOR)项,根据IID中的第4个字段定位DLL的名称

    ​ 3:根据IID项的第5个字段DLL对应的IAT项的起始地址

    ​ 4:根据IAT中的指针定位到相应API函数名称字符串

    ​ 5:通过GetProcAddress获取API函数的地址并填充到IAT中

    ​ 6:当定位到的IAT项为零的时候表示该DLL的API函数地址获取完毕了,接着继续解析第二个IID,重复上面的步骤。

    1)定位导入表

    img

    2)定位到导入表的起始地址

    img

    3)根据第一个IID项中的第四个字段得到DLL名称字符串的指针,这里指向的是USER32.DLL

    img

    根据第五个字段的内容定位到IAT项的起始地址,这里是403184,我们定位到该地址处。

    img

    这里我们可以看到已经被填充了正确的API函数的入口地址,跟我们dump出来的结果一样,我们再来看看相应的可执行文件偏移处的内容是什么。

    img

    这里我们可以看到第一个API函数的名称位于4032CC地址处,我们定位到该地址处。

    img

    第一个API函数是KillTimer,我们在OD中看到的KillTimer的入口地址是操作系统调用GetProcAddress获取到的。

    img

    这里我们可以看到KillTimer的入口地址为77D18C42。该地址将被填充到IAT相应单元中去覆盖原来的值。

    img

    这里是IAT中的第一元素。

    我们再来看下一个元素,向后偏移4就是,来看一看该API函数名称字符串的指针是多少。

    img

    定位到可执行文件的相应偏移处:

    img

    32D8即4032D8,来看看该API函数的名称是什么,这里由于该指针不为零,说明该API函数还是位于USER32.DLL中的。

    img

    这里我们可以看到第二个API函数是GetSystemMetrics,通过该函数名称可以通过GetProcAddress获取到其函数地址然后填充到IAT中。接下来按照以上步骤依次获取USER32.DLL中的其他的函数地址,直到遇到的IAT项为零为止。我们来看一看可执行文件中结束项位于哪里。

    img

    我们可以看到当IAT中元素为零的时候表明USER32.DLL就搜索完毕了,

    手动修复导入表

    ​ 前面的工具是搜索内存中的导入函数来重构导入表,但是也有失败的时候。先了解一下导入表如何工作的。导入表在内存中实际有两个表,一个是函数名称或者序号列表,其中包含记载器或脱壳存根需要的函数名称或者序号;第二个表是所有导入函数的地址列表。实际上运行的时候只需要第二个,所以加壳程序可以通过移除名字列表阻止分析。这时候就需要手动重构。

    ​ 在反汇编中遇到导入函数时,逐个进行修复是最简单的策略,比如下图为例,IDA中DWORD处的值明显位于加载程序的地址范围之外,使用OD打开文件,将光标移动到该处查看内容,OD标注为WriteFile,所以我们可以将其标注。

    ​ 另外一种方法是需要运行脱壳程序,如果发现了导入函数表就看可手动重构原始的导入表,也可以写个脚本来帮助输入这些信息,缺点是复杂又费时。

    image-20211201103634458

    查找OEP

    ​ 自动工具查找OEP,最常使用的是OllyDump。通常脱壳存根与可执行文件不在一个节中,使用step-over或者step-into的方法,当程序跳转到另外一个节中运行时,OllyDbg可以探测到这种转移,并在那里进行中断。call函数就是用来调用其他节的代码,这种方法旨在防止OllyDbg错误标注这些调用OEP的曹祖破,step-over方法将跳过所有call指令。但是如果该call没有返回,那么OllyDbg就会定位到OEP。因此一些恶意代码经常包含一些没有返回的call,以此来干扰分析。

    ​ 所以实际情况要结合step-over和step-into方法。

    手动查找OEP

    1.最简单的就是查找尾部跳转指令(一串无效字节指令前的最后一条有效指令),填充这些字节的目的是为了保证节的字节对齐。

    image-20211130161428842

    ​ 图中非常明显的标志是:位于尾部但是链接到了一个很远的位置正常情况为几百个字节以内),并且一般情况下跳转指令之后会有一个返回,但是图中全是一些无意义代码

    ​ 在IDA中,如果是一个尾部跳转会标注为红色

    ​ 在脱壳存根开始运行时,尾部跳转的地址不包含有效指令,但是一旦被运行就肯定包含有效指令。

    2.在栈上设置读断点。反汇编中的大部分函数,包括脱壳存根都是以push开头,首先在栈中记录第一个入栈的内存地址,然后在这个栈位置设置一个读断点,只有在脱壳完成的情况下,才能获取原始push的堆栈地址。所以在pop指令获取那个地址的时候,就会命中断点。(通常尾部跳转紧跟pop

    ​ **3.**在代码中每个循环后面设置断点(通过扫描代码来识别循环,并且在每个循环之后设置断点)

    ​ **4.**在GetprocAddress函数设置断点。

    ​ **5.**在被原始程序调用且继续向后工作的函数上设置断点。因为壳通常相同,因此可以在它调用的一个函数上设置断点,来发现OEP。比如壳通常很早就调用了GetVersion以及GetCommandlineA函数,所以可以在这些函数调用时中断程序。于GUI程序,GetModuleHandleA通常是第一个被调用的函数。

    ​ **6.**使用OllyDbg的Run Trace。Run Trace提供一些额外的断点选项,这使得能在较大范围的内存地址上 设置断点。很多加壳程序都会留下原始文件的.text节,OEP总是位于原始文件的该节中,一般是这个节中第一个被调用的指令,Run Trace可以让你设置这样的一个断点,无论什么时候执行.text节中的指令,此段点都能被触发。

    常见的壳

    UPX

    ​ 这个壳开源免费并且易于使用,但是它不会对分析人员造成阻碍,所以很多恶意代码看似使用了UPX,但是实际上使用其他的壳或者修改过的UPX,这个时候可以根据前面描述的策略查找OEP,也可以通过OllyDump的Section Hop功能来查找OEP,或者直接仔细检查脱壳存根,直到找到尾部跳转为止,然后使用OllyDump转储文件,并重构导入表,成功脱壳。

    PECompact

    ​ 是一个商业的壳,它有一个插件框架,允许第三方工具集成进去,因此恶意程序进场使用该第三方工具,使得脱壳变得更困难。该加壳方法的脱壳与谦和相似,程序会产生几个异常,所以需要设置OD将异常传回程序(options-Debugging options-Exceptions)。可以通过查找尾部跳转失灵来查找OEP。

    ASPack

    ​ 使用了自我修改代码,让设置断点和分析它变得困难。在程序上设置断点,可以让程序立即终止,但是可以在栈地址行设置硬件断点完成手动脱壳,但是因为该壳很流行,所以网上有很多自动脱壳程序。

    ​ 手动脱壳时,首先打开脱壳存根的代码,在代码开始部分,会看到一个PUSHA指令。确定用来存在寄存器的栈地址,然后在这些栈地址设置硬件断点,调用POPAD指令时,就会触发,然后会在离尾部跳转不远的地方找到OEP。

    Petite

    ​ 使用了单步异常,但是可以像前面说的将异常传回程序,与ASPack相同,使用栈上的硬件断点来查找OEP是最佳策略。另外,它保持从原始导入表的每个库中至少导出一个函数,这在没有脱壳的情况下,很容易确定恶意程序使用了那些dll。

    WinUpack

    ​ 一个有着GUI终端的壳,设计目的在于优化压缩而不是安全。该壳有一个命令行版本叫做UPack,有专门针对该两种壳的自动脱壳器。

    ​ 识别尾部跳转有一个技巧,大多数脱壳存根都小于0x4000个字节,因此跳转的大小大于或者等于0x4000一般是跳转到OEP(针对书中例子),脱壳存根通常有很多条件跳转并且在函数中间返回,但是OEP周围的代码应该不会有这些不寻常的元素。

    ​ 另一种针对Upack的策略是在函数GetModuleHandleA(GUI)或者函数GetCommandlineA(命令行)上设置断点。在windows中,这些函数在调用Oep不久后就会被调用,一旦触发断点就向后搜索代码查找OEP。

    ​ WinUpack有时会使用一个OD不能正确解析的头部,使其崩溃,这时候需要首先使用WinDbg.

    Themida

    ​ 一个非常复杂的壳,包含阻止使用vmware、调试器,以及Procmon分析的功能。此外还有一个内核模块,运行在内核中的代码限制很少,而且分析程序通常运行在用户空间中,所以分析会受到很多限制。自动化工具成功与否与加壳版本有关。

    ​ 如果自动化工具无法脱壳,那么一种较好的方法是用procDump工具从内存中转储不在进行调试的进程。该工具的最大优点是在不停止进程或者调试进程的情况下,转储进程中的内存。这个过程并不能完全恢复可执行文件,但是能让我们在代码上运行strings工具并做一些分析。

    加壳DLL

    ​ 加壳Dll列出的开始地址是脱壳存根中的一个地址,位于DllMain中,而不是主函数中。DllMain函数在OC终端他运行之前被调用,中断发生的时候,脱壳存根已经运行,这将很难找到OEP。解决这个问题,我们可以打开PE文件,定位到IMAGE_FILE_HEADER节的特征标志域,将该节0x2000处的比特位从1修改为0,文件从dll变为exe.