好友
阅读权限40
听众
最后登录1970-1-1
|
L4Nce
发表于 2015-7-14 16:35
By L4Nce[C.L.G]
1. 引言
本文分享我在学习safengine这个系列壳中所遇到的问题以及解决方法,并结合我写的修复脚本讲解safengine这个系列壳中修复iat,资源等方面的技巧。希望本文章能够抛砖引玉同时也给正在学习这款壳脱壳的朋友一些参考。
(本文选用的被加壳文件为xp自带的记事本,加壳用的是最新的Safengine Shielden v2.3.6.0)
2. 初探
一般来说,我拿到一款壳,首先做的第一件事就是把程序跑起来,等解码完毕后,看看调用iat的代码变成了什么样。一般来说能搞清楚这部分代码所做的工作,能给修复iat带来极大的帮助。
首先来看加壳前后iat的变化情况。
图2.1
图2.2
很明显原本call dword ptr [iat]这类的调用方式,在加壳后被替换为call sedata_section+byteRandom的形式,也就是说直接的api调用已经变成了通过se的代码间接调用的方式。
既然已经如此就进入这个调用一看究竟。进入这个调用之后我们见到的就是各种夹杂花指令和被乱序的代码。但是我们经过单步分析之后会发现其中一些值得注意的地方:
首先我们会来到一个大范围的调用(也就是常说的远call)
图2.3
在010e58e9处明显有个大范围的调用,一般来说这种范围的调用不会作为乱序的转移指令,而是具有真正意义的功能调用。
图2.4
观察当前栈顶的数据,会发现此时该数据是一个指针指向了一个dll字符的名字。而这个call 0101f084进去之后调试下去就会发现,这个调用进入了一个虚拟机,虚拟机分析的话需要有效的辅助手段支持,毕竟机器只能用机器来对抗,人肉的话确实相当痛苦。现在这个call的具体功能无法分析,不过我们根据参数,该函数估计是用来获取模块基址的,暂时称他为SE_GetModuleHandle,其余的我们先不管,继续往下看。
图2.5
接下载没走几步会发现又有一个大范围的调用,观察当前栈中数据
图2.6
会发现有个数值为0x83800000(不同的机器应该这个值不一样,在SE的免费版中这个值的算法是固定的,通过跟踪数据流可以发现这个值是这样算的的,not(dllbase-1),我的系统当前的kernel32.dll的基址是0x7C800000,not(0x7C800000-1)即为0x83800000,当然这些是后话,我们现在假装我们不知道。),这个0x83800000值如果我们记得的话是之前有个push eax指令压入到栈中的,而eax的值是图2.3中那个调用返回的。同样这个call调用进去后里面也是进入了虚拟机。
我们执行步过这个call,观察eax的值,会发现eax是一个指针,指向了一块内存,查看这块内存的内容。
图2.7
在图2.7中,可以发现此处代码规整,而且看起来像是某个api的代码,很明显se shadow(不知道如何叫恰当)了一些api函数的代码。那么把这个函数称为SE_GetProcAddress算过分吧。
图2.8
接下来继续往下,会发现将此指针的值存到了某个特殊的地方。
图2.9
如图2.9所示,如果大家记性好的话会发现,010e58dc处的代码我们刚才单步的时候是执行过的。那么我们回过头看010e58dc处的代码
图2.10
此处的会将原本的push 0变成push addr的形式,然后接下来有个判断会影响后续流程,说明要是这个调用的地址已经获取了,那么就会走另一个流程了。
继续看后续流程
图2.11
如图2.11所示,很明显这部分代码取了shadow api部分的第一字节,然后用一个运算的方式间接的检测了第一字节是不是0xcc,以此来达到检测调试器的目的。
图2.12
之后就是修正返回地址(如若是直接返回,call之后有一个字节的随机值),然后就进入到该shadow api部分进行执行。
那么该流程主体如下:
首先检测当前调用的地址有没有被获取
若获取
进行直接的调用
若未获取
使用SE_GetModuleHandle获取基址,使用SE_GetProcAddress获得shadow后的api地址,修正代码,检测0xcc,修正返回值,调用。
其实说起来这个部分的流程很像是linux下懒绑定的过程,只不过懒绑定最终会将获取的地址存入got表中,而se中根本就没建立这种表,而是把获得的地址散落在代码数据里,给修复带来了更多的麻烦。
3.柳暗花明
那么我们现在所面临的问题就是如何获得该调用的真是目标api,也就是该shadow api的本体。
首先来看,SE_GetModuleHandle与SE_GetProcAddress的有虚拟机挡着,若直接和虚拟机硬肛正面,在没有插件辅助的情况是属于下下策,这些恶心的代码会很轻易就将你的耐心和信息消磨完毕,我们暂时不考虑。
其次,根据shadow api中所含的特征码,搜索内存找到真实的api,但是这个办法也是有问题的,首先自动提取特征是个麻烦的事,而且也有可能两个函数就有相同的特征,导致结果误判。所以这个办法实现起来也是有点困难的。
当我们遇到问题无法解决的时候,有时候回归最原始的本质,可能就能找到解决问题的办法。一般来说壳作者写壳的时候,为了提高脱壳的难度会将GetProcAddress这个函数重写,无论这个函数怎么写,有个地方是无法改变的,这个函数一定会访问dll模块中的导出表,根据导出表才能算出当前api函数的真实地址,这是之后各种变幻手法的基础。
有了这个想法以后,我们就可以在dll的导出表部分下内存访问断点,然后分析壳是怎么来获取api的,当然你也得祈祷这部分的代码没有被vm,如果实在是没办法也只能硬肛了。
首先来到被shadow的GetModuleHandle处下断(可以通过特征码找到)。
图3.1
在函数的返回处,断下后,看到已经获取到了dll的基址。
在函数的导出函数名部分下内存访问断点
图3.2
然后会发现,果然会有对导出表访问的代码
图3.3
该部分的代码开始遍历寻找需要获得函数名,部分有用的代码如下
[Asm] 纯文本查看 复制代码 movzx eax,byte ptr ds:[ebx]
movzx edi,cl
sub eax,edi ;cmp
je 010569F6
jnz 01056A0C
test cl,cl ;end of string
je 01056A0C
图3.4
之后,就开始进行获取真是api的操作了最后在
01062F94 03F8 add edi,eax
计算获得了当前api的真实地址。之后能,继续跟踪会发现,后续代码会判断是否需要对该api地址进行shadow的映射,
图3.5
如图所示,在免费的版本中是可以看到一些信息的
图3.6
那么,现在只需要将代码add eax,dword ptr ds:[ecx+0xC]进行nop操作即可阻断se建立shadow映射的行为。而且大部分的SE_GetProcAddress都会来同一个地方来获取真实的api地址(使用api hash在别的地方也是一个位置,少数api,CreateThread除外这几个需要具体定位一下)。于是乎我们已经有了办法知道每个调用的真实api是啥了。只需找到被替换的api调用,然后在
01062F94 03F8 add edi,eax 这个位置下断(找真实api的时机并不是在此处最好),即可在edi中获得真实的api地址了。
4. 日臻完善
到目前为止,我们已经能够获取真实的api地址了(非api hash选项),那么接下来需要修复的就是iat的调用类型了。
一般来说iat的调用类型分为三种:
1.call dword ptr [iat]
2.call @F
@@:
jmp dword ptr [iat]
3.mov reg,dword ptr [iat]
call reg
Se在加壳的时候把这三种调用类型都替换成为了call se_section这样的调用,起先我区分这三种调用时,使用脚本查找的特征码,当时se的版是2.2.6确实是可以的,后来随着se的更新特征码慢慢变得不再通用,这种办法慢慢失效了,特别是nooby在52 cm区放出的一个SN
(http://www.52pojie.cn/thread-235837-2-2.html),进入调用之后直接就是虚拟机。那么特征码就毫无作用了。
为了解决这个问题,在tuts4you的论坛上LCF-AT给了我解决的办法
(https://forum.tuts4you.com/topic/34639-unpackme-safengine-shielden-2260/)
首先来说,在我们获取真实api地址之后,就在该api的首指令下硬件执行断点(之前说了会检查0xcc),并在call se_section的返回处下断(randombyte后一句),接下来会有两种情况:
1,断在api首地址,此时判断[esp]的返回值,若是call se_section的返回值则是call dword ptr [iat]类型的调用,若不是则是jmp dword ptr [iat]
类型的的调用。
2,若是直接断在了返回处,则检查当前的寄存器是否出现了api地址(之前shadow已破),即可发现是到底是哪个寄存器在参与调用了。脚本代码如下
图4.1
至于资源修复的问题只需要,把散落在内存中的资源数据搬回来即可。相关脚本如下。
[Asm] 纯文本查看 复制代码 _FINDRESBASE_:
VAR DosHead
VAR NTHead
VAR DataTable
VAR ResBase
VAR MODULEBASEADDR
GMI eip,MODULEBASE
MOV MODULEBASEADDR,$RESULT
MOV DosHead,MODULEBASEADDR
MOV NTHead,[DosHead+3C] //PE HEAD
ADD NTHead,DosHead
ADD NTHead,78
MOV DataTable,NTHead
ADD DataTable,10
MOV ResBase,[DataTable]
ADD ResBase,DosHead
RET
_GETFIXRESADDR_:
VAR ResNumber
VAR StructBase
VAR ResPoint
VAR ResFix_Start
VAR ResFix_End
MOV StructBase,ResBase //一级目录
ADD StructBase,10
MOV ResNumber,[ResBase+0E],2 //获取数量
MOV Temp,[ResBase+0C],2
ADD ResNumber,Temp
MOV ResPoint,[StructBase+4]
AND ResPoint,7FFFFFFF
ADD ResPoint,ResBase //二级目录开始
MOV StructBase,ResPoint
ADD StructBase,10
MOV ResPoint,[StructBase+4]
AND ResPoint,7FFFFFFF
ADD ResPoint,ResBase //三级目录
MOV StructBase,ResPoint
ADD StructBase,10
MOV ResPoint,[StructBase+4]
ADD ResPoint,ResBase //定位到第一个指针处
MOV ResFix_Start,ResPoint
MOV StructBase,ResBase //一级目录
ADD StructBase,10
DEC ResNumber
MUL ResNumber,8
ADD StructBase,ResNumber
MOV ResPoint,[StructBase+4]
AND ResPoint,7FFFFFFF
ADD ResPoint,ResBase //二级目录开始
MOV StructBase,ResPoint
MOV ResNumber,[StructBase+0E],2 //获取数量
MOV Temp,[StructBase+0C],2
ADD ResNumber,Temp
ADD StructBase,10
DEC ResNumber
MUL ResNumber,8
ADD StructBase,ResNumber
MOV ResPoint,[StructBase+4]
AND ResPoint,7FFFFFFF
ADD ResPoint,ResBase //三级目录
MOV StructBase,ResPoint
MOV ResNumber,[StructBase+0E],2 //获取数量
MOV Temp,[StructBase+0C],2
ADD ResNumber,Temp
ADD StructBase,10
DEC ResNumber
MUL ResNumber,8
ADD StructBase,ResNumber
MOV ResPoint,[StructBase+4]
ADD ResPoint,ResBase //定位到第一个指针处
MOV ResFix_End,ResPoint
RET
_FIXRES_:
var Fix_Addr
var Souce_Addr
var Des_Addr
var Res_Size
ASK "Do you have a place to save fixed Res?IF you input 0,script will alloc"
CMP $RESULT,0
JNE HAVE_SPACE
ASK "input Res Size"
ALLOC $RESULT
HAVE_SPACE:
mov Des_Addr,$RESULT
mov Fix_Addr,ResFix_Start
FixResLoop:
mov Souce_Addr,[Fix_Addr]
add Souce_Addr,MODULEBASEADDR
mov Res_Size,[Fix_Addr+4]
MEMCPY Des_Addr,Souce_Addr,Res_Size
mov [Fix_Addr],Des_Addr
sub [Fix_Addr],MODULEBASEADDR
add Fix_Addr,10
add Des_Addr,Res_Size
inc Des_Addr
cmp Fix_Addr,ResFix_End
JBE FixResLoop
RET
5. 脚本构建
接下来我说说我的脚本构建思路:
图5.1
脚本分为如图5.1的几个部分,首先是寻找OEP去掉几个检测线程,然后获取一些必要的数据,patch一些数据,然后开始修复iat,最后还原eip指针到oep,修复资源。当然还有修复被替换的代码部分(这里不做过多的介绍了)
图5.2
找OEP是对VirtualProtect下断点观察其操作的目标,直到操作到代码段之后,在对代码段进行内存访问断点(有时这种方法会失效)。
图5.3
之后就是寻找代码断中的call se_sction,当然call se_sction不单是call api还有被替换的代码什么的,至于具体怎么分别我也没有好的办法(我是用的特征码)。然后就按我说的就行修复了。
6.其他
本文依旧有很多东西没有提及,计算api名字的算法,包括在选择散列api名的选项(其实也是一个点找起来也不麻烦),替换代码的修复,一些特殊的函数处理(需要熟悉度)等。还有就是SE的文件校验(其实可以找vadd4两个文件对照着直接爆掉即可,不过好像有个虚拟切换,找起来比较麻烦)。希望各位前辈能给出更好的办法。
7.参考文章
https://forum.tuts4you.com/topic/34639-unpackme-safengine-shielden-2260/
http://bbs.pediy.com/showthread.php?t=130066
http://www.52pojie.cn/thread-160458-1-2.html
http://www.52pojie.cn/thread-162305-2-1.html
由于排版问题大家直接看附件。
感谢@peace 大叔在平时对我的激励。
特别感谢@mycc 老师当初的那几篇帖子让我开始学习这个壳。
感谢clg所有成员。
感谢52破解让我学到了很多。
文档:
浅谈safengine系列脱壳文档.rar
(1008.72 KB, 下载次数: 1255)
试炼品:
试炼品.rar
(2.11 MB, 下载次数: 826)
视频:
http://pan.baidu.com/s/1eQ8OWgi 密码:wexk
注:视频里好像忘记修复资源了,用脚本修完之后,再用DT_FixRes处理一下资源就修复了
|
免费评分
-
查看全部评分
|