1. 构造有效载荷:根据以上条件,创建一个满足要求的 kfile.data 文件(例如,包含至少8个‘G’且总长度≥16字节的字符串)。

  2. 验证成功:使用构造的文件,程序验证通过,显示成功对话框。

破解条件总结(代码描述):

# 伪代码描述 crackme 的验证逻辑
def validate_kfile(file_path):
    if not os.path.exists(file_path):
        return False
    with open(file_path, 'rb') as f:
        data = f.read()
    if len(data) < 16: # 0x10 字节
        return False
    if data.count(b'G') < 8:
        return False
    return True

4. 给初学者的逆向工程建议 💡

逆向工程是一门需要耐心和经验的技艺。以下是给初学者的一些核心建议:

  • 具备编程基础:了解开发者如何编程(编码风格、常见模式),能极大帮助你在逆向时理解程序结构。

  • 关注代码集中性:功能相关的代码或数据往往被放在相邻位置。理解一块代码时,可查看其上下文。

  • 理解代码复用:程序员会复用代码。识别出重复或相似的模式,能帮助你快速理解程序模块。

  • 七分逆向,三分猜测:逆向需要基于经验和代码上下文进行合理推测,但推测后必须通过动态调试等方式进行验证。

  • 区分代码:学会区分程序核心逻辑与编译器生成的启动代码、库函数代码等。

  • 保持耐心:逆向分析,尤其是初期,可能进展缓慢。遇到复杂程序花费数小时是正常的,坚持练习是提升的关键。

推荐学习资料

  • 《C和指针》:深入理解C语言核心,其指针概念与汇编寻址紧密相关。

  • 《汇编语言(第3版)》:王爽著,经典的汇编入门教材。

  • 《恶意代码分析实战》:学习分析方法和工具集。

  • 《深入理解计算机系统》:从底层理解程序如何运行。

  • 《加密与解密》:专注于软件保护与逆向分析。


5. 课后作业与练习 🏆

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/6b89c91ac1fea8225d5c9733c6049691_10.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/6b89c91ac1fea8225d5c9733c6049691_12.png

为了巩固本节课的知识,请完成以下练习:

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/6b89c91ac1fea8225d5c9733c6049691_14.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/6b89c91ac1fea8225d5c9733c6049691_15.png

作业:完成“合天网安实验室”逆向游乐场的第1关至第3关。

  • 平台:登录“合天网安实验室”或类似CTF平台。

  • 方式:创建实验机(预置环境的虚拟机),按照实验指导的步骤进行分析和破解。

  • 目的:这三关的设计能很好地练习文件检查、简单算法逆向和条件修改等基础技能。

工具准备:课程群文件中提供了 IDA Pro 等核心逆向工具,请自行下载安装以备使用。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/6b89c91ac1fea8225d5c9733c6049691_17.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/6b89c91ac1fea8225d5c9733c6049691_19.png


6. 常见问题答疑 ❓

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/6b89c91ac1fea8225d5c9733c6049691_21.png

  • Q:如何判断一个程序是否加壳?

    A:使用 StudyPEPEiDExeinfo PE 等工具查看程序的文件类型信息。将显示的类型在搜索引擎中查询,即可知是何种编译器生成或何种壳保护。未加壳的程序静态分析时逻辑清晰,加壳程序入口点代码通常混乱且复杂。

  • Q:在IDA中面对复杂的控制流图,如何找到突破口?

    A:1) 始终从程序入口点(start)开始。2) 结合动态调试,运行程序并观察其实际执行路径。3) 明确你的目标(例如,找到成功提示的代码位置),然后从目标点反向追踪,分析哪些条件会导致程序到达这里。4) 善用IDA的图形视图缩放功能,先从整体把握程序结构,再深入细节。

  • Q:逆向工程很难,学不会怎么办?

    A:逆向工程确实是网络安全中门槛较高的领域,因为它直接处理不直观的二进制代码。初期感到困难非常正常。关键在于 多实践、多调试、多总结。从简单的 crackme 开始,逐步增加难度。年轻是最大的资本,只要持续投入时间练习,一定能掌握这项强大的技能。


https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/6b89c91ac1fea8225d5c9733c6049691_23.png

本节课中我们一起学习了两种实用的软件破解方法,回顾了完整的逆向分析流程,并探讨了给初学者的学习建议。逆向工程的核心在于“大胆猜测,小心求证”,并结合动静态分析工具反复实践。请大家务必完成课后作业,在实践中深化理解。如有任何问题,欢迎在课程群中提出讨论。

093:基础逆向工具使用指南 🔍

在本节课中,我们将学习逆向工程分析的第一步:如何查看和分析目标文件的基本信息。我们将介绍两款核心工具,并讲解在安全环境下进行分析的重要性。

文件基本信息查看 🔎

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_1.png

上一节我们介绍了逆向工程的基本概念,本节中我们来看看如何开始分析一个具体的程序文件。首先,我们需要了解文件的基本情况。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_3.png

可以使用十六进制查看和修改工具来完成这一任务,例如 UltraEdit (UE)

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_1.png

UltraEdit 这款工具可以查看文件的十六进制内容,并允许进行修改。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_3.png

它也可以用于比对两个文件的差异。我们的演示是在虚拟机中进行的。在进行逆向分析时,最好也在虚拟机环境中操作,因为你分析的程序很可能带有后门或恶意代码。如果在主机上进行分析,一旦中招,清理会非常麻烦。因此,建议专门准备一个虚拟机用于逆向分析。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_5.png

这是我们打开的 UltraEdit 程序界面。接下来,我们将用它来分析今天的目标程序。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_5.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_7.png

首先清除之前的编辑内容。这是今天要分析的程序,我们可以在 UE 中打开它。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_7.png

通过十六进制视图可以查看文件内容,理论上也能进行修改。但在当前阶段,我们不了解程序机制,无法确定修改何处。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_9.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_9.png

相信大家对这些基础工具的使用已有所了解,因此不再赘述。UE 还包含一个 UE Compare 功能,用于比对两个文件。当你修改程序后,可以用它来查看具体修改了哪些部分。红色高亮区域即表示存在差异。

PE 文件分析工具 📁

接下来,我们介绍另一个重要工具:Stud_PE。它是一款专门用于分析 Windows PE(Portable Executable)文件格式的工具。

我们打开 Stud_PE,并加载要分析的程序文件。

从这里可以看到,它明确告诉我们这是一个 32位的PE文件。这个信息至关重要。例如,在使用反汇编工具 IDA 时,你需要根据程序位数选择对应的版本:

  • 32位程序使用 IDA 打开。

  • 64位程序使用 IDA64 打开。

如果这里是64位,你就知道应该用 IDA64 来打开它。

下方还显示了文件类型等信息。有些文件可能被加壳(如 UPX),这里会显示“无加壳”。通过 Stud_PE,你可以查看 PE 文件的文件头、各个节(Section)等详细信息。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_11.png

这是 Stud_PE 工具的主界面。

Linux 平台下的对应工具 🐧

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_11.png

在 Linux 系统中,可以使用 readelf 命令来查看 ELF(Executable and Linkable Format)文件的属性。

例如,分析一个简单的 a.out 文件(一个打印 “Hello World” 的程序):

readelf -a a.out

因为 Linux 平台的可执行文件格式是 ELF。刚才介绍的 Stud_PE 是 Windows 平台下的工具。readelf 命令会输出类似“ELF 64-bit”等信息以及其他文件细节。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_13.png

以上内容简单介绍了逆向分析初期需要了解的文件相关基本情况。

总结 📝

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3025152c81530b29dbaffc9c878ee713_13.png

本节课中我们一起学习了逆向工程分析的基础准备工作:

  1. 环境安全:强调在虚拟机中进行逆向分析以保障主机安全。

  2. 基础查看:使用 UltraEdit 进行十六进制查看和文件比对。

  3. 文件格式分析

    • 在 Windows 下,使用 Stud_PE 分析 PE 文件的位数、加壳情况等关键信息,为后续使用 IDA 等工具做好准备。

    • 在 Linux 下,使用 readelf 命令分析 ELF 文件。

掌握这些工具和步骤,是开启任何逆向分析任务的第一步。

094:IDA Pro 介绍及实战 🛠️

在本节课中,我们将学习逆向工程中一款核心工具——IDA Pro。课程将分为两部分:首先介绍IDA Pro的基本概念、界面和常用功能;随后,我们将通过一个实际的破解程序案例,演示如何运用IDA Pro进行静态与动态分析,以理解程序逻辑并找到关键破解点。

IDA Pro 简介

上一节我们明确了课程目标,本节中我们来看看IDA Pro是什么。

IDA Pro是一款交互式反汇编器(Interactive Disassembler),是目前功能最强大、应用最广泛的静态反汇编软件。该软件为商业软件,但存在激活方法。其核心功能是分析二进制程序,将其机器码转换为人类可读的汇编代码,并尝试识别其中的函数、库和数据结构。

IDA Pro 界面与窗口

使用IDA打开一个程序后,软件会自动进行分析,并将分析结果呈现在多个窗口中。下面我们通过实际操作来认识这些窗口。

我们使用IDA打开一个32位、未加壳的示例程序。打开后,主界面如下:

以下是主要的窗口及其功能:

  • 反汇编窗口(IDA View):这是主要的分析区域,以流程图或列表形式显示程序的汇编代码。图中箭头表示程序执行的可能跳转路径。

  • 函数窗口(Functions Window):列出IDA分析出的所有函数,包括程序自身的函数(如start)和导入的Windows API函数。点击函数名可快速定位到其代码。

  • 流程缩略图(Overview):当程序流程非常复杂时,可以在此缩略图中拖动,快速定位到需要查看的代码区域。

  • 信息输出窗口(Output Window):显示IDA的操作日志、错误信息等。

  • Python脚本窗口(Python Script):IDA支持Python脚本扩展,可以在此执行Python命令或脚本。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_1.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_3.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_5.png

除了默认窗口,用户可以根据需要打开或关闭其他窗口。例如,在View -> Open subviews菜单中可以打开Strings窗口,该窗口会提取并列出程序中出现的所有字符串常量,这在逆向分析中非常有用。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_7.png

常用快捷键

熟练使用快捷键能极大提升分析效率。IDA与后续将学习的OllyDbg等调试器有许多通用快捷键。

以下是核心的快捷键列表:

  • F2:在光标所在行设置或取消断点。

  • F7(Step into):单步执行。如果当前指令是函数调用(call),则会进入该函数内部。

  • F8(Step over):单步执行。与F7的区别在于,遇到函数调用时,会将整个函数当作一条指令执行,不会进入函数内部。

  • F5:将当前函数的汇编代码转换为更易读的C语言伪代码。

  • F9:运行程序。如果遇到断点则会暂停,否则运行到程序结束。

  • ESC:返回到上一个查看的位置。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_9.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_11.png

掌握工具的最佳方式是在实践中反复使用,无需死记硬背。

显示代码地址

默认情况下,反汇编窗口可能不显示每条指令的地址。为了方便定位和协作,我们需要将其显示出来。

设置方法为:Options -> General,在对话框中勾选Line prefixes (graph)选项。勾选后,每条汇编指令前都会显示其内存地址。

实战:破解一个演示程序

前面我们介绍了IDA Pro的基本情况,下面我们通过一个实际案例来体验其强大功能。本次实战的目标程序是一个简单的许可证检查程序,运行时会提示“valuation period out of date”。我们的目标是在不购买许可证的情况下使其正常运行。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_13.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_15.png

实验环境:本案例可在“和丰实验室”在线实验平台完成。课后同学们可以自行登录平台进行练习。为演示流畅,此处使用本地环境。

第一步:初步分析与运行

首先,我们运行目标程序,确认其行为是弹出过期提示。然后使用查壳工具(如Stud_PE)确认其为32位程序且未加壳,这意味着我们可以直接使用IDA进行静态分析。

第二步:静态分析定位关键字符串

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_17.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_19.png

我们使用32位IDA打开程序。面对一个陌生程序,一个高效的切入点是查找程序中的字符串。

  1. 打开Strings窗口(View -> Open subviews -> Strings)。

  2. 在字符串列表中,我们发现了目标字符串“valuation period out of date”。

  3. 在该字符串上按Ctrl+X,可以查看所有引用该字符串的代码位置。我们跳转过去,发现它在一个显示消息框的函数(MessageBoxA)调用之前。这证实了错误提示由此产生。

  4. 观察上下文,发现程序执行流在此处有一个关键跳转(jnz)。如果跳转到左侧分支,就会执行MessageBoxA显示错误;反之,则可能走向正常流程。因此,我们的目标是让程序执行右侧分支。

第三步:动态调试验证猜想

我们分析跳转条件上方的代码。发现程序调用了CreateFileA函数,尝试打开一个名为keyfile.dat的文件。如果文件不存在或打开失败,函数返回值(通常存储在EAX寄存器)会是一个特殊值(如-1,即0xFFFFFFFF),导致后续比较(cmp eax, 0FFFFFFFFh)不相等(jnz),从而跳向错误分支。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_21.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_22.png

为了验证,我们在程序目录下创建一个空的keyfile.dat文件,然后重新运行程序。此时错误提示变为“keyfile is not valid!”,说明程序执行流发生了变化,我们的分析方向正确。

第四步:深入分析验证逻辑

我们继续用同样的方法,在Strings窗口找到“keyfile is not valid!”字符串,并定位到其引用代码处。在此处设置断点。

分析附近代码,发现程序会读取keyfile.dat文件的内容(ReadFile),并检查读取的字节数。关键判断指令为:

cmp     [ebp+NumberOfBytesRead], 10h
jl      short loc_4010F7 ; 如果读取字节数 < 16,则跳转到失败分支

这意味着,keyfile.dat文件的内容必须不少于16个字节,程序才会继续向下执行,否则将提示无效。

我们向keyfile.dat文件中填入超过16个字节的任意内容(例如一串“A”),再次运行程序。使用IDA的调试功能(Debugger -> Select debugger -> Local Windows debugger,然后按F9运行),程序会在我们设置的断点处暂停。观察流程图的箭头闪烁提示,确认程序跳向了“有效”分支,绕过了“keyfile is not valid!”的提示。

第五步:定位最终校验逻辑

程序继续执行后,进入一个循环结构。在循环结束后,存在另一个关键跳转,决定最终是显示成功信息(“You really did it!”)还是失败信息。我们在此处设置断点进行分析。

动态调试运行到此处,观察寄存器状态和判断条件。例如,判断可能涉及对文件内容特定位置的校验(如比较ESI寄存器的值与某个常数)。通过修改keyfile.dat文件的内容,并观察程序执行路径的变化,我们可以推断出最终的校验逻辑,从而构造出能通过所有检查的合法keyfile.dat文件,完成破解。


https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/ddfc92776b7029edcb4c51c2c7a07481_24.png

本节课中我们一起学习了IDA Pro的基础知识,包括其界面、常用快捷键和核心的静态分析功能。更重要的是,我们通过一个完整的实战案例,演练了逆向工程的基本思路:运行观察 -> 静态分析定位关键点 -> 动态调试验证假设 -> 逐步深入理解程序逻辑。掌握这个流程,并熟练运用IDA Pro这样的强大工具,是迈向网络安全逆向工程领域的重要一步。课后请务必在实验平台上亲自完成整个练习,以巩固所学知识。

095:IDA Pro实战(二)🔍

在本节课中,我们将继续学习使用IDA Pro进行逆向工程分析。我们将通过一个具体的实例,深入理解程序如何验证外部文件,并学习如何通过分析汇编代码来找到正确的验证条件。


概述

上节课我们介绍了逆向工程的基本概念和IDA Pro的初步使用。本节中,我们将通过一个实际的程序,分析其文件验证逻辑。我们将重点关注一个循环结构,理解它如何读取文件内容并进行判断,最终找出使程序验证通过的关键条件。

分析验证逻辑

上一节我们介绍了程序的关键跳转点,即比较ESI寄存器的值与数字8。本节中我们来看看ESI的值是如何被确定的。

程序的关键在于一个循环结构。该循环负责读取文件kfile.data的内容,并根据内容计算ESI的值。我们的目标是让ESI大于或等于8,从而使程序走向成功的分支。

以下是循环内部的核心操作步骤:

  1. 读取文件内容:程序从文件地址0x40211a读取一个字节到AL寄存器中。

  2. 检查循环结束:比较AL的值是否等于0。如果等于0,则退出循环。

  3. 关键比较:比较AL的值是否等于0x47(即十进制71,对应ASCII字符‘G’)。

  4. 条件跳转与计数

    • 如果AL不等于0x47,程序会跳转(JNZ)并跳过下一条INC ESI指令。

    • 如果AL等于0x47,程序不跳转,顺序执行INC ESI指令,使ESI的值增加1。

因此,ESI的值实际上就是文件中字符‘G’(0x47)的个数。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_1.png

动态调试验证

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_3.png

为了更直观地理解这个过程,我们使用IDA Pro的调试器逐步执行。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_5.png

  1. 在循环开始处设置断点,并启动调试。

  2. F8键单步执行。观察AL寄存器的值,它依次变为0x31, 0x32, 0x33, 0x61… 这对应了文件内容“123a…”的十六进制值。

  3. AL的值(例如0x31)与0x47比较时,由于不相等,程序发生跳转,ESI不会增加。

  4. 我们可以手动修改内存中的文件数据。将当前字节(例如0x62)改为0x47

  5. 继续执行,此时AL等于0x47,程序不跳转,执行INC ESI指令,ESI从0变为1。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_7.png

通过这个动态过程,我们确认了之前的分析:程序通过循环统计文件中字符‘G’的数量,并将其存储在ESI寄存器中。

构造有效文件并破解

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_9.png

根据以上分析,要使程序验证通过,我们需要满足两个条件:

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_11.png

  1. 文件长度至少为16个字节(根据上节课分析)。

  2. 文件中至少包含8个字符‘G’(大写字母G)。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_13.png

以下是创建有效验证文件的步骤:

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_15.png

  1. 创建一个名为kfile.data的新文件。

  2. 在文件中输入至少16个字符,其中至少包含8个‘G’。例如,内容可以是“GGGGGGGGxxxxxxxx”(x代表任意其他字符)。

  3. 保存文件。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_17.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_19.png

现在运行目标程序,程序将成功读取kfile.data文件。在循环中,每当读到‘G’时,ESI就会加1。循环结束后,ESI的值将大于等于8。

程序随后比较ESI与8,条件成立,执行流跳转到成功分支(例如弹出一个显示“Success”的窗口),从而完成了破解。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_21.png

总结

本节课中我们一起学习了如何深入分析一个程序的验证逻辑。我们通过IDA Pro的静态分析与动态调试相结合,揭示了一个基于文件内容计数的验证机制。核心发现是:程序通过统计文件中特定字符(‘G’)的数量来决定验证是否通过

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_23.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/7d8a337b1ea909a3d9e17cc5ffb0196c_25.png

这个实例展示了软件验证的一种常见模式:程序读取用户输入(或文件),经过一系列运算后与预期值进行比较。逆向工程的任务就是解读这些运算和比较的逻辑。掌握这种方法,是理解和应对更复杂软件保护机制的基础。

096:Keypatch 插件使用与程序脱壳 🛠️

在本节课中,我们将学习如何修改程序逻辑以绕过文件依赖,并介绍如何为加壳程序进行脱壳分析。我们将使用 IDA 的 Keypatch 插件来直接修改二进制指令,并利用 UPX 工具处理常见的压缩壳。


Keypatch 插件安装与使用

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/2d66a38acc9ed3e48ee9d116243d137c_1.png

上一节我们分析了程序对 kf.data 文件的依赖逻辑。本节中,我们来看看如何通过修改程序本身来移除这种依赖。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/2d66a38acc9ed3e48ee9d116243d137c_3.png

Keypatch 是一个用于给程序打补丁的 IDA 插件。以下是安装步骤:

  1. 在 Keypatch 的项目仓库中找到 keypatch.py 文件。

  2. 将该文件拷贝到 IDA 安装目录的 plugins 文件夹下。

  3. 重新启动 IDA,插件即可生效。

插件安装完成后,即可开始使用。我们将演示如何修改程序,使其无需 kf.data 文件也能运行到期望的代码段。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/2d66a38acc9ed3e48ee9d116243d137c_1.png

首先关闭当前分析窗口。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/2d66a38acc9ed3e48ee9d116243d137c_3.png

我们为程序添加一个副本。现在,程序在没有 kf.data 文件的情况下直接运行,这是我们希望改变的初始状态。

我们的目标是修改程序,让它不读取 kf.data 文件也能执行到最终的成功逻辑。根据之前的分析,我们知道第一步会检查文件是否存在,并通过 jnz 指令决定跳转方向。

如果我们将 jnz(非零则跳转)改为 jz(为零则跳转),就能逆转判定条件。原本跳转到左侧失败分支的路径,就会转向右侧的成功路径。

以下是修改单条指令的步骤:

  1. 右键点击目标 jnz 指令。

  2. 在右键菜单中选择 Keypatch -> Patch

  3. 在弹出的对话框中,将 jnz 修改为 jz

  4. 点击 Patch。插件可能会询问是否修改后续指令,选择取消,我们仅修改当前这一条。

修改后运行测试,发现并未成功。这是因为修改仅作用于 IDA 的数据库,并未实际保存到程序文件中。我们需要完成所有修改后统一保存。

同理,程序中还有其他几处依赖文件内容的判定需要修改。例如,将 jl(小于则跳转)改为 jge(大于等于则跳转),这可以通过在指令前添加 n(否定)前缀来实现。

以下是需要修改的关键指令列表:

  • 将检查文件是否存在的 jnz 跳转改为 jz

  • 将检查文件内容长度的 jl 跳转改为 jge

  • 将验证内容是否有效的条件跳转进行反向修改。

每次修改时,在弹出提示中只应用当前指令的修改,不修改后续指令。

所有指令修改完毕后,需要将更改保存到程序文件。在 IDA 菜单栏选择 Edit -> Patch program -> Apply patches to input file。此时可以创建原始程序的备份。

现在,我们得到了修改后的程序。运行新程序,尽管没有 kf.data 文件,它也不会输出 “out of date”,这说明我们的修改生效了。如果未能到达最终成功提示,说明仍有判定点需要修改,可以动态调试并重复上述步骤进行修正。

最终,我们得到了一个无需任何外部条件即可直接运行到成功逻辑的修改版程序。

综上所述,我们掌握了两种绕过验证的方法:一是分析验证逻辑并构造符合条件的数据文件;二是直接使用 Keypatch 插件修改程序二进制代码。


程序加壳与脱壳分析

在 IDA 分析中,常会遇到加壳的程序。加壳是指在原始可执行文件(EXE)外包裹一层代码,对内部代码进行压缩或加密。程序执行时,由壳代码将原始程序解密并加载到内存中运行,这增加了直接静态分析的难度。

最常用的压缩壳之一是 UPX。下面我们通过一个实例来演示脱壳过程。

这是一个来自 CTF 比赛的题目。首先用查壳工具检查,发现它是 64 位文件,但文件类型显示为 unknown

在区段信息中,我们注意到多个包含 UPX 字样的区段,这表明它使用了 UPX 压缩壳。如果直接用 IDA 分析加壳后的文件,主逻辑看起来非常简单,通常只调用一个复杂的解密函数,分析将难以进行。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/2d66a38acc9ed3e48ee9d116243d137c_5.png

我们需要先使用 UPX 脱壳工具进行脱壳。请确保 upx 命令已添加到系统环境变量中。

在命令行中,使用以下命令进行脱壳:

upx -d [加壳文件] -o [脱壳输出文件]

例如:

upx -d packed.exe -o unpacked.exe

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/2d66a38acc9ed3e48ee9d116243d137c_5.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/2d66a38acc9ed3e48ee9d116243d137c_7.png

脱壳完成后,使用 IDA 打开生成的 unpacked.exe 文件。此时,程序逻辑变得清晰可读,可以正常进行逆向分析,并找到类似 flag 的关键字符串。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/2d66a38acc9ed3e48ee9d116243d137c_9.png

这是一个简单的脱壳示例。掌握脱壳是分析此类保护程序的前提。详细的步骤也已包含在课程资料中。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/2d66a38acc9ed3e48ee9d116243d137c_7.png


课程总结 🎯

本节课中,我们一起学习了两个重要的逆向工程技能:

  1. 使用 Keypatch 插件修改程序:通过直接修改二进制指令(如将 jnz 改为 jz),我们能够改变程序执行流程,绕过特定的条件检查。

  2. 识别与脱去 UPX 压缩壳:我们了解了程序加壳的概念,并实践了使用 upx -d 命令对常见的 UPX 壳进行脱壳,为后续的静态分析扫清障碍。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/2d66a38acc9ed3e48ee9d116243d137c_9.png

掌握这些技能,能够帮助你更灵活地处理和分析各类经过保护或修改的二进制程序。

097:Ollydbg 介绍及实战 🛠️

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_1.png

在本节课中,我们将学习一款强大的 Windows 用户级动态调试工具——Ollydbg(简称 OD)。我们将了解其基本界面与功能,并通过一个实战案例,学习如何使用它来分析和破解一个简单的软件注册验证逻辑。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_3.png


Ollydbg 界面与基本功能

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_5.png

上一节我们提到了 Ollydbg 是一款动态调试利器。本节中,我们来看看它的主要界面窗口和基本操作。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_1.png

打开 Ollydbg 后,可以通过菜单栏的 File -> Open 加载一个可执行文件进行分析。其主界面主要包含以下几个关键窗口:

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_7.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_3.png

以下是各窗口功能的简要说明:

  • 反汇编窗口:显示程序的机器码和对应的汇编指令。这是分析代码逻辑的核心区域。

  • 寄存器窗口:实时显示 CPU 各寄存器的值。在分析时,我们经常需要查看和修改寄存器的值来改变程序执行流程。

  • 数据窗口:用于查看和跟踪特定内存地址中的数据。

  • 堆栈窗口:显示当前的堆栈信息。函数调用、参数传递等都依赖于堆栈。

在反汇编窗口中,我们可以进行以下关键操作:

  • F2 键或双击地址左侧区域,可以设置或取消断点

  • 双击汇编指令或按空格键,可以修改该处的汇编代码

  • 在注释栏可以为指令添加说明,这有助于分析,但不会影响程序执行。

寄存器窗口和数据窗口中的值也支持直接修改,这为我们动态调试和测试猜想提供了便利。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_5.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_9.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_10.png


常用快捷键与基本设置

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_12.png

熟悉了界面后,高效的调试离不开快捷键。Ollydbg 的快捷键逻辑与许多调试器类似。

以下是几个最常用的快捷键:

  • F2:在当前位置设置或取消断点。

  • F7:单步步入(Step Into)。遇到函数调用时会进入函数内部。

  • F8:单步步过(Step Over)。遇到函数调用时,将整个函数作为一步执行。

  • F9:运行程序,直到遇到断点或程序结束。

关于设置,通常需要在 Options -> Interface 中指定 UDD(用户数据)和插件的目录,以便保存调试数据和加载功能插件。


实战:破解 CrackMe 程序

理论学习之后,我们通过一个名为 “CrackMe95” 的实战程序来应用所学知识。我们的目标是分析其注册验证逻辑。

程序行为观察

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_14.png

首先运行目标程序 CrackMe95.exe

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_16.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_9.png

程序界面要求输入 Name(用户名)和 Registration Code(注册码)。我们随意输入,例如 Name: 1234, Code: 1111,点击 OK

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_10.png

程序会弹窗提示 “Sorry, this is an invalid registration code.”。这表明程序对我们的输入进行了验证。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_18.png

定位关键字符串

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_20.png

我们的思路是,在程序中搜索这个错误提示字符串,从而定位到验证逻辑所在的代码区域。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_22.png

  1. 用 Ollydbg 载入 CrackMe95.exe

  2. 确保程序已运行:载入后,当前模块可能显示为系统模块(如 ntdll)。按 F9 让程序运行起来,使其进入 CrackMe95 自身的代码空间。

  3. 在反汇编窗口点击右键,选择 Search for -> All referenced text strings

  4. 在弹出的字符串列表窗口中再次点击右键,选择 Search for text,输入 “Sorry”。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_24.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_12.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_26.png

  1. 找到字符串后,双击它,OD 会跳转到引用该字符串的代码位置。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_28.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_14.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_30.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_32.png

分析验证逻辑

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_34.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_35.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_36.png

我们定位到了显示错误信息的代码块。可以看到,在弹窗函数 MessageBoxA 之前,有两个条件跳转指令(JE)。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_37.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_39.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_16.png

程序逻辑是:如果这两个跳转条件任何一个成立,程序就会跳走,从而跳过错误弹窗。只有两个条件都不成立时,才会顺序执行到弹窗代码。因此,这两个跳转是验证是否成功的关键。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_41.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_43.png

向上查看代码,找到决定跳转的条件:

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_18.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_45.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_46.png

  • 第一个跳转:取决于 ECXEDX 两个寄存器的值是否相等。

    CMP ECX, EDX
    JE ... (跳转到成功流程)
    
  • 第二个跳转:取决于 EDX 是否等于一个固定值 0x361DECA

    CMP EDX, 361DECA
    JE ... (跳转到成功流程)
    

由此可知,ECXEDX 寄存器中存储的值是验证的核心。ECX 很可能是程序根据我们输入的 Name 计算出的“正确注册码”,而 EDX 是我们输入的“待验证注册码”。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_48.png

方法一:修改程序逻辑(爆破)

这是一种直接的方法:我们不让程序进行复杂的比较,而是直接修改指令,强制让它跳转到成功分支。

  1. 在第一个跳转指令 JE 处按 F2 下断点。

  2. F9 运行程序,在弹出的程序窗口中随意输入 NameCode,点击 OK

  3. 程序会在断点处暂停。观察寄存器窗口,此时 ECXEDX 的值通常不相等,所以第一个跳转不会发生。

  4. 双击这个 JE 指令,将其修改为 JNE(不相等则跳转),或者直接改为无条件跳转 JMP

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_30.png

  1. F9 继续运行。程序将跳转到成功流程,弹出注册成功的提示。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_48.png

保存修改:在菜单栏选择 File -> Save file,可以将修改后的程序保存到新文件。

方法二:分析算法,获取真码

这种方法旨在弄清程序计算正确注册码的算法。

  1. 重新载入原始程序,在验证代码段下断点。

  2. 运行程序,输入一个特定的用户名进行测试,例如 Name: findme123, Code: 123456

  3. 程序断下后,我们跟踪数据流。在内存中搜索字符串 “findme123”,并在所有引用它的代码上下断点。

  4. 重新运行并跟踪,会发现程序调用函数获取用户名后,进入一段计算代码。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_22.png

分析这段计算代码(课件中已提取关键部分):

  • 程序读取用户名的第一个字符(‘f’ 的 ASCII 码)。

  • 对该值进行一系列乘法、移位等运算。

  • 最终结果存放在 ECX 寄存器中,这就是针对用户名 findme123 计算出的正确注册码(十六进制)。

  • 程序随后比较 ECX(计算值)和 EDX(输入值)。

  1. 当程序运行到比较指令时,从寄存器窗口直接读取 ECX 的值。例如,得到 0x1150CC

  2. 使用计算器,将其转换为十进制:1134796

  3. 重新运行程序,输入 Name: findme123, Code: 1134796,即可注册成功。

额外发现

  • 该程序只使用了用户名的第一个字符进行计算,因此只要首字母相同,注册码就相同。

  • 第二个比较 (EDX == 0x361DECA) 是一个后门。无论用户名是什么,只要输入十进制 567456740x361DECA 的十进制),都能注册成功。


逆向工程分析建议 📝

在本节课的最后,我们总结一些逆向分析的通用建议:

  • 具备编码基础:了解常见的程序结构和编码风格,对逆向分析大有裨益。

  • 代码与数据分离:程序通常将代码段和数据段分开存放,分析时注意区分。

  • 善用代码复用:开发者会复用代码或使用开源库。遇到似曾相识的代码块,可以尝试在互联网或代码库中搜索。

  • 大胆假设,小心验证:逆向没有固定公式,常需要根据经验猜测程序逻辑,然后通过调试去验证猜测。

  • 区分自有代码与库函数:集中精力分析程序自身的业务逻辑,对于系统或第三方库函数,了解其功能即可,不必深入其内部。

  • 保持耐心:逆向分析是一个细致且耗时的过程,需要耐心和坚持。


https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/3001de5c2bcd4a9706b063267f8acb22_50.png

本节课中,我们一起学习了 Ollydbg 调试工具的基本使用方法,并通过一个 CrackMe 程序的实战,掌握了两种基本的软件逆向分析思路:修改程序逻辑分析验证算法。希望大家通过实验进一步巩固这些技能。

098:CTF-PWN-栈溢出-栈环境与参数寄存器 🧠💻

在本节课中,我们将要学习CTF-PWN中栈溢出的核心概念,包括栈环境的基本结构和函数调用时参数传递的规则。理解这些基础知识是后续进行漏洞利用的前提。

栈溢出概念介绍

上一节我们介绍了PWN的基本含义,本节中我们来看看“溢出”的具体概念。

我们所说的PWN,在CTF比赛中特指溢出类题目,主要考察对漏洞的利用能力。溢出是指:程序为输入数据或参数分配了一段固定空间,但实际提供的数据量超过了这个空间。例如,一个缓冲区本应在0x80(十六进制)的空间内活动,但输入数据过多,就会侵占相邻的内存空间。

在栈结构中,如果输入数据过多,超出了为其分配的缓冲区范围,就可能覆盖下方的栈地址,例如函数返回地址。这通常会导致程序报错。但如果精心构造输入数据,使超出的部分包含一个我们预先设定好的地址,那么程序在返回时,就会跳转到我们指定的位置执行代码,这就实现了漏洞利用。PWN题目主要就是考察这类溢出漏洞的利用。

栈环境与关键寄存器

理解了溢出的基本概念后,我们来看看实现溢出的关键战场——栈环境。

栈环境是为每个函数调用维护的一块内存空间,用于存放函数的局部变量和临时数据。函数执行完毕后,其栈空间会被清除。在64位系统中,主要由两个寄存器维护栈空间:

  • RBP:栈基址指针(Base Pointer),指向当前栈帧的底部。

  • RSP:栈顶指针(Stack Pointer),指向当前栈帧的顶部。

在32位系统中,对应的寄存器是EBPESP

此外,还有一个至关重要的寄存器:

  • RIP:指令指针寄存器,存放着下一条将要执行的指令的地址。

进行栈溢出利用的核心目标,就是通过覆盖栈上的数据,最终改变RIP寄存器的值,使其指向我们想要执行的指令,而非程序原定的指令。

函数参数传递规则

在掌握了栈和寄存器的基本作用后,我们来看看函数调用时参数是如何传递的,这对于构造利用链至关重要。

函数调用时,参数传递的规则因系统架构而异。以下是64位Linux系统下的常见规则:

以下是参数传递顺序:

  1. 前六个参数依次放入以下寄存器:RDI, RSI, RDX, RCX, R8, R9

  2. 如果参数超过六个,从第七个开始,将被压入栈中传递。

以一个常见的read函数为例,其原型为read(int fd, void *buf, size_t count)

  • 第一个参数fd(文件描述符,如0代表标准输入)放入RDI

  • 第二个参数buf(存储读取数据的缓冲区地址)放入RSI

  • 第三个参数count(要读取的字节数,如0x80)放入RDX

而在32位系统中,所有参数通常都通过栈来传递。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/abea4eb40c1dfe285e48a7360d92e663_1.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/abea4eb40c1dfe285e48a7360d92e663_1.png

(图示:标准输入、输出、错误流的关系)

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/abea4eb40c1dfe285e48a7360d92e663_3.png

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/abea4eb40c1dfe285e48a7360d92e663_3.png

(图示:函数调用时栈与寄存器的状态示例)

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/abea4eb40c1dfe285e48a7360d92e663_5.png

(图示:参数在寄存器和栈中的传递示意)


https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/abea4eb40c1dfe285e48a7360d92e663_5.png

本节课中我们一起学习了栈溢出的基本原理、维护栈环境的关键寄存器(RBP/RSP/RIP)以及64位与32位系统下函数参数传递的不同规则。理解这些概念是分析漏洞和构造利用载荷的基础。在接下来的课程中,我们将运用这些知识,结合工具进行实际操作。

099:栈溢出-汇编指令及字节序 🧠

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_1.png

在本节课中,我们将要学习栈溢出相关的核心汇编指令,以及一个关键概念——字节序。理解这些基础知识是分析栈溢出漏洞和构造利用载荷(Payload)的前提。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_3.png

概述

栈溢出主要涉及栈的操作和函数调用。为了理解溢出过程,我们需要先掌握几个关键的汇编指令和计算机存储数据的方式。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_5.png

常见的汇编指令

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_7.png

上一节我们介绍了课程背景,本节中我们来看看与栈操作相关的核心汇编指令。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_9.png

栈的结构

在深入指令之前,我们需要了解栈的基本结构。栈是一段连续的内存区域,用于存储临时数据。栈顶由栈指针寄存器(32位为esp,64位为rsp)指向。栈的增长方向是从高地址向低地址。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_11.png

PUSH 指令

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_13.png

PUSH指令用于将数据压入栈中,即“入栈”。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_15.png

以下是PUSH指令的操作步骤:

  1. 首先,栈指针(esprsp)的值会减少。在32位系统中减少4字节,在64位系统中减少8字节。这相当于在栈上“开辟”了新的空间。

  2. 然后,将要压入的数据存入新的栈顶地址所指向的内存位置。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_17.png

用伪代码可以描述为:

// 以64位系统为例
rsp = rsp - 8;        // 栈顶指针上移(地址减小)
*(rsp) = data;        // 将数据存入新的栈顶

POP 指令

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_19.png

POP指令与PUSH相反,用于从栈中取出数据,即“出栈”。

以下是POP指令的操作步骤:

  1. 首先,将当前栈顶地址所指向的内存中的值,加载到指定的寄存器中(例如rdi)。

  2. 然后,栈指针(esprsp)的值会增加(32位加4,64位加8)。这相当于“释放”已取出的栈空间。

用伪代码可以描述为:

// 以64位系统为例
rdi = *(rsp);         // 将栈顶的值存入rdi寄存器
rsp = rsp + 8;        // 栈顶指针下移(地址增加)

CALL 指令

CALL指令用于调用一个函数。它的内部操作可以分解为几个步骤。

以下是CALL指令执行的操作:

  1. 将下一条指令的地址(即rip寄存器的当前值)压入栈中。这个地址被称为“返回地址”,是函数执行完毕后应该返回的地方。

  2. 跳转(jmp)到被调用函数的起始地址开始执行。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_21.png

当被调用的函数执行完毕,使用RET指令返回时,它会从栈顶弹出之前保存的返回地址,并跳转到该地址,从而继续执行CALL之后的指令。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_23.png

RET 指令

RET指令用于从函数调用中返回。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_25.png

RET指令执行的操作是:

  1. 从当前栈顶弹出一个值,这个值就是之前CALL指令保存的返回地址。

  2. 跳转到这个返回地址,继续执行。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_27.png

它相当于执行了POP rip。需要注意的是,RET指令本身通常不负责清理栈上的局部变量等数据,这部分清理工作通常由被调用函数在返回前完成(例如通过add rsp, X指令)。

字节序

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_29.png

上一节我们介绍了函数调用的过程,本节中我们来看看数据在内存中是如何存储的,这涉及到字节序的概念。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_31.png

在计算机领域,字节序(Endianness)指的是多字节数据(如整数、地址)在内存中或网络传输时的字节排列顺序。主要分为大端序和小端序。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_33.png

大端序

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_35.png

大端序将数据的高位字节存储在内存的低地址处,将低位字节存储在内存的高地址处。

例如,对于十六进制数据 0xdeadbeef(共4个字节:de, ad, be, ef),在大端序系统中的内存布局(从左到右地址递增)为:

地址:  低  ---->  高
数据: de    ad    be    ef

这种存储方式比较符合人类的阅读习惯。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_37.png

小端序

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_39.png

小端序与大端序正好相反,将数据的低位字节存储在内存的低地址处,将高位字节存储在内存的高地址处。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_41.png

同样对于数据 0xdeadbeef,在小端序系统中的内存布局为:

地址:  低  ---->  高
数据: ef    be    ad    de

这种存储方式看起来与我们的书写习惯相反。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_43.png

注意:在x86/x86-64架构的个人计算机和服务器上,普遍采用小端序。因此,在后续构造栈溢出利用的Payload时,我们写入的地址(如返回地址、函数地址)必须按照小端序的格式排列,否则程序会错误地解释这些数据。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_45.png

总结

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_47.png

本节课中我们一起学习了栈溢出相关的核心知识:

  1. 我们掌握了PUSHPOPCALLRET这几个关键汇编指令的工作原理,理解了函数调用和返回时栈的变化过程。

  2. 我们了解了字节序的概念,区分了大端序和小端序,并明确了在常见的x86/x64环境下使用的是小端序。这是后续正确构造攻击载荷的关键。

https://github.com/OpenDocCN/sec-notes-zh/raw/master/docs/hetian/img/d367e9412c96449f661fd38267e0d9c4_49.png

理解这些底层机制,是分析软件漏洞、进行二进制安全研究的重要基础。

Logo

AtomGit AI 社区提供模型库、数据集、Agent、Token等资源

更多推荐