频道栏目
首页 > 资讯 > 加密解密 > 正文

静态分析诈欺术: Windows x86下IDA Pro混淆技巧

15-10-13        来源:[db:作者]  
收藏   我要投稿

0x00 前言


此文为我在台湾骇客年会2015社群场中以TDOHacker社群名义发表的「欺骗IDA Pro Hex Rays插件!让逆向分析者看见完全不同的结果 」这边把我在HITCON上发表的一些观念、技巧整理成简单入门的文章方便以后的人想学比较好入门,拓展了些有趣的观念。
 
以前分析恶意软体、破解企业产品、找产品弱点??总是得动态分析追组合语言指令并且对系统架构、API有很深的概念才能深入分析出一支软体架构、功能、手法;但近日各式静态分析工具(大名鼎鼎如IDA Pro)掘起,使数位鑑识、软体破解、逆向工程??等的门槛降低许多(令许多破解者舒适的F5功能)
 
但其实静态分析工具虽是依照真实程序内组合语言状况来做反组译「推论」出原本C语言状况下可能是如何开发的,那麽既然是透过「推论」来还原原始程式码状况,那必定静态分析工具会对一些组合语言排列状况的「特徵」或者「性质」来判定编译器编译特徵,例如说VC编译的迴圈会往下跳执行一段程式码然后回跳,中间插入一句判断句来离开向上跳的行为,这就是一种VC标准迴圈的特徵,当然还有更多种特徵,不一一举例。
 
说其实这类手花诈欺静态工具已经是老伎俩了,这边为初心者入门提供了一些简易入门的手花技巧,如何简易的做到让IDA Pro这类静态反推原始码的工具不好做分析、程序开发者如何自行插入一些手花在程式码来扰乱这类静态工具(而非只是去花钱买一些Themida、VMP之类的壳来帮程序保护,我们也能自己做些小手段做扰乱分析者)最后目标是做到让破解者以静态工具去分析程序原始程式码和实质执行状况不同的一些小伎俩,欺骗那些只会用静态工具做分析的猴子们吧!
 
附注:此篇所有手花伎俩都是建立于诈欺IDA Pro为基础上、并不保证对所有类型静态分析工具皆有效果而测试的IDA Pro版本号为6.6版本,编译环境为Visual C++ 2013(并且保留pdb完整资讯给IDA Pro做参考做分析)0x01 试探IDA Pro静态分析(逻辑顺序)
 
0x01 试探IDA Pro静态分析(逻辑顺序)
在一开始我们完全对IDA Pro静态分析的模式完全没有任何认识的情况下,我用VC写了一个简单的程序并透过逐步加一些手花来测试IDA Pro反组译后的反应如何,首先我写了一个大家都能看明白的Hello World程序:

接著可想而知IDA Pro一定能准确的分析出原始编写状况:

那么接著可以试著尝试做一些简易的改变,例如说简易的跳转但是Jmp太简单了,我试著改成push地址与return,看看IDA Pro反组译的反应如何

状况一如预期的直接无视了push+return的组合(就跟Jmp一样)反组译过程中IDA Pro直接将跳跃后的状况解析进来了,忽视掉跳转指令

好吧,没关系 那我们可以再试试看别的!例如说我们知道push会先在堆栈上申请一个DWORD的空间并且写入你要push的资料,意思是例如push 00可拆解为sub esp,04 + mov [esp], 00,于是我们可以把上述跳转的手法再改写:

不过IDA Pro反组译起来的结果令人感到有些失望:

不过到此我们可以得知的是:1. IDA Pro反组译会根据跳转顺序做一定的推敲 2.除了基础的跳转(jmp、Jl、Je...)也会对ret与堆迭上最后返回的地址做相当的推敲来根据顺序拼凑出原始编写程式码的状况;那如果我们在做些改变呢?例如往下跳转后在做记忆体的往上回跳,是否IDA Pro会误会是一个Loop来处理呢?于是把程式码改写一下:

这时候会看到我们已经成功阻止IDA Pro做逻辑跳转的进阶解析了(至于为什么可以显示出Next01,编译时候的pdb资讯是有给IDA Pro参考的)如果点击Next01标籤,IDA Pro是无法进阶跳转进去的(会被阻止进阶解析进入)看来IDA Pro认为这原本应该会是一个迴圈结构但是找不到判断句跳出所以就当作跳入不再出来了(这也是为什么反组译结果仅显示returun &Next01)
 

咦,不过就有人会想“既然不过就是诈欺IDA Pro误判为迴圈吗?那为什么不直接这么写就好?”于是撰写了以下程式码:

不过很可惜的是IDA Pro遇到push+return的组合就直接作为Jmp处理了,所以这方面解析是完全没有问题的不会有任何误判的,到这边我们可以得到一些结论如下:
 
IDA Pro对于Jmp的跳转是完全没有任何问题的(废话)IDA Pro可识别push+return的组合等价于Jmp指令IDA Pro对于纯暂存器搬移处理[ESP]在做条件/return返回的分析能力是很弱的
逻辑跳转大致上用的手法如上介绍,当然不一定要把跳转写在函数内也可写在外部函数,如下:
 
这边在入口做了一个简单的长程跳跃,而跳跃后的函数地址保存于全域变数中

而效果也是令人感到不錯滿意的,僅顯示一個return Label

如果點擊了_next()該標籤做解析,此時IDA Pro就會告知以下資訊:

仅告知它是全域变数,偏移地址为多少,至于这个手法虽然不能很有效的避免分析(可透过人工阅读组合语言程式码来做分析跳转位置)但在一些自动化的加壳、混淆工具上做保护,「逻辑混淆」便也是依此方式做大量的来回跳转来扰乱静态分析工具的追踪。

0x02 举个栗子

这种手法可以用在哪裡?例如说有一个小型的CTF — AIS3中有个Crypto考题这麽出的:

图中可以看到在入口处(public start)可以看到从入口处有个红色箭头往底下指了下去,但是指下去后又跳回了loc_4000CF的位置上(灰色箭头返回处)而透过IDA Pro的反组译功能观察到的程式码如下:

 

这边就用了这样一个很有趣的做法来增加Crypto考题的难度,避免参加CTF比赛者完全没有阅读组合语言的能力,仅透过反组译工具来静态阅读、分析,考出了「组合语言阅读能力」,到这个题目上可以看到它用的跳转是Jmp,由文章开头到此我们可以对IDA Pro的「函数结尾」判定解析有了一些了解:
 
正常情况下,遇到ret视为函数结束若找查不到ret则以Jmp当作结尾Jmp在函数内来回跳可做解析Jmp往下跳则无法解析回C(但可点击至该地址)Jmp往下跳后又往上跳(往回跳)则无法做出明确反组译

接著讲到此,顺道提一下由知名资安厂商出的CTF — Flare-On,解完第一题后通关会获得第二道题目,第二道题目中也不巧用了这个手法,所以就顺道写进来介绍了XD

首先我们可将此道题目透过IDA Pro分析,可看到此程序有三个函数,一个标准入口点还有两个自定函数,接著我们一一稍微检阅一下它们:

首先看见Sub_401000,点击做进阶解析是无法做解析的,IDA Pro会提示该函数的堆栈空间坏掉了(也许是函数头、申请空间、对堆栈空间释放、使用...等状况)导致IDA Pro无法做精确的解析反组译回标准C的状态,好的 那麽先放著,看下一个。

接著看到sub_401084上,IDA Pro可很轻易的反组译推敲出原始状态,看这模样大概是KeyGen一部份吧,先放著不理它,来看看入口点怎么做。

看到入口处可看见主要拆成红区、橘区、绿区三区,红区做了一个函数呼叫然后丢给橘区做运算处理接著绿区就用到了我们前面提到的逻辑顺序置换的手法,跳到入口的组合语言观察一下:

这边可以看到有趣的是绿区的jmp反应了前面反组译时看到的回跳,应证了前面我们实验时所见的,IDA Pro反组译对于「回跳」到前面的记忆体模块这个行为,会当作函数尾端来处理的(视同return一般作为解析的尾巴)

好的,接著那么我们可以来观察一下红区的函数呼叫怎么做的,我们回到sub_401000的组合语言来观察一下!(也就是一开始我们发现IDA Pro可辨识出有三个函数,但是不能做反组译、函数堆栈坏掉的那个函数)

接著可以发现,原来在标淮函数头初始化(也就是橘区那段)之前被插入了一个pop eax!接著橘区做的就是标淮函数头会做的初始化函数,然后底下开始做题目的显示、解析Key正确与否...等KeyGen的行为。(原来就是该pop eax导致此函数被IDA Pro认为函数堆栈坏掉啦!)

所以回到函数头可以发现call sub_401000这个行为可以拆解成push 0x004010E4 接著Jmp sub_401000,但又因sub_401000的函数第一个指令就做了pop eax,故前面push的0x004010E8原本位在堆栈第一个指标就被释放掉了!于是我们会发现这个入口点其实底下红区处是完全不会被执行到的,事实上入口处进来就直接jmp到sub_401000做事情了,参赛者完全不必分析底下多馀的程式码,这是一个很漂亮的IDA Pro反组译诈欺手法呀!
 
最后,透过VC我实作了这题的诈欺手法原始是怎么撰写出来的,写法如下:

编译后将程序交给IDA Pro反组译分析,可看见:

嘿嘿,与Flare-On第二题完全一样的伎俩被我们学会怎么编写了!
 
以上内容提及了手花做混淆如何影响静态分析工具与一些实际应用这些技巧的例子,不过我们并不满足于此,我们终极目标是撰写出让分析者用静态工具观察到A程式码而实际执行的是B行为呢!那我们该如何做到这种感觉是「不可能的任务」的手法呢?
 
0x03 IDA Pro反组译分析模式观察
 
静态分析工具在做反组译理论上应以「组合语言程式码做了多少事情,就应该推论出多少原始码」为主,所以只有前面0x01~0x02的主题做单纯的逻辑置换,显然对于我们要做到「隐藏要做的程式码」与「显示出欺骗的程式码」的目标还很遥远呢!
 
对此我们需要有更进阶对静态分析工具的反组译推敲模式有更进阶理解才行!这边我透过内迁组合语言的方式插入了一堆「纯暂存器操作」的行为,意图确认IDA Pro在做反组译时主要的基淮是以什么为基淮点,这边我插入了大量对纯暂存器的操作(红区)意图扰乱静态分析工具的反组译行为,接著橘区做我想做的程式码,最后恢复一下堆栈(因为前面用了push 00)然后离开这个入口函数。

而在IDA Pro解析了编译出的程序后,可见组合语言方面我做扰乱的红区部分没有因为VC编译器优化而被移除,橘区要做的程式码也都还在,那么我们来看看IDA Pro反组译的结果:

 

而在IDA Pro解析了编译出的程序后,可见组合语言方面我做扰乱的红区部分没有因为VC编译器优化而被移除,橘区要做的这时候反组译之后看到的程式码却只还原出橘区的程式码,前面红区那些针对纯暂存器的操作通通被IDA Pro认为无用途而移除不做解释了!这边可以发现IDA Pro在反组译解析时的特性:针对一个个函数为基础单位做解析仅针对函数内区域变数(或者操作到全域变数)的部分做解释回C程式码由第二点,对于其馀纯暂存器操作是不做任何解析的(被忽视掉)第三点唯一的例外为esp暂存器,用于确认该函数是否损毁、实际返回状况?(透过esp返回状况,IDA Pro才能画出流程图)这时候可以想想看,有什么办法是可以做到:跳转到其他函数、地址上不操作到任何区域变数、全域变数不使用Jmp、Jl、Je等跳转指令(只要出现IDA Pro便能画出图)纯暂存器操作0x04 SEH(Structured Exception Handling)程式码也都还在,那么我们来看看IDA Pro反组译的结果:

举例到这边,对Windows PE结构熟的朋友立刻就能想到:这不就是Windows异常处理机制SEH吗?是的!有用过SEH形式的异常处理并且逆向过就可以得知SEH组合语言写法上如下
 
push Handlermov eax,fs:[0]push eaxmov fs:[0],esp
上面是一个标淮的SEH式的try在做「异常机制注册」的程式码,fs:[0]指向了Windows的Thread遇到异常出错时,要去找谁解决,这时候fs:[0]保存著一群帮忙解决异常问题的「Handler」的清单(至于Handler是什么?例如说你的try中的catch要做的处理,就是Handler在做的处理,只是编译器会帮你封装为Handler的函数型态)
 
所以注册时会先把Handler先push入堆栈,用堆栈空间来纪录Handler的地址接著把原始fs:[0]清单上原始Handler也push推到堆栈做保存,接著把esp写入到fs:[0]之内,如此一来fs:[0]内清单Handler就会是:原始Handler -> 接著是你的catch的handler -> 剩馀的其他Handler …依此接续下去,如果Thread在运作时发生任何异常,就会调阅出fs:[0]的清单来找Handler协助解决问题。
 
那么,我们既然做了push两次,势必做完try包起来的事情必须帮忙try做恢复,避免堆栈被我们玩坏掉呢,有注册就有解除注册的机制,其实也就是把前面那段注册的程式码倒著写
 
mov eax,[esp]mov fs:[0],eaxadd eap,0x08
到这边你会发现,SEH异常处理机制不就恰恰满足我们想要的「纯暂存器处理」、「不动用程序中的变数」了吗?简直为我们这次目的量身打造的设计呀!
 
0x05 诈欺IDA Pro:反组译与行为完全不同的伎俩
首先,将VC的专案中C/C++的例外处理机制改为「是,但有SEH例外状况」来处理(预设为「是(/EHsc) )如此一来我们就可以不用深入理解SEH结构体的运作写法以纯组合语言方式来操作SEH异常处理写法了(不过有兴趣的童鞋还是可以Google一下的)

我们可以写一个很简单的try,在try内写上印出“ADR Is Handsome!”而在发生异常时显示出“Hello World”,那么我们来看看IDA Pro分析状况:

在0x004113D5 ~ 0x004113E0的部分与底下0x00411408就是一整个SEH的注册。 (push了Handler并且重新连结fs:[0]中的Handler顺序)

而到底下可看见前面我们发现的0x00411408就是最后一句完成SEH注册的部分,底下做了我们写好的透过printf打印出“ADR is Handsome!”,如果打印没出任何状况,底下jmp loc_41144E就会做恢复SEH注册并且离开函数。这裡我们可以发现,咦?他居然有了jmp,所以IDA Pro应该会把此Jmp视为此函数的结尾,并把catch部分的程式码忘记才对!接著我们透过IDA Pro反组译看看是否如我们预期一样:

看起来效果相当显著啊,肯定是电系神奇宝贝遇上了水系神奇宝贝了(?)这裡看见我们已经可以透过try的方式来隐藏我们不希望别人看到的程式码,不过目前执行的程式码的确就是执行印出“ADR is Handsome!”那么我们该如何改变程序执行流程,来跳到Handler的部分而不执行原始程式码呢?这问题简单呀,SEH本来就是用于作异常机制处理,那我们只要再执行显示”ADR is Handsome!“以前引发一个异常就可以了嘛!

所以我们可以写出像这样子的程式码,在printf出”ADR is Handsome!“以前先透过xor eax,eax清空eax后再去存取[0]的值为多少,就会引发记忆体不存在的错误而跳转到Hello World显示了!编译好后交给IDA Pro反组译状况如下:

效果显著啊,那么执行起来呢?

成功做出了执行与显示是两回事,嘴上说不要身体却很实在的程序呀!
 
0x06 延伸有趣的解析伎俩
 
前面从0x01 ~ 0x05章节我们一路透过IDA Pro的解析特性来引导IDA Pro反组译时解析到我们故意提供的错误资讯,那么还有什么有趣的伎俩呢?例如说:

我们前面透过SEH类型的Try然后引发异常错误来引导IDA Pro解析到与执行是不同的程式码段,那么这样还有什么有趣的呢?既然我们知道IDA Pro会由函数头一路解析至Try内包的所有程式码结束,那么有趣的事情来了,在标淮C++编译下的函数,例如说有3个参数,那么因为函数头部分会去申请3个DWORD空间来存放参数,所以return时候必须释放掉来平衡堆栈,一般来说返回会写成:ret 0x0C (0x0C = 12 = 3* sizeof(DWORD) = 3 * 4)
 
所以假设我们故意在IDA Pro解析时给予ret 0xFF呢?是否会被视为参数有63几个参数呢?
 
把编译好的程序交给IDA Pro做反组译得到:

嘿嘿,真的成功引起IDA Pro反组译对参数需求的辨识错误啦!
 
最后,还有没有什么跳转手法是可以引起IDA Pro反组译解析错误的?事实上还有许多的,只要能达成纯暂存器处理与不动用变数即可!这边做个简单好吃的小栗子!

这边我把实际执行的程式码放在H3llo()函数内,在入口处想办法Jmp过去就对了!不过前面我们推论出不管是用push + ret或者Jmp系列跳转指令,IDA Pro反组译都可推论出正确的ESP并且进阶追踪下去,无法做到完全隐藏的行为,那么真的就这样放弃吗?
 
这边我做了一个纯暂存器处理与不动用变数的例子,透过push函数地址,再push随意一个垃圾上去,最后想办法去把那个垃圾从堆栈上释放掉(例如这边我用了lea指令来实现ESP+4的行为)在做ret,此时IDA Pro反组译就无法推论出正确的ESP做持续追踪,出现了以下的反组译状况:

又是一个可以做到实际执行与反组译结果完全不同的做法萝!
 
0x07 总结
 
玩手花是相当好玩、有创意的!这边内容总结了许多静态反组译工具的一些解析都是因为分析时太过依赖一些特徵才导致可以被恶意利用,撰写出造成静态分析工具分析错误的一些小伎俩;而现在静态分析工具越来越进步下,许多恶意软体鑑识人员渐渐仰赖静态分析工具而少开了动态分析工具,这边提及了一些简单的诈欺手法怎么做混淆、手花,也藉此提醒分析人员应以动态分析为主而静态分析为辅,在动态分析时遇到难以解决或者看不懂的部分才以静态分析工具做查询,才是正确的分析王道!
相关TAG标签
上一篇:ocp-324
下一篇:ocp-336
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站