记两次相似的脱壳过程
最近碰到一个木马样本,喜欢加强壳,其中一个壳是经常见到但没去脱过的“Enigma”壳,由于需要分析木马的代码流程,就选了这个壳玩玩,然后发现其中隐藏导入表的手法和去年看雪秋季CTF最后一题“九重妖塔”(@不问年少)有异曲同工之妙,特此记录下脱壳的过程。
九重妖塔
先来看看九重妖塔的壳,样本MD5:b8b6bfe47a9c40117e2c6bbd5839f198
拖入IDA,马上你就会看到2000多个函数里没看到一处API调用,这很不科学,原因其实很简单,作者把导入表都隐藏了,你会看到导入表是空空的,连基本的kernel32都不给你留:
没有导入函数,作者怎么调用系统函数呢,很简单,在入口函数采用shellcode常用的方式自己获取API,先通过FS段寄存器指向的TEB结构获取kernel32的基址,进而搜索导出函数表获取GetProcAddress
的地址,然后再准备几个必要的API:
这个时候进程空间里还只有运行一些基础的模块:
下面就是该做windows加载器运行程序做的事情——加载运行库,把需要用的API填入IAT,这一步主要在函数sub_4b8690
里完成。过程我就先不跟了,直接看函数运行完后的效果:
可以看到多出了很多运行时需要的库映像模块,并且在接下去一小部分代码把IAT也填充完毕,然后就return前往原程序入口点函数开头了:
可以发现上面三个call很像API调用,地址0x4ADXXX
其实就是指向.idata节的IAT表区域,原本应该填充的是API地址,加完壳后却全部被抹0占位了:
但是,若在此时通过OD的dump工具将程序重建导入表dump出来后扔进IDA,依然还是无法看到导入函数调用,原因出在了作者没有直接把API函数的地址填进IAT,而是“间接”地填入IAT。什么意思呢?随便跟一个地址就知道了,比如就拿上面的地址0x4AD158
来说,发现此时存的数据是一个内存块分配地址0x1470000
,并且从内存映射窗口可以观察到其拥有可执行属性,跟进该地址可以看到写入一小段的shellcode代码,可以容易猜到该代码是为了定位实际要调用的API地址:
跟踪一下该shellcode1代码,内部一路的近跳,最后发现要return到附近的内存块分配地址0x1480000
:
该内存块又存储着另外一段相似的shellcode,只不过最终才是要定位到实际的API地址:
最终该shellcode2返回到实际要调用的kernel32.GetSystemTimeAsFileTime
地址执行:
于是就比较清晰了,壳代码将原有的每个API调用转换成通过对应的2个shellcode调用来实现,这样做有两个好处,一个是阻止dump出来的程序在IDA里识别出API调用,另一个则是在调试器里跟踪的时候也无法一眼看出实际调用的API,大大减少调试分析的效率。另外提一下比较有意思的现象,由于作者图方便每次分配shellcode用VirtualAlloc
直接分配一小块,导致内存布局呈现出大量连续的小内存块,如下:
OK,这个程序隐藏导入API的手法就描述到这里,下面说说对抗这种隐藏手法的解决办法。
每个人的对抗手法可以根据自己习惯不同,个人倾向于直接针对IAT重写做恢复,大概思路为:既然每2个shellcode都能对应到一个具体的API地址,那我想办法直接把最后定位到的API地址反过来填充回对应IAT表项就好了。
具体地,可以先跟下2个shellcode,发现每个shellcode在定位下一个跳转地址时都事先对某个数据进行一次异或运算,并且异或的key不固定。这暗含一个事实,这些跳转地址最终都是通过计算得出的,API地址也是,说明在分配shellcode之前就有事先获取过每个API地址并加密,而我们只需要找到那个加密的时机,就能直接拿到所有API地址,然后在其将shellcode地址填写到IAT表项时把API地址回写到对应位置即可。而这个加密的时机也很好找,其实就在前面没有具体跟进的函数sub_4b8690
里面,也可以直接在开始前对VirtualAlloc
下断再细跟,最终可在地址0x4b8965
跟到API加密的时机:
接着加密完的shellcode地址将先保存在一张表里,而这张表是在自己事先分配的内存,并不是在IAT,但是等这张表填完后壳代码会按顺序依次将这张表拷贝到IAT,所以其实这张表也可等同于IAT。那么,找到填写这张表的位置,直接把API地址填入该位置就行,回填表项的位置如下:
OK,万事俱备,只欠脚本,直接贴出本例回填API到IAT的OD脚本(v1.65)仅供参考:
var addr_api
var addr_table
bp 4447bd
loop_import:
bpgoto 4447bd, end_import
go 4b8965
mov addr_api, edi
go 4b899d
mov addr_table, ecx
mul addr_table, 4
add addr_table, eax
go 4b89a0
mov [addr_table], addr_api
jmp loop_import
end_import:
log "Import ok, now can dump."
运行完脚本后直接OD进行重建输入表dump,然后再次扔到IDA,导入表完美恢复:
此时OD也很方便跟踪了(换了个支持v1.65脚本的OD):
Enigma
接着看最近分析的木马,样本MD5:47e279bdce8508a37f7fec702ca2e4ca
Enigma是商业壳,其中包含多种反调试反分析的手段,然而本文我们只关心其隐藏IAT表的方式。
样本入口稍微跟一下,就会发现内存从资源节和输入表节中间的一个节数据解密出一个壳的核心库:
壳的各种对抗功能和商业验证功能应该就在其中实现,大概看一眼导出函数:
接着直接对地址0x401000
下硬件执行断点,可以把该地址简单认为是程序入口点,此时IAT已经填充完毕,位于data节里:
但是仔细观察一下填充的地址是一个内存块地址如0x0ef5d90
,该地址并不是一个API函数的地址,而是某个API函数(实际上是OpenServiceA
)的“复制”版本:
这就很有意思了,这样做的好处除了和“九重妖塔”一样具有隐藏程序API调用的作用外,另外使在调试时候对API函数的断点也失效了,比如原程序有调用CreateProcessA
来启动一个进程,我们想对该API下断点,但会发现徒劳,程序直接跑飞根本断不下来,原因正是壳代码把CreateProcessA
的函数体转移到了自己控制的内存去了,“所有”API调用都转到新函数地址,压根就不去调用原API地址。很显然,破解之法和“九重妖塔”异曲同工,只要找到调用的原始API地址往IAT对应的位置进行回填再进行输入表重建dump就行。在那之前,需要先分析一下这个API隐藏的过程。
曲折的跟踪过程就不细讲了,直接说说有效的方法吧。先在程序中随便找一处能够确定调用到的具体API,这步看经验吧,还是比较容易的,如下面明显是一处Sleep
调用,这样我们能确定两个信息,第一是壳代码一定会拷贝Sleep
这个API函数到自己分配的位置,第二是拷贝后的函数头地址会被写入IAT对应的位置0x40f084
:
那么我就先对Sleep
函数下硬件访问断点——hr Sleep
来确定壳拷贝API函数代码的大概位置,程序跑起来后在拷贝Sleep
函数的时候断下,可以看到取了函数头的一个字节先做检查,这里就是我们要找的位置:
具体检查的代码这里不细跟,简单提下壳代码在拷贝函数头的时候还会随机加入一些垃圾指令,目的应该是使新的函数地址整体上更“随机化”一点,增加调试难度。然后壳代码拷贝API函数的过程是把目标API一条指令一条指令地检查后再进行拷贝,碰到跳转或call地址的指令需要修复偏移量,有时候甚至还做一些指令精简,可以猜测其内置了一个反汇编器。但我们可以不关心这个,直接在IAT对应的位置0x40f084
下个硬件写入断点——hw 40f084
,看看什么时候把完成拷贝后新的Sleep
函数地址进行回写:
接着往后跟会发现,此时回写IAT,实际上是在该导入模块所有的API都已经拷贝完后才进行,这意味着拷贝API的过程中会先把新API函数的地址保存在某个位置,最后再回填,这样想要跟踪原始API地址与对应IAT位置的关系就会更难,需要再往前跟。******说来也巧,本来我往前跟踪壳代码拷贝某个函数的过程,想要在壳代码拷贝完API最后一条指令的位置附近找临界点,却意外因为对目标内存块(0x00eb0000
,即分配用来存储新API函数代码的内存)下了内存访问断点,发现新函数拷贝完成后存储的位置恰好处于同一块内存,并且原位置存储的数据就是我们需要的原始API:
壳代码就是从该位置(表索引定位,本例目标位置为0x00eec2e4
,用来存储Sleep
函数地址)获取原始API地址并进行拷贝任务,拷贝完成后进行回填,最后等同一导入模块需要的API函数都拷贝完毕后再统一把新API地址回填到IAT。可以对0x00eec2e4
下硬件访问断点对此进行验证,将会断在上面回写IAT的代码前面一点。
意外找到这个关键位置后,要过这个API隐藏就很容易了,道理同前,取原API地址回写目标地址即可,先贴这部分代码:
var num
bphws 401000, "x"
loop_import:
bpgoto 401000, end_import
go 4d97ea
mov num, eax
mul num, 4
add num, 4
add num, edx
mov ecx, [num]
jmp loop_import
end_import:
log "import ok"
此时进行dump后喜忧参半,IAT确实恢复了一些API,但仔细看发现有大半的API仍旧没有识别。仔细查看IAT表位置后才发现,原来壳还留了一手,某些API采用的隐藏手法不是上述的拷贝API函数,以下IAT位置存储的地址明显不像API地址:
随便定位过去一个查看代码,发现是一些处理过的代码:
但是最终是调用到了API函数kernel32.GetProcAddress
(可以先挂起其他线程,然后对kernel32.dll
的代码段下内存断点):
发现这类对抗是事先就准备好的对某些重要API的另一种保护,于是针对这种保护采取类似的破解方式,原理就是遍历IAT表检查每个地址,不像是API地址的就手动改EIP跑一遍代码定位对应的API回填(事先挂起其他线程),下面是第二步的OD脚本:
var offset
var addr
var pointer
mov offset, 0
loop_check:
mov addr, 40f000
add addr, offset
mov pointer, [addr]
cmp pointer, 70000000
ja next
cmp pointer, 0
jz next
mov eip, pointer
bprm 7c801000, 84000
run
mov [addr], eip
bpmc
next:
add offset, 4
cmp offset, 150
jb loop_check
log "again ok"
mov eip, 401000
最后恢复的效果如下:
结语
这两个程序隐藏导入表的手法各有特色,但本质上是非常相似的,都是控制了IAT表,自己定位需要的API再进行调用,故破解原理也是比较接近——寻找原始调用API地址回填IAT。攻防有道,针对不同的目的可以有不同的应对之法,善于抓取问题的本质进行分析才是关键。
目前没有反馈
表单载入中...