作业 5:来一点汇编#

本次作业有两个部分。第一部分是一个 ATM 取款程序,需要用到你的 C 和汇编技能来查找一些漏洞,并演示如何利用这些漏洞做一些“违法”操作。第二部分是一个二进制炸弹程序,不提供 C 代码!你将使用汇编和逆向工程技能,拆除该程序中的“炸弹”。希望你乐于探索,并能够解决这些 C 和汇编“难题”!

为了帮助你评估学习进度,对于每个作业/实验,我们罗列了一些要点,并提供了一些思考问题。在完成作业后,可以使用这些问题进行自我检查。如果你不能很好地回答这些问题,那么还需要进一步努力。

  • 哪些 gdb 命令允许在执行程序中修改执行路径?

  • 汇编代码包含循环操作的标志是什么?

  • 解释函数返回值与返回地址之间的区别。

  • 研究函数指针在汇编级别的工作机制,看看与普通函数调用相比,通过函数指针进行调用有什么异同?

  • 出于性能原因,编译器倾向于尽可能将局部变量存储在寄存器中,然而某些情况下,编译器不得不将局部变量存储在栈上。这是什么情况?

  • 对于下面的指令序列,op1op2 的值必须是什么时,才会进行跳转?如果用 ja 代替 jg 会有什么变化?

    cmp op1,op2 
    jg target
    

初始项目#

你的个人用户目录下应该已经有 cs102 这个文件夹了,通过下面的命令拷贝初始代码到该目录中:

cp -r /home/cs102-shared/assignments/assign5 ~/cs102

配置调试器#

本次作业严重依赖 gdb 的使用,为了获得更好的体验,建议按如下命令配置 gdb,该命令设置了一些全局选项用于课程的教学目的。

cp /home/cs102-shared/.gdbinit ~/.gdbinit

作业初始项目中还提供了另一份 .gdbinit 文件,可以用于设置 gdb 的启动任务。在二进制炸弹中,这将非常有用。

任务 1:ATM 取款漏洞#

程序 atm 简单地模拟了银行取款操作,通过指定帐户和余额来调用该程序。如果账户已有授权并且账户有足够的资金,则该金额将被提取。未经授权的用户以及超额取款都是不允许的。

运行 ./atm 40 $USER,将从你的用户名关联的帐户中提取 40 美元。每次运行该程序,终端都会打印相关的事务或可能的错误信息。例如,如果你打算从帐户中提取 100 美元,则程序将拒绝该请求并显示错误消息,因为这将使你的当前余额低于所需的最低余额。如果尝试从另一个帐户中窃取现金,例如 ./atm 40 xuehao 或不存在的用户,你的请求将会因为未经授权而被拒绝。目前为止,一切似乎都很好;ATM 机可以正常工作。注意:每次重新运行该程序时,所有余额都会恢复到原来的值(默认 102 美元)。文件 samples/bank/customers.db 包含所有有效用户及其余额的信息。

该银行最近将 ATM 软件更新了一些附加功能。IT 团队审查了新的代码,没有发现特别的问题;但将其安装到生产环境中后,他们观察到了一些可疑的活动。因为你出色的 C 和汇编技能,银行现在打电话给你,需要你来调查并解决这些问题!

你的第一个任务是查看 atm.c 的源代码。该程序大约有 150 行 C 代码,其复杂性和你在本课程中编写的代码类似。尽管代码缺少了注释,但经过合理的分解,该程序仍然具有一定的可读性。你应该发现该程序的设计似乎很合理,并且代码也确实可以正确运行。阅读完后,请花一点时间反思一下,为了理解该程序,用到了你的哪些核心的 C 技能!

通过跟踪程序输出和余额,银行发现了三个运行异常情况,需要你的协助和调查。以下是你的任务:

  • 对于下面的每个漏洞,构建一个测试用例来展示如何利用它,并将其添加到 custom_tests 文件中。请注意,触发漏洞的方法可能不止一种。

  • readme.txt 中,你还应该为每个漏洞提供一份简明的描述,并解释如何构建测试用例来利用它,以及修复该漏洞的建议。

请注意,解决每个漏洞可能不止一种修复措施。银行并不打算对程序作较大的改动,因此在你提议中,你应该尝试直接解决该漏洞,并尽量减少对其他部分的干扰。修复的同时还要确保不会删除该程序的预期功能,并且修复后不会引入任何潜在的其他安全问题。以下是你必须提供的攻击列表:

  • 情况 A:以你自己的身份提款,提款金额多于你账户中的金额

  • 案例 B:从 xuehao 的账户中提取 40 美元

  • 案例 C:尽管金库账户 VAULT 的密码已禁用,但仍可以从该金库提取 300 美元

情况 A:负余额#

旧版本的 ATM 程序允许客户将其帐户提取至 0 美元,但不能再进一步提取。新程序更改了 withdraw 函数,要求最低余额非零。预期的行为应该是,所有帐户余额都保持在最小值以上。然而,银行发现一笔原本很普通的提款交易,不仅导致账户低于最低限额,还出现了透支,最终出现负余额。这是绝对不应该发生的!

查看函数 withdraw 的 C 代码,特别是对旧版本的更改。该函数似乎在很多情况下都是有效的,但显然并非全部。仔细阅读此函数,尝试发现该漏洞——你对有符号和无符号整型的理解在这里将大显身手!发现该漏洞后,需要你确定一条命令,该命令可以从你的账户中提取出超过总额的金额

情况 B:未经授权的帐户访问#

该银行还收到客户关于未经授权从其账户提款的投诉。似乎有用户能够破解授权限制,成功从受害客户的帐户中提取资金。此外,其使用的凭据似乎是假冒的——在数据库中查不到这样的用户!任何用户都应该无权访问其他帐户,尤其是不能通过提供虚假的凭证来访问!

查看 find_account 函数的 C 代码,该函数负责将提供的用户名与其帐号进行匹配。该函数似乎只适用于认证帐户,无效用户名则不适用。你能看出这个函数在这种情况下的行为吗?一旦用户名无效,这个函数的行为似乎不可预测。

你的下一个任务是检查生成的汇编代码,以确定该函数的行为。需要特别关注一些特殊寄存器,以及影响其值的指令。发现漏洞后,需要你确定一条命令,使用设计好的假冒用户名,从 xuehao 账户(即 account #7)中提取 40 美元

$ ./atm 40 虚假用户名
Please take your cash: $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
[Transaction Log: Presented credential as 虚假用户名, account #7, withdrew $40, new balance = $62]

情况 C:访问银行金库账户#

最令人担忧的问题是银行金库账户(即 account #0)发生了多次非法提款。金库账户的名称不是实际用户,因此无法简单地直接通过用户名访问该账户。相反,为了增强安全性,用户必须指定两个参数:账户 ID 及其密码。尝试运行 ./atm 40 0 314628 使用账户 ID 和密码 314628 测试金库账户。

$ ./atm 40 0 314628
Please take your cash: $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
[Transaction Log: Presented credential as 0, account #0, withdrew $40, new balance = $4999960]

起初,银行认为金库密码可能发生泄露,但是更改密码后并没有阻止攻击行为。无奈之下,银行删除了所有金库的密码文件,认为这样可以禁止对金库的所有访问,但流氓用户却继续非法提款操作!

由此可以推断,高安全性的密码认证系统可能存在自身的安全漏洞!处理密码认证的代码位于 lookup_by_numberread_secret_passcode 两个函数中。这些函数在大多数情况下都能正常工作,但在某些边界情况下会失败。有时虽然缺少密码文件,但提供的凭据似乎仍会被程序接受。

该漏洞在 C 代码中很微妙,因此你还是应该使用 GDB 在汇编层面检查代码,并画出这些函数在内存中的调用栈。导致该安全漏洞的可能原因是涉及到多个数据的内存布局,以及访问栈变量时考虑不周。一旦发现该漏洞后,需要你确定一条命令,该命令可以在密码禁用的状态下,从银行金库账户提取 300 美元

如果你很喜欢类似的破解漏洞任务,那么像 CS155 这样的计算机安全类课程,你一定也非常感兴趣!

任务 2:二进制炸弹#

邪恶的 Dr. Evil 攻击了我们的课程服务器,并植入了一些称之为“二进制炸弹”的神秘可执行文件。我们观察到程序似乎由一系列阶段组成。每个阶段都要求用户输入一个字符串。如果用户输入正确的字符串,关卡将被化解,程序进入下一个阶段。但是,如果输入错误,炸弹会被引爆,程序将会打印“BOOM!!!”消息并终止。如果想要拆除整个炸弹,需要成功化解每个阶段的关卡。

你出色的汇编技能将会帮助我们解决这个问题。对于这个可执行炸弹文件,需要你利用逆向工程方法,构建出原始 C 代码的逻辑。但要注意的是,我们的目标并不是还原整个 C 代码,而是找出一个正确的输入来化解关卡。这需要你按照代码的路径进行非常详细的探索,路径之外的代码也需要你进行一番调查。一旦了解了炸弹的引爆逻辑并确认出所需的输入,你就可以拆除它。关卡的难度将会越来越难,通过化解这些难题,你的专业知识也将逐步增加。

逆向工程需要综合多种不同的方法和技术,你将有机会使用各种工具进行练习,其中最重要的就是 GDB!用好 GDB 将会为你的职业生涯带来极大的回报!

完成作业后,在 input.txt 文件中添加密码,用于破解二进制炸弹的每个阶段。需要注意添加的格式,每一行表示一个级别,并以标准 Unix 换行符(\n)结尾。

在其他平台上编辑时,macOS 可能会以 \r 结束行,Windows 可能会以 \r\n 结束行,这些约定可能会造成一些困扰。按课程推荐,使用 VS Code 编辑 input.txt 文件,大概率不会发生问题。

前期调查#

我们的调查工作已经确认了炸弹工作原理的一些事实:

  • 如果不提供命令行参数直接启动 ./bomb,炸弹会通过终端键入的方式读取输入。

  • 如果提供命令行参数,比如 ./bomb input.txt,炸弹将从该文件中按行读取,直到到达 EOF 文件末尾,然后切换到终端继续读取。这个特性允许你将已解决的关卡字符串存储到 input.txt,避免每次重新输入。

  • 直接在终端或 gdb 中执行炸弹程序,可能会触发爆炸。但是 gdb 提供了可以拦截爆炸的工具,因此最安全的选择是尽可能在 gdb 下工作并采取必要的预防措施。

  • 炸弹文件是使用 gcc 从 C 代码编译而来的。但看起来,该程序在编译时并没有更改编译标志,以便对目标代码进行大量混淆。

  • 函数名在目标代码中是可见的,Dr. Evil 在编译时没有对这些名称作任何掩饰。因此,函数名 initialize_bombread_six_numbers 可以作为调查的线索。另外,程序似乎也使用了标准库函数,因此,如果遇到对 qsortsscanf 的调用,那就是真实的调用。

  • 直接修改可执行文件也能改变二进制炸弹的行为,但对于本次作业,这样的努力并没有多大用处。

  • 有一个重要的限制:不要使用暴力求解!你可以编写一个程序来尝试所有可能的输入,但这不仅非常麻烦,而且不太现实。作业的目的是为了训练你的汇编技能!

准备工作#

在拆解炸弹之前,我们建议使用下述工具和 gdb 技巧先弄清楚如何可靠地避免爆炸。可以使用任何技术,例如利用 gdb 功能、调整全局程序的状态、修改某些设置、引导炸弹以安全的方式运行等等。一旦弄清楚如何避免爆炸,你就可以放心地探索各个关卡。请注意,炸弹只有在“活动”时才会爆炸,即在终端或 gdb 中执行时。在使用 gdbnmstringsobjdump 等工具检查可执行文件时,炸弹不会发生爆炸。

  • 在可执行文件上使用 nm 工具(nm bomb)可以打印出可执行文件的“符号表”。符号表包含了函数和全局变量的名称及其地址。这些名字也许能让你对炸弹的结构有所了解。

  • 在可执行文件上使用 strings 工具(strings bomb)可以打印出可执行文件中包含的所有可打印字符串,包括字符串常量。调查下这些字符串是否与拆除炸弹有关。

  • 此后 gdbobjdump 将是最有用的工具。使用 objdump -d bomb 可以打印出可执行文件的汇编代码。阅读并跟踪反汇编代码是获取大部分信息的途径。在不执行程序的情况下,检查这些目标代码是一种称为 deadlisting 的技术。一旦你理清了目标代码的工作过程,你其实可以将其翻译回 C,然后再推断期望的输入。对于简单的代码,这个方法效果非常好,而当代码变得复杂时,这个方法就相对笨拙了。此时就是 gdb 发挥作用的地方。

  • gdb 允许你单步执行汇编指令、检查并更改内存和寄存器、查看运行时栈、反汇编目标代码、设置断点等等。对执行中的炸弹进行现场实验是熟悉汇编级程序的最直接方法。

  • 使用 Compiler Explorergcc 等工具,可以验证任何 C 代码的汇编形式。如果不确定某些转换,例如如何访问某种特殊数据结构,break 在汇编中如何工作,或者 qsort 如何调用函数指针等等,你就可以编写一个测试用的 C 代码,并追踪其汇编形式。

GDB 技巧#

调试器对于本次作业来说绝对是无价的。历次实验中也提供了大量的 gdb 练习。如何最大限度地利用 gdb,下面给出了一些额外的建议。

  • 扩展你的 gdb 知识库。

    历次实验中,已经介绍了一些方便的命令,例如 breakxprintinfodisassemblestepi/nexti。以下是补充一些其他命令,同样非常有用的:displayset variablewatchjumpkill、和 return。在 gdb 中,你可以使用 help name-of-command 来获取 gdb 命令的帮助信息。了解更多 gdb 命令,可以参阅 gdb reference card

  • 灵活使用断点。

    你可以将函数名、源码行、某个指令的地址设置为断点。使用 commands 可以指定一个命令列表,每当遇到给定的断点时就会自动执行。这些命令可能用于打印变量、收回调用栈、跳转到不同的指令、更改内存中的值、从函数提前返回等等。断点的 commands 命令非常强大,可以定制一些期望的操作,在到达代码中的某个位置时,自动且无误地完成这些操作。

  • 使用 .gdbinit 文件

    作业目录中提供的名为 .gdbinit 的文件可用于配置 gdb 的启动。在这个文件中,你可以输入一系列命令,和你在 gdb 中键入的命令完全相同。启动后,gdb 将自动执行这些命令。

    这个配置文件非常方便,可以在每次启动调试器时固定执行某些 gdb 命令。特别是在调试炸弹时,可以创建一些固定的断点位置,避免重复输入。

    作业默认提供的 .gdbinit 文件只有一个命令,用于输出“Successfully executing commands from .gdbinit in current directory”消息。如果在启动 gdb 时看了这个消息,则表明 .gdbinit 文件已经加载。

  • 自定义 gdb 命令

    使用 define 可以将经常使用的命令定义为较为简短的 gdb “宏”,并添加到 .gdbinit 文件中。在后续 gdb 会话中,可以直接使用。

  • 启动 tui 模式

    使用 layout asmlayout reg 命令,可以分别打开汇编窗口和寄存器窗口。窗口布局将分为三个部分,上面显示寄存器的值,中间显示汇编指令序列,下面用于输入 gdb 命令。

    当以单指令 si 步进执行时,寄存器窗口会自动更新值,并高亮显示刚刚更新过的寄存器。汇编窗口将跟随控制流,更新到下一条指令。

    该布局可以方便地查看程序在机器级别的行为。但遗憾的是,tui 模式并不稳定,偶尔会出现一些错误。如果发生显示错误,可以尝试使用 refresh 命令进行刷新。退出 tui 模式可以使用 tui disable 命令。

测试与提交#

默认的 sanitycheck 测试用例是一个 ATM 输入和一个报告 input.txt 文件行数的测试用例。另外,custom_tests 文件仅用于 ATM 测试,二进制炸弹不需要进行 sanitycheck 测试。

作业提交方式参考作业 0,可以使用 submit 提交你的代码。