查看原文
其他

初识逆向工程

计算机与网络安全 计算机与网络安全 2022-06-01

一次性进群,长期免费索取教程,没有付费教程。

教程列表见微信公众号底部菜单

进微信群回复公众号:微信群;QQ群:460500587


微信公众号:计算机与网络安全

ID:Computer-network

软件逆向工程(Software reverse engineering)自从诞生以来就一直是黑客技术爱好者们最为重视的技术之一,正是由于此项技术的诞生才促成了无数黑客技术的蜕变。


软件逆向工程大多数时候都会被简称为逆向工程(Reverse engineering)或直接将其称为逆向(Reverse)。软件逆向工程的基本思路是将二进制代码按照一定格式进行正确有效的反汇编,并通过分析反汇编代码再配合其调用的外部函数或系统API等,对其代码逻辑进行理解,而在这个过程中最重要的就是推理出该二进制代码使用的数据结构


对于想从事网络安全工作的人来说,无论是一名黑客技术爱好者还是一名反病毒工程师,无论所做的工作是渗透测试还是二进制代码审核,掌握逆向都是一项不可或缺的基本功。说得更直白些,一个人在网络安全领域中所能取得的高度取决于其对逆向技术的熟练程度。


很显然,软件逆向工程在免杀技术领域中的地位也是举足轻重的。黑客们往往运用逆向技术分析并学习其他恶意软件所用的技巧,反病毒工程师们也会运用逆向分析技术找到恶意软件的软肋。除此之外,黑客们对第三方应用的恶意利用,或者发现某种新的免杀手法,这些都是以逆向分析技术为根基的。


一、走近逆向


软件逆向工程是一项比较繁复的技术,对于大多数的人来说,对软件逆向工程的研究没必要过于深入,只需要知道其原理,并且能很好地在反汇编代码的丛林中快速锁定那些关键的逻辑分支与特定的数据结构即可,毕竟将其应用到实际工作中才是大多数人迫切想要做到的。


1、工具及基础知识


(1)工具


初学软件逆向工程时需要强大的工具作为支撑,以弥补前期逆向分析能力的不足,而到了后期,成手大多都已经达到“草木皆可为剑”境界,即便使用较为简陋的工具也能迅速展开逆向工作。


因此对于初学逆向的新人来说,手头的工具要全面且强大。下面是需要的工具。


IDA Pro:IDA Pro(简称IDA,Interactive Disassembler),是一款由Hex-Rays开发的世界顶级的交互式反汇编工具。IDA有两种可用版本,标准版(Standard)支持20多种处理器,高级版(Advanced)支持50多种处理器。IDA Pro不存在任何注册机、注册码或破解版,除了测试版和一个5.0的免费版外,网络上能下载的都是包含用户许可证的正版。


OllyDbg:OllyDbg是德国人Oleh Yuschuk开发的一款免费的用户层调试工具,也是迄今为止使用人数最多的用户层调试工具。每个人都可以在其官方网站(www.ollydbg.de)上获取OllyDbg的最新版本。


Microsoft Visual Studio:微软推出的开发环境,可以进行多语言多平台的开发工作。

(2)基础知识


对于软件逆向工程来说,显然越丰富越扎实的基本功对快速理解逆向工程就越有帮助,最佳状态是你已经可以熟练地使用C++开发程序并能熟练地运用汇编语言,而且还对编译原理有一定的了解。


2、程序是从哪里开始运行的


必须要从入口点开始分析代码吗?很显然这样做会浪费大量的时间,因为从入口点开始到main()函数之间的代码都是编译器加进去的用于初始化环境用的,例如判定操作系统版本号、获取模块句柄等操作,直到这些都准备好后,才会执行到main()函数,因此首先要找出main()函数的位置。


那么应该怎样找到main()函数呢?我们先从一个最简单的程序开始,如代码清单1所示。

代码清单1  打印"Hello World!"的代码

我们用Visual Studio以Debug方式编译,再用OllyDbg打开,会看到如下反汇编代码:

编译器在编译我们的代码前会做很多准备工作,而这些准备工作由于涉及的东西较多,且每个由编译器生成的那些用于准备工作的代码都大体一样,因此我们无须深究,快速且准确地找到main()函数才是我们的目的。


但是这看似最简单的工作对于初学逆向的朋友来说往往是一道很难逾越的门槛。下面就介绍怎样突破这个障碍。


想要找到main()函数,就要从C语言本身讲起。我们的程序中必须包含一个名叫main()的函数,不管你多么讨厌它,都必须如此。


查看C99标准,发现int main(int argc,char*argv[])与int main(void)都是被接受的。然后又查看MSDN,可以清晰地看到一句话:The main and main functions can take the following three optional arguments,也就是告诉我们main()函数其实是有3个参数的,其后面有一个微软给出的例子。

很明显,上面的例子和C99标准是不符,但是考虑到大部分程序都是用Visual Studio编译的(而且Borland编译器所要求的C++主函数参数也是如此),因此我们只好无条件地接受它了。


有的朋友可能会感到疑惑,如果使用的是符合C99标准的main()函数呢?例如源码的main()函数不就是两个参数吗?不管代码中实际使用了几个参数,在程序被Visual C++编译器编译时,其main()函数肯定是3个参数的,因为这取决于Windows系统的机制。


至此,我们已经知道了main()函数一个显著特征:有3个参数,而且从本质上看这3个参数的大小都是4字节的。除此之外,通过MSDN可知,应用程序会随着main()函数结束而退出,这使我们知道了main()的第二个显著特征:main函数肯定是在程序退出代码附近的(目前主流的调试、反汇编工具都可以正确识别出退出函数exit)。


知道了这些特征,要找到main()函数就不难了。下面为大家介绍3种方法。


(1)字符串搜索法


对于字符串搜索法,总体来说就是在安装各个版本的C++编译器后,逐个写Hello World,然后用OllyDbg的搜索字符串功能搜索"Hello World"这个字符串,最后逐步回溯即可,以此来建立起各个编译器对于main()函数的调用路径。下面为大家演示一下使用这种方法的步骤。


用OllyDbg打开目标文件后,先记住程序默认停在哪里,然后在CPU窗口单击,依次选择“超级字符串参考”→“查找ASCII字符”,在弹出的界面中选中Hello World并双击,即可定位到main()函数中。OllyDbg的反汇编代码如下:

单击选择函数入口后,可以看到CPU窗格下面的信息窗格中显示如下信息:

单击选中此信息后右击,在弹出的快捷菜单中选择“转到JMP来自0041100F”后即可到上层调用函数(以后我们称之为“返回到调用”)。

我们可以看到与此条语句相邻的其他两条语句都为无条件跳转的JMP指令,由此可以判定:上层调用必然是直接调用这个地址的。因此我们直接返回到调用即可,此时来到真正调用main()函数的地方。

通过上面的代码我们便看到了main()函数的典型特征,临近exit,且有3个4字节长度的参数。接下来我们要做的就是不断地重复上面的步骤,一直到找到程序入口点为止。


最后要做的就是针对不同版本、不同种类的编译器重复上面的步骤,直到收集到自认为足够丰富的信息后即可结束。从此就再也不用为找不到main()函数而苦恼了。


(2)栈回溯法


栈回溯法是先找到main()函数中的那个HelloWorld,设下断点并按F9键运行后查看堆栈情况。这里的堆栈情况如下:

对于这些信息我们只需要关注注释前面有“返回到”3个字的。离我们最近的是以下内容:

单击选中该项后,按Enter键即可来到返回地址0x00411978处。

此时我们又来到了这个熟悉的地方。接下来的事情就要各位自己发挥了(重复上面的步骤)。


(3)逐步分析法


以上两种方法都是在学习与知识储备时用的,在实战情况中由于程序不是我们自己写的,因此很难精准地找到main()函数的关键信息,也就不可能收到什么实战效果。假设我们现在碰到了一个当时就需要分析的软件,而且它的编译环境我们以前没遇到过,这就要求我们纯手工分析并找到main()函数了。这时候怎么办?这就要用到逐步分析法了。


之所以将之称为逐步分析法,是因为我们不需要阅读其代码的具体含义,只需要以JMP与CALL指令为单位逐个跟进,根据main()函数的特征判定其所在位置。


其实这种方法类似于文件搜索,先搜索根目录,再逐层加深搜索其子目录,直到找到需要的东西。


以代码清单1为例,OEP处就是一个JMP,因此其第一层代码内显然不可能有main()函数了。当我们跟进这个JMP后会发现如下代码:

我们发现第二层代码内也没有main函数,但是有两个CALL。因此我们跟进第一个CALL。在这里并没有发现main函数,但是发现了数个JMP与CALL。需要注意的是,一定要逐层搜索。因此这里的CALL与JMP就不要再继续跟下去了,我们现在要做的是返回上一层,看看第二个CALL内是什么。如代码清单2所示。

代码清单2  CALL Test_0.004117D0里的部分内容

我们很幸运,在第三层调用中就找到了main()函数。但是在实际操作中寻找一个陌生编译器的main()函数时,可能没有这么好的运气了。


二、举个栗子


代码清单3  一个简单的小例子

大家应该都知道,程序在执行时会将自己的路径保存到argv字符数组中,因此argc的值肯定是始终大于1的,所以程序如果未经处理总是会显示"Hello world!"。现在就让我们给它做一个“微型外科手术”,让其显示"Hello everybody!"吧。


为了更接近实战,这次我们分析Release版的,找到main()函数,其内容如下:

由于ESP是栈指针,ESP的值也就是当前栈的所在位置,故[ESP+4]的意思就是在当前堆栈+4的地方取其内容。综上可知,上述代码中第一句话的意思就是:取当前堆栈+4的地方的内容,然后与0比较。


第二句话的意思是检查其比较结果。大家都知道,其实CMP指令的作用就是对两个操作数做减法操作,只不过不保存操作结果,仅影响标志位。因此结合起来看,可以用C语言将CMP命令描述为如下形式:

但是[ESP+4]又怎样理解呢?下面带领大家顺便再温习一下栈的结构,如下图所示。

栈结构示意

我们应该还记得,在进入一个CALL后,其ESP指向的地址为这个CALL的返回地址,如果在调用这个CALL之前压入了一个参数,那么要找到它的方法肯定是将当前ESP加上4再取内容。为了便于您理解,下面举一个比较典型的例子。

上述代码的栈结构大致如下图所示。

以上反汇编代码执行后可能的栈结构

通过以上的讲解,相信您对内存中栈的结构应该有了一个比较形象的了解。现在我们回到破解的主题,看看怎样实现我们的目的。以下是3种解决方案:


将0x00401005处的JZ改为JMP。

将0x004020F4处的文本信息改为"Hello everybody!"。

将0x00401007至0x00401012填充为nop,并在0x00401007处加上一句push0x00401018,这样当它执行retn指令时就会返回到0x00401018处。


期望通过这个简单的小例子能使您对软件逆向工程熟悉起来,并在内心对软件逆向工程的难度进行重新定位。

微信公众号:计算机与网络安全

ID:Computer-network

【推荐书籍】

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存