对抗反汇编技术是让程序执行流程不变的前提下修改汇编语句,使反汇编引擎无法正确解析出汇编指令或真正的程序流,只有掌握了基本的对抗反汇编技术才能在分析恶意代码过程中不被带偏。
做过反汇编引擎项目的读者应该在开发时都会意识到这个问题:一行汇编指令是什么取决于他的首字节是什么
,也就是说一个字节会决定整条汇编指令的内容。根据这个特点,就引出了两种反汇编引擎:线性反汇编引擎 与 代码流反汇编引擎
顾名思义, 线性反汇编引擎在解析字节码时,会按顺序依次向下进行解析, 这也是最简单的反汇编引擎。 这种设计写法简单,无需关注汇编指令是否合法是否符合实际情况,这种写法被大多数硬编码课程采用。正是由于这种无脑向前解析的思路使得对抗反汇编变得非常容易,通过观察下面这个例子来具体感受下什么是对抗反汇编:
观察上面的指令, 我们发现了两个可疑点:
由于jmp是一个无条件跳转,跳转位置为loc2+1,所以loc2+0处的代码应该永远不会被执行(仅对于当前案例)
但是对于一个线性的反汇编引擎,它的写法不会考虑这条指令是否合法是否会被执行,在解析时它将永远将当前字节视为指令的一部分。即使这会影响到分析者的阅读。因为它的逻辑实在是太方便开发了。
那么对于上文的汇编指令,如果我们改成这个样子:
如果我们不将loc_2处的字节视为代码,而是一段数据,那么整段指令就变得十分正常了,一个对于Sleep函数的调用。
像上面这个案例,我们看到了jmp时就会去寻找目标位置,发现在下一条指令中间,于是我们将那条指令拆开,将其从第二个字节重新解析,就得到了正确的结果。
正是这种跟随代码流走向,有层次结构的分析思路,落实为代码就变成了面向代码流的反汇编引擎
面向代码流的写法相对于线性写法更加准确和智能,如下面这段代码只有面向代码流的反汇编引擎才能正确解析出来:
面向代码流的大致思路为遇到跳转类指令(JCC CALL)时,会对跳转目标地址做出标记,这样可以保证目标地址不会被吞入某条指令中,而是作为一条指令的开始。
但即使面向代码流的反汇编引擎已经足够强大,仍然可以通过人工设计字节码的方式来欺骗引擎,这要求分析师有扎实的汇编指令基础。具体欺骗方法在练习中会涉及到,此处不赘述。
所谓的混淆控制流听起来可能很高大上,总结为一句话就是绕远路
。如某一个指令的其中一个功能是跳到XX代码处,那么通过混淆控制流让跳到XX代码的过程变得复杂且混乱,但最终都会跳到XX代码处。这种思想在各个语言的混淆与加密中常作为核心思路出现。如果想要深入了解混淆控制流相关的技术手段,需要具备一定的编译原理知识,其中AST语法树是混淆控制流的重要概念。有兴趣的朋友可以去了解下。
本书中简单列举了几个简单的混淆控制流技术
简单地说,这种手段是将call xxxx改为mov A,xxxx call A的形式,这使反汇编引擎无法静态解析出A的目标地址,只有分析师们手动对A进行赋值的溯源。
如上这个例子,函数指针var_4在4011D5处被赋值,但对于var_4的两处调用IDA并未帮我们识别出var_4的内容,这时我们就需要观察var_4的来源。此处例子代码量小,若是大型样本,这里的var_4的赋值可能会很曲折。甚至多次多var_4的call调用可能为不同的函数地址。因此会加大分析师们分析的难度。
在学习汇编知识时,老师们会说,retn语句用于返回call调用的下一条语句。这会使部分人对其有个死印象。当恶意代码作者修改了返回指针,这些分析师可能会错过真正的代码流而被欺骗到另一个虚假的分支中。
在了解滥用返回指针前,需要知道返回指针的原理。当一个call指令被执行时,会将EIP修改为call的目标地址,call指令的下一条指令的地址会被压入堆栈。
那么当这个call指令内部执行完毕,准备执行retn时,cpu并不知道他要返回到call的下条语句,cpu只会搜索堆栈,找到存储返回地址的位置,将eip修改为返回地址。也就是说,cpu是非常笨的,他只会一味的按照堆栈内的数据去操作。 这时当我们通过代码修改了堆栈内的返回地址,CPU就会乖乖的将EIP修改为我们指定的值。鉴于这种特性,恶意代码作者们开始使用修改返回指针的方式隐藏真正的执行流程。
如图的代码,看起来好像只是一个简单的call调用,局部变量加5后就结束了。但是看起来又感觉不是很舒服。这是因为IDA无法识别混淆后的代码逻辑。
代码在call $+5后会将下一条指令的地址4011C5存入堆栈,此时ESP指向返回地址,接着执行add [esp+4+var_4],5
,var_4是局部变量,地址为esp-4,那么此条指令就变成了add [esp+4-4],5
,实际上是对esp内存储的数据加5,而esp内现在存储的正是返回地址4011C5,对其加5变成了4011CA
,接着一个retn,cpu将esp内的数据存储赋值给EIP寄存器。所以实际上这段代码是对4011CA处的调用。总结为:
这就是典型的绕远路,虽然代码看起来混乱,但最终目的始终不变,通过这种恶意操作返回指针的方式,达到了简单的隐藏代码流走向的目的。
既然是混淆控制流,那么只要保证目标地址不变的前提下,混淆方式可以说是层出不穷,只要脑洞打开就有新的混淆思路。本书仅简单列举了几个,其中一个是经典的操作SEH链。在学习混淆手段前,需要先了解下SEH技术。
windows为了维持程序的正常运行,提供了一个名为SEH的功能,SEH是一条单向链表,其中存储着函数指针,当异常发生时,系统会调用SEH节点的函数进行异常的处理,若当前节点未处理这次异常,则会继续调用下一个节点,直至到达链表尾部,这时这个异常被认为处理失败了,windows就会弹出错误框。
SEH链表每个节点的结构如下:
prev存储下个节点的地址,handler存储当前节点的异常处理函数地址。
而SEH整调链表的头部地址就存储在fs:[0]处,fs是个段寄存器,通过段选择子可以找到TEB结构,然后再找TIB结构。。。。。想要细致了解可以去学习内核知识。在这里我们只需要记住fs:[0]存的就是链表头部。
知道了SEH的背景,一个混淆控制流的思路就出来了:
先手动构造一个SEH节点结构,将其链到链表头部,然后我们再手动触发一个异常,除0或API抛异常都可以,这时windows就会调用链表第一个节点,也就是我们构造出来的节点。当我们把节点内的函数地址换成自己的函数,这样就达到了混淆控制流的方法。
如图,这段代码在栈中构造了一个SEH节点结构,其中push eax是节点中的handler,push fs:[0]是节点中的prev,也就是原链表的头部,再通过mov fs:[0],esp将我们构造好的SEH节点进行替换。到这里就已经完成了构造链表节点,链到头部,修改fs指向新头部的操作。然后通过xor ecx,ecx div ecx来触发除0异常让系统调用我们自己的函数。这就实现了简单的混淆控制流。
不管恶意代码作者如何混淆控制流或欺骗反汇编引擎,只要分析师有扎实的基础和耐心,就可以找到真正的代码流。了解简单的对抗手段可以在分析时快速判断出代码的真正意图。
乍一看,这个样本就是个遍历进程,线程,模块的小工具,貌似没有什么特别的,但是当我们细致观察,会发现可疑点:
程序首先计算了400000 or 148C,结果为40148C,然后将这个值赋值给[ebp+4],画过堆栈图的小伙伴,应该还记得[ebp-4]是局部变量,[ebp]存储原栈底,[ebp+4]为返回地址,[ebp+8]第一个参数,这里通过修改返回地址实现了上文的滥用返回指针,所以这个样本在遍历完进程后会调用40148C处的代码。我们跟过去看一看。
跟过来我们慢慢分析,先看第一段,这里的jmp地址明显不是正常操作。而上面的jz目标地址在jmp指令中间。我们在401496处按下U恢复为字节码,在401497(jz的目标地址)按下C将其变回代码,结果如下:
可以到代码恢复正常了,下面是上文提到过得SEH技术,实际目的为执行4014C0处代码,在继续分析前,我们再来优化一下。在401496处右键选择Key Patch->Patcher,或按Ctrl+Alt+K
在弹出的窗口中我们将401496处的代码修改为nop
修改后在401496处按C将其转为代码,在0040148C处按P让IDA重新识别为函数,结果如下
红条为IDA提示堆栈不平衡,毕竟混淆控制流后的代码已经不是正常逻辑了,所以提示不平衡属正常现象,不用管。我们去往4014C0处继续分析
红框标记的两处很明显被恶意混淆过了,我们使用上文的方法依次进行nop填充,函数重新识别
,结果如下
修改过后程序流程变得清晰,代码通过对两处字符串进行亦或解密来实现文件下载,并通过WinExec来动态执行
这个样本最终是一个经过简单混淆的木马下载器
jmp short near ptr loc_2
+
1
loc_2:
call near ptr
15ff2a71h
or
[ecx],dl
inc eax
jmp short near ptr loc_2
+
1
loc_2:
call near ptr
15ff2a71h
or
[ecx],dl
inc eax
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!