看到这个标题,想必大家第一个问题多半是什么叫马甲API,第二个问题是为什么叫马甲API?在回答之前,先请思考一个问题,刚上手调试一款软件,最重要的是什么?答案恐怕五花八门,我给大家捋一捋,根据目前的调试技术,回答基本只能从动态和静态两大方向入手,而动态肯定比静态好用太多,举个简单例子,做过代码审计的同学,都一定有过那种看到茫茫多陌生的代码时脑子瞬间“嗡”地要断线的经历,这还是面对着高级程序语言,而且很可能还是手边能找到文档注释的情况,要是换了什么参考也没有,而且语法结构最反人类的反汇编代码,这种痛苦只是翻倍。动态调试为什么好?因为只要咬住执行流,就可以避免陷入无关紧要数量又特别巨大的旁枝代码中,省时省事。
但这是理想情况。要咬住执行流,绝对是一项需要智力和运气的技术活,具体体现就是下断点。抛开各种不断突破下限的反调试手段不说,就算OD载入能F9跑起来,不知道怎么断下执行流也是没法开工干活的。下断点很重要。回忆一下,现在市面的教程教材都是怎么教下断点的,断关键API对不对?这篇马甲API,就是用来对抗这种说教。
说得太虚不好理解,从一个具体的例子入手,譬如说某个CrackMe,输入用户名和注册码,点击“注册”以后,会弹出错误警告框。这时我们要怎么做?最教科书的做法是,因为有弹框,所以断MessageBox,这里还要注意,Windows编程里有MessageBox这个函数,但没有这个 API,这个函数经过编译器编译后,根据编码的不同,可能最终调用的是MessageBoxA或者MessageBoxW,不管哪个,下断的位置都有两个,一个是程序调用API的位置,通常是Call该API在IAT中的对应位置(是个DWORD),一个是API的入口位置。效果是一样的,但如果使用OD,断前者可以看到参数,所以这是首选,一般用Ctrl+N能找到,不过有可能会漏,断在API入口则会相对保险一点。
首先必须承认,这种断核心API的方法确实是咬住执行流的好方法,直接的后果是现在很多CrackMe都不敢弹框的,不过是不是就没法对抗呢?恰恰相反,对抗的方法很简单,譬如本文所用的马甲API。这里先揭开第一个谜底:这个名词是我“杜撰”的,只是用来描述一种技术,利用这种技术,我们不再直接调用核心 API,而改为调用它的“替身”,目的就是使断核心API的方法无法咬住执行流。实现这种技术,只需要三步。
第一步,找到目标 API,也即需要制作“替身”的核心 API。以上文为例,就确定是MessageBoxA吧,获取的方法有很多种,最大路货的方法如下:
DWORD addrofAPI = (DWORD)GetProcAddress(LoadLibrary(user32.dll), MessageBoxA);
用GetProcAddress可以获取系统dll导出API的地址,需要注意的是第一个参数,是HMODULE类型,需要传入API所在dll的句柄,获取的方法有两种,一种是GetModuleHandle,一种是 LoadLibrary,如果是控制台程序就只能用后者,因为只有窗口程序才会载入user32.dll,控制台程序需要手动载入这个dll。除了这种方法以外,还有个更简单的方法:DWORD addrofAPI = (DWORD)MessageBox;
这种方法不常见,但是确实可用的,C语言中函数名称就是该函数的地址,只要通过强制转换就可以直接赋值使用。唯一的限制是必须为通过库文件导出的函数,对于微软私有的API就不能用了。
第二步,制作“替身”。名字听起来很逆天,其实原理很简单:复制。多谢冯诺依曼挖下的坑,API里的执行指令其实也是最普通的数据,只要在内存中另寻一处,将API中的指令(数据)统统复制过去,“替身”就制作完成了,而且只要能将执行流导流过去,功能和原来的API将一模一样。也有人将类似的技术称为制作镜像,或者影子(shadow)。不过制作“替身”的方法,却需要巧思,这是决定成败的关键。如果别人三下两下就顺藤摸瓜找到“替身”,那马甲API的西洋镜也就被戳破了。首当其冲的就是怎么复制,直接用memcpy绝对是自寻死路,不能让别人轻易找到你的复制现场,用repmov之类的指令吧,只要动动脑筋,想玩出点花样来不难。这里只提两个注意的地方,第一个是复制到哪里去,找个Alloc系的API(如VirtualAlloc)分配内存再复制过去行不行?行,常见的镜像就是这么做的,但容易被抓住马脚。这里讲一个具有自主知识产权的方法,顺便揭开第二个谜底,为什么叫“马甲”。先说说马甲,马甲俗称“小号”,首先它也是个 ID,如果我们另外申请一片内存,虽然馅一样了,但皮总是有点差(特别是用OD来看,调用地址就不一样)。所以,要货真价实,马甲API,首先也应该是个API。转变一下观念,所谓API也不过是在特定的内存中放了一些特定的数据(指令)罢了,和新申请一片内存没什么两样。不过话虽如此,但马甲API不是随便找个API就可以了,为了程序稳定,首先得保证这个API不会被其它地方调用,也就是说它必须是个闲置的API,道理简单,你把人家的馅换了,但皮还是原来的,万一被别处调用了,作用肯定会发生改变,最好的结果就是程序崩溃了,要是还因此毁掉重要的数据就更糟糕。此外,这个API也不是随便就行,对传参还有要求,这里选的是VirtualProtect,原因放在第三步仔细讲。最后还有一点,API所在的内存空间通常是不可写的,直接复制会导致失败,需要先重设一下属性。
第三步,调用。你说调用很简单?不,大有文章。前面说了,这里被选作MessageBoxA的马甲 API的是 VirtualProtect,为什么选它?首先它们的参数必须一致,我们将会这样调用这个马甲API:VirtualProtect(0, (DWORD)tst, (DWORD)HF,0);这里演示了如何传参,如果参数数目不一致,编译器是不依的,至于其它,还有类型不同的问题,这个让不少人颇为棘手,如果不指点可能又得是多少百次徒劳无功的搜索,其实强制类型转换就可以了。其次就是它的空间够“大”。前面肯定有同学要问复制的内存块大小,其实不需要太纠结,只要保证需要的都复制过去就可以了,总的来说复制多了比复制少了好,不需要太精确,只要保证复制的目的内存从起到止都不会有其它的执行流经过就可以了。
最后看看编译之后的效果,尽量让编译器不要优化,最佳的观赏体验如图1所示。
是不是顿时明白为什么敢叫马甲 API而不叫其它?VirtualProtect一转眼成了MessageBoxA的马甲,迷惑效果非常好(还附赠 OD品牌认证的全套参数识别,简直跟真的一样),特别是对那些死搬教科书的新手,一来无论如何也找不到弹框的调用,二来无论如何也想不到要通过断VirtualProtect来实现断MessageBoxA的效果。这种方法不但能对抗动态调试的断核心API,对抗静态反汇编效果也很好,不说多了,最后放上IDA最新版的反汇编效果,如图2所示,不看广告看疗效!