第3章  逆向工程分析 本章进入系统安全领域的学习,围绕逆向工程相关技术,介绍软件安全的基本知识,并 通过实践案例的方式阐述逆向工程分析的方法。 本章学习目标: ● 学会逆向工程关键技术。 ● 运用IDA、Ghidra、GDB等工具进行逆向分析。 ● 学会针对对称密码和非对称密码等算法的逆向技术。 ● 熟悉二进制代码保护与混淆、符号执行、约束求解、软件加固脱壳技术。 通过本章的介绍和实践,希望可以帮助读者快速熟悉软件安全逆向工程,在逆向分析中 还原软件结构,及时发现其中的安全漏洞,提升软件安全意识和系统安全实践技能。 3.1 逆向工程基础 逆向工程(ReverseEngineering)也叫反向工程,通过逆向分析程序获取或猜测其相关 的实现代码。绝大多数应用程序作为公司的商业机密,其源代码并不会随意公开。如果希 望获取这些程序的原始设计思路或代码实现逻辑,唯一可行的方案就是逆向分析。总体上, 软件逆向分析主要是指对软件的结构、程序设计的流程、程序设计的加密算法以及相关功能 实现代码进行逆向分析与拆解。 逆向分析技术包含静态分析与动态分析。在深入学习逆向工程之前,先介绍逆向分析 所需的主要工具。静态分析工具主要有Ghidra和IDA Pro,动态分析工具主要有GDB 和x96dbg。 3.1.1 逆向分析工具Ghidra Ghidra是由美国国家安全局(NationalSecurityAgency,NSA)研究部门开发的软件逆 向工程套件,支持对各种系统平台(包括Windows、macOS和Linux)代码的分析,具有反汇 编、汇编、反编译等功能。 Ghidra安装可遵循官网的指导步骤。安装完成后,进入GhidraInstallDir目录,运行 GhidraRun.bat(Windows)或GhidraRun(Linux或macOS),即可在GUI模式下启动Ghidra。 Ghidra按项目进行管理,使用者需要先创建一个项目,之后就可以使用ImportFile功能导 入需要反编译的文件。Ghidra在加载完反编译文件后,会显示该文件的基础信息,例如架 网络对抗演练 构、大小、MD5值等。由于Ghidra基于Java开发,会花较长的时间分析,效率低于C/C++ 编写的IDA。 下面通过演示某大赛真题来阐述Ghidra功能,目标程序为一个JPG 文件。使用二进 制文件编辑工具010Editor或WinHex打开该文件,图3-1显示文件下方区域含有未知填充 块。根据文件结构可知,PK是压缩包的文件头。因此,可以用binwalk进行分离。binwalk 是一款快速且易用的、用于逆向工程分析和提取固件映像的工具。 图3-1 010Editor查看程序结构 读者可自行下载binwalk安装文件。 binwalk遵循标准的Python安装过程,即运行setup.py文件。安装完成后,运行命令 分离文件,结果如图3-2所示。 binwalk -Me RE_Cirno.jpg 解压分离后的文件夹,得到可执行文件re.exe。在命令行中运行re.exe,查看提示信 76 第3章逆向工程分析 图3-2 文件分离结果 息。根据图3-3显示的运行结果,可以推测需要对获得的字符串进行反转,然后由栅栏密码 对反转后的字符串进行解密,其中参数为9,即每组字符数设置为9。 图3-3 命令行运行目标文件 接着在Ghida新建项目,将reee导入其中进行分析,如图3-4所示。 r.x 图3-4 在Ghidra中分析目标文件 77 网络对抗演练 由于运行程序有按任意键继续的提示,猜测程序使用了system("pause")函数。因此 可以对Ghidra反编译的文件进行字符串搜索。在Ghidra上方的选项栏中找到Search按 钮,单击ForStrings按钮,在选项中查找pause,验证是否调用system("pause")函数,如 图3-5所示。 图3-5 在Ghidra中找到目标文件关键字符串 通过单击右边的交叉引用功能,即XREF 区域,找到调用该功能的关键函数,如图3-6 和图3-7所示。 图3-6 在Ghidra中找到关键函数 图3-7 在Ghidra中找到关键函数及对应伪代码 继续单击Windows按钮,在子菜单中单击FunctionGraph,可以看到该函数的图形化 显示界面,如图3-8所示。 78 第3章 逆向工程分析 图3-8 Ghidra生成的关键函数调用逻辑关系 在右侧的反编译窗口,将关键的代码段复制下来,如下所示。 uint local_70; uint local_6c; int local_68; int local_64 [24]; local_64[0]= 0x73; local_64[1]= 0x5e; local_64[2]= 0x61; local_64[3]= 0x72; local_64[4]= 0x67; local_64[5]= 0x2f; local_64[6]= 0x6b; local_64[7]= 0x72; local_64[8]= 0x41; local_64[9]= 0x30; local_64[10]= 0x31; local_64[11]= 0x69; local_64[12]= 0x75; local_64[13]= 0x76; local_64[14]= 0x65; local_64[15]= 0x30; local_64[16]= 0x71; local_64[17]= 0x5f; local_64[18]= 99; local_64[19]= 0x2f; local_64[20]= 0x5c; local_64[21]= 0x74; local_64[22]= 0x5d; local_64[23]= 0x66; local_68 = 0; 79 网络对抗演练 while (local_68 < 0x18) { local_70 = local_64[local_68]+ 9U ^ 9; local_68 = local_68 + 1; local_6c = local_70; } 其伪代码逻辑清楚,只需要简单修改代码,重新编译运行即可还原该程序算法,还原代 码脚本如下所示。 a=[115,94,97,114,103,47,107,114,65,48,49,105,117,118,101,48,113,95,99,47,92,116,93,102] res='' for i in range(len(a)): res+=chr((a[i]+9)^9) print res reversed_res = res[::-1] print reversed_res 运行脚本,两次打印分别得到以下字符串: uncry1}rC03{wvg0sae1ltof fotl1eas0gvw{30Cr}1yrcnu 最后,读者可以在网上搜索一个在线网站来求解栅栏密码(关键词可以是:栅栏密码加 密解密),设置每组字符数为9,得到最终结果flag{C1rno1sv3rycute0w0}。 3.1.2 静态分析工具IDA Pro IDA(InteractiveDisassembler)是一款交互式反汇编工具,官方网站提供的IDA 安装 包已经扩展到了多个操作系统平台,包括Windows、macOS、Linux。基于不同的授权模式, IDA 提供专业版和免费版两种不同版本。免费版本仅包含基本的处理器加载模块和 Windows系统下常见的可执行文件分析模块。专业版(Pro版本)包含了所有平台的处理器 信息,支持几乎所有的二进制格式,并且支持Java、.NET、MIPS、ARM、DLL等文件格式。 IDA 提供了强大的交互功能,用户可以通过编写自己的加载器和脚本来指导IDA 分析 未知格式文件。如果用户有未知硬件平台的处理器指令集信息,还可以编写基于特定处理 器的文件分析模块。本节继续使用上一节的案例演示IDA 的使用方法。 首先介绍IDA 对主界面各个区域的功能,如图3-9所示。 (1)工具栏。 工具栏(Toolbar)包含了文件分析最常用的一些工具,通过菜单中的“View→Toolbar” 可以添加或者删除工具栏的按钮,通过拖拽功能可以将按钮放置到自己喜欢的位置。 (2)导航带。 导航带(Overviewnavigator)以线性方式显示了当前加载文件的地址信息。默认情况 下导航带会覆盖整个地址空间。在导航区域中单击鼠标右键,可以进行放大或缩小,以便进 行代码区域定位。通过拖动导航带两侧的箭头,可以快速更换反汇编窗口显示其他地址空 间的代码。窗口左侧显示了不同颜色对应的数据类型,可以快速得知当前光标所在地址的 数据类型。IDA 菜单中“Options→Colors”的Navigationbar选项提供了导航带默认数据 类型的颜色设置功能。 80 第3章逆向工程分析 图3-9IDA的主要窗口 (3)标签栏。 标签栏(Tabs)提供了当前已经打开的子窗口标签。通过单击子窗口标题,可以在多个 视图中快速切换。图3-9显示的子窗口包括IDAView-A(反汇编窗口,当打开多个反汇编 窗口,会依次按字母序号命名,例如IDAView-B、IDAView-C)、HexView-A(十六进制窗 口,同IDAview窗口一样,当打开多个窗口会依次按字母序号命名)、Structures(结构体窗 口)、Enums(枚举窗口)、Imports(输入表窗口)、Exports(输出表窗口)。如果需要其他参考 窗口信息可以通过菜单“View→OpenSubviews”功能打开新的参考窗口。 (4)反汇编窗口。 反汇编窗口(DisasemblyView)是主要的数据展示窗口,提供了两种不同的显示风 格———图形视图和文字视图。通过图形视图可以快速分析程序的流程以及函数对于程序流 程的影响。当两种视图激活之后,可以通过空格键在两种视图中快速切换。 (5)图形全局视图。 当图形视图激活时,窗口仅显示了部分图形,这时IDA就会激活图形全局视图(Graph overview),通过在该窗口单击并拖拽鼠标平移设计图面,可以在IDAView-A中快速定位 代码,有助于用户加深对程序整体流程的认识。激活文字视图时,该窗口将自动隐藏。 (6)消息窗口。 消息窗口(MesageWindow)又称日志窗口,用于显示IDA在分析文件过程中执行的 一些操作,或者显示IDA在分析过程汇总时出现的错误信息。如果运行IDC脚本,脚本的 日志输出同样会在该窗口中显示。 (7)函数窗口 如果安装了Hex-Rays插件,函数窗口(FunctionsWindow)将显示当前已经识别的或 者插件认为可能是函数的一些数据,包括函数所在的区段、地址等信息。如果没有安装插 网络对抗演练 件,这个区域显示名称(Names)和字符串窗口(Strings)。 (8)命令窗口。 命令窗口(用于执行简单的命令或者命令序列,以及显示命令执行结果和执行错 误信息。 IDC) 下面演示IDA 逆向分析操作流程。将re.xe拖入IDA 中,加载完成后,单击左侧函数 e 窗口中的_main函数,如图3-10 所示。 图3-10 单击IDA 主要窗口中的_main函数 继续单击_main_0函数,进入主要函数逻辑区域,如图3-11 所示。 图3-11 IDA 主窗口中main函数汇编界面 单击空格键将其转换为汇编模式,如图3-12 所示。 在IDAPro中可以使用F5 功能进行伪代码的转换。如果没有Pro版本的IDA,则无 法使用F5 功能,但可以结合汇编语言以及Ghidra中生成的伪代码同步查看,如图3-13 所示。接 着便可以结合伪代码和汇编代码,对目标程序进行逆向分析,获取程序结构、设计流 程等信息。分析结束后,一般选择不保存相关数据选项,直接退出程序。 根据两种逆向工具的分析结果,简单做一个比较。首先,观察Ghidra的逆向结果,图3-7 28 第3章 逆向工程分析 图3-12 IDA主窗口中汇编代码界面 图3-13 程序伪代码 显示了生成的伪代码,清晰地还原了程序逻辑。接着,观察IDA 的逆向结果,图3-12为生 成的汇编代码,在其地址0x0040F43E对应的汇编指令为“xoreax,9”,但在图3-13中并未 体现异或操作。 Ghidra和IDA 作为目前两种最流行的静态逆向工具,各有所长。Ghidra查看、定位反 编译后的代码更接近源代码,不过其处理某些混淆后代码的能力还有所欠缺。IDA 的功能 更加完善,界面更为友好,性能优于基于Java开发的Ghidra。因此,逆向分析时结合多种工 具进行交叉分析,有助于提升分析的正确性和效率。 3.1.3 动态分析工具GDB GDB是GNU 开发工具系列中的一个重量级产品,它是一个功能强大的调试器,既支 持多种硬件平台,也支持多种程序语言;既可以用于本地调试,也可以用于远程调试;既支持 符号调试,也支持指令级的反汇编调试。通过使用GDB可以完成以下工作: ● 启动程序,指定任何会影响其行为的条件; 83 网络对抗演练 ● 让程序在特定的条件下暂停 ; 检查程序何时暂停,以及暂停时发生了什么事情 ; ● ● 改变程序状态或执行流程。 除此之外,GDB还具有一些特色功能,例如命令自动补全功能、命令行编辑功能、面向 对象语言支持(如C++)、多线程支持等。 GDB的安装非常简单,在Ubuntu下使用apt-getinstalgdb即可完成。在终端使用命 令gdb即可启动调试。 GDB的命令非常多,根据功能特点分类,包含断点类命令(Breakpoints)、数据类命令 (Data)和文件类命令(Files)等。一个命令类中包含了功能相近的一组命令集合。GDB提 供了help命令帮助初学者了解所有命令类列表,用户可以使用help指定相应的命令类来 列出该类型下所有命令的简短说明。除此之外,GDB还能执行shel 命令。 GDB有两种退出方式:quit命令和Ctrl+D快捷键。需要注意的是,在GDB命令行中 使用Ctrl+C快捷键并不会使GDB退出,而只会中断正在执行的被调试程序。 下面开始通过实例演示GDB的功能和使用方法,实例来源于某场大型网络安全竞赛中 的真题。查看文件的概要信息,包括文件类型、文件是32位还是64位、文件运行的基本情 况。使用010Editor打开文件查看其二进制信息,可以判断是一个ELF文件,如图3-14 所示。 图3-14 文件类型 使用filehero查看文件信息,可知hero是一个ELF64-bit的执行程序,那么就能使用 IDA64查看分析目标程序,如图3-15所示。 图3-15 查看文件位数 将文件复制到Lix系统中,在其所在目录下使用命令.o运行,收集程序结构、分 支条件等相关信息,如图3-16所示。 可以看到,程序运行后用户需要输入一个对应的功能选项。选择不同的标号,程序将会 有不同的结果分支:选择1挑战slime,选择2挑战bos,选择3会进入商店花费一定的 coin升级战斗力。 nu/her 84 第3章逆向工程分析 图3-16 文件运行情况 获取文件概要信息后,即可开始对其进行逆向分析。将程序放入IDA64 中,得到程序 的整体控制流程图,包括程序各个功能点及其函数分支,如图3-17 和图3-18 所示。 图3-17 文件的程序逻辑结构 图3-18 文件的slime 函数 58 网络对抗演练 接着按照相同的方法反编译bos 函数的伪代码。图3-19显示,bos 函数有三条龙,战 斗力分别是1000000 、3000000 、5000000,需要打败三条龙才能拿到最终的flag。因此,解题 思路为修改变量ef 的值,让其满足打败三条龙的条件。 图3-19 文件的boss函数 使用IDA切换到汇编代码,boss函数有三个关键比较,分别在地址0x4015f9、 0x40165c、0x4016bc处。可以得知,程序首先与eax的值进行比较,然后根据比较结果决定 是否跳转。因此,在调试过程改变eax的值,即可控制程序流程,从而打败三条龙,如图3-20~ 图3-22所示。 图3-20 boss函数的第一个关键判断指令 图3-21 boss函数的第二个关键判断指令 68 第3章逆向工程分析 图3-22 bos 函数的第三个关键判断指令 在终端使用命令gdbhero启动调试,使用命令b*0x4015F9 、b*0x40165C 、b* 0x4016BC在三个比较语句的位置打上断点,如图3-23所示(注:本书中安装了gdb的插件 pwndbg,读者可以自行安装)。 图3-23 关键语句添加断点 执行命令r运行程序,选择2,进入bos 函数的第一个断点处,如图3-24所示。 图3-24 运行程序至断点1 在第一个断点处使用命令set$eax=0x4c4b44改变eax寄存器的值。根据图3-19反 汇编逻辑,eax的值只要大于cmp语句的第二操作数,程序就不会发生跳转,可顺利执行到 decrypt函数,如图3-25所示。 图3-25 设置寄存器eax的值1 使用命令infor查看寄存器值,发现寄存器rax的值已经发生了改变。特别的,64位寄 87 网络对抗演练 存器中的低32位可延用32位寄存器名,如rax的低32位可用eax表示,该部分内容可详 见第5章寄存器的介绍。接着使用c命令继续运行至下一个断点,如图3-26所示。 图3-26 继续运行程序至断点2 程序运行之后,发现已经完成了第一个bos 程序的验证,因此继续使用c命令执行程 序并修改eax寄存器中的值,使用set$eax=0x4c4b44命令,如图3-27所示。 图3-27 设置寄存器eax的值2 继续使用命令c运行程序至下一个断点,如图3-28所示。 图3-28 继续运行程序至断点3 继续使用命令set$eax=0x4c4b44设置寄存器的值,如图3-29所示。 图3-29 设置寄存器eax的值3 88 第3章逆向工程分析 继续执行程序,获得最终结果,如图3-30所示。 图3-30 获得flag 回顾上述分析过程:首先,定位到slime函数和bos 函数,发现在bos 函数中出现了 flag;接着,分析程序分支,执行命令r动态调试程序,使用set$eax=0x4c4b44在断点1处 改变条件判断语句的结果,在断点2、断点3处做相同的操作;最后,使用c进行继续运行程 序,一直选择2操作,不停地挑战bos,挑战3次成功,获得最终结果flag{0259-6430726f077b-5959-15ba412c83b }。 继续观察store函数,发现还存在另一种解题方法———整数溢出。使用F5反编译store 函数,如图3-31所示。注意到store函数通过scanf方式接收输入并比较,如果scanf输入 变量发生溢出,则可能改变下一条判断语句的执行结果,进而改变程序的执行流程。 图3-31 文件的store函数 int类型可表示的十进制数据范围是-2147483648至2147483647,当v1>2147483647 或v1<-2147483648时会造成整数溢出。v1>2147483647溢出后就会变为负数;相反,当 v1<-2147483648溢出后就会变为正数。故当v1≤2147483647时,能够进入第一个判断 语句,有机会执行充值提升攻击力。进一步,当v2(即coin-2*v1)<-2147483648时,会 发生整数溢出变为正数,可执行第二个判断语句的条件分支eff+=v1、coin=v2。 综合v1和v2需满足的条件,运行程序,设置v1=1073741828、coin=5,如图3-32所 示。由于v1>0,则可执行v2= (coin-2*v1)的赋值语句,发生溢出后可得v2= 2147483645。由于v2>0,可执行coin=v2的赋值语句,得到coin=2147483645,满足打败 三条龙的条件。继续执行,最后得到flag{0259-6430-726f077b-5959-bf477a78c83b }。 98 网络对抗演练 图3-32 运行程序并发生整数溢出 注意,本案例中的flag存在多解,读者可自行分析其多解的原因。 3.2 逆向脱壳分析 UPX(theUltimatePackerforeXecutables)脱壳是逆向工程中一项重要的技术,本节 围绕UPX壳,阐述软件加壳原理、ESP定律脱壳法等内容。 3.2.1 软件加壳原理 加壳是利用特殊的算法,对EXE、DLL文件里的资源进行压缩、加密的过程,是保护文 件的常用手段。加壳后的程序可以直接运行,但要经过脱壳才可以查看源代码。 UPX是一款先进的可执行程序文件压缩器(压缩壳),其工作原理主要是压缩和实时解 压。压缩包括两方面:在程序的开头或者其他合适的地方插入一段代码;将程序的其他区 段压缩。压缩也可以叫作加密,因为压缩后的程序比较难看懂。较之未压缩的代码,压缩后 的代码程序本身变小了,有利于程序的传输。程序执行时由压缩插入的代码完成实时解压, 不会影响程序的执行效率。 UPX加壳操作非常简单,可以直接在官网下载安装。针对不同平台架构,选择对应的 UPX壳版本安装即可。安装完成后,对要加壳的程序使用命令UPX,即可进行加壳操作。 3.2.2 ESP 定律脱壳法 针对压缩壳,可以使用命令upx-d自动脱壳。然而,针对某些“魔改”UPX 壳,使用命 令并不能成功脱壳,因此要采用ESP定律手动脱壳。 90 第3章逆向工程分析 硬件断点是ESP脱壳定律的关键技巧,是由硬件提供的调试寄存器组,用户可以对这 些硬件寄存器设置相应的值,然后让硬件断在需要下断点的地址。硬件断点的触发条件有 四种:访问、写入、I/O以及读写。ESP定律主要运用堆栈平衡的属性,关于栈的详细介绍 可参考本书第5章。对于一个加壳程序,在进行自解密或者自解压时,会将当前寄存器的状 态使用命令pushad入栈。相应地,在解压结束后,会使用命令popad将保存的寄存器状态 出栈。当寄存器出栈时,原有的壳代码会恢复,触发ESP访问硬件断点,再继续单步运行就 非常容易找到程序真正的入口点。 下面演示使用x64dbg调试器对64位程序进行ESP定律脱壳调试。32位程序调试使 用x32dbg或OlyDbg,脱壳过程同64位程序。将目标程序拖入x64dbg,如图3-33所示。 图3-33 使用x64dbg打开目标程序 由于Windows系统特性,此时属于Windows的ntdll阶段,因此需要执行下一步操作, 继续单击“运行”按钮直到找到push指令(即壳压入栈中代码的区域)。继续向下单步执行, 如图3-34所示。单击右侧RSP寄存器,使用其“在内存窗口中转到”功能转到内存1。 在RSP所指向地址中,从内存1窗口界面显示的起始位置开始随意选取一段内存区 域,设置硬件访问断点,图3-35显示选择了4字节内存区域。继续执行程序,当该内容再次 被硬件访问会被断下,如图3-36所示。硬件断点在第一条语句处断下。 继续使用F8键向下单步运行直到jmp指令,其所指向地址即为程序真正的入口点 (OEP),如图3-37所示。 使用F4键运行到jmp所指位置,再使用F8键单步调试,如图3-38所示。 19 网络对抗演练 图3-34 在内存窗口跟随 图3-35 在程序中设置硬件访问断点 92 第3章逆向工程分析 图3-36 硬件访问地址断点位置 图3-37 使用x64dbg脱壳找到程序的OEP 93 网络对抗演练 图3-38 单步调试找到程序真正的OEP 该位置为程序真正的入口点,随后单击“插件”,单击Scyla插件。首先,进行IAT Autosearch、GetImports操作,如图3-39所示。 图3-39 使用插件修复目标程序 94