作业 5:来一点汇编¶
本次作业有两个部分。第一部分是一个 ATM 取款程序,需要用到你的 C 和汇编技能来查找一些漏洞,并演示如何利用这些漏洞做一些“违法”操作。第二部分是一个二进制炸弹程序,不提供 C 代码!你将使用汇编和逆向工程技能,拆除该程序中的“炸弹”。希望你乐于探索,并能够解决这些 C 和汇编“难题”!
为了帮助你评估学习进度,对于每个作业/实验,我们罗列了一些要点,并提供了一些思考问题。在完成作业后,可以使用这些问题进行自我检查。如果你不能很好地回答这些问题,那么还需要进一步努力。
哪些
gdb命令允许在执行程序中修改执行路径?汇编代码包含循环操作的标志是什么?
解释函数返回值与返回地址之间的区别。
研究函数指针在汇编级别的工作机制,看看与普通函数调用相比,通过函数指针进行调用有什么异同?
出于性能原因,编译器倾向于尽可能将局部变量存储在寄存器中,然而某些情况下,编译器不得不将局部变量存储在栈上。这是什么情况?
对于下面的指令序列,
op1和op2的值必须是什么时,才会进行跳转?如果用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_number 和 read_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_bomb或read_six_numbers可以作为调查的线索。另外,程序似乎也使用了标准库函数,因此,如果遇到对qsort或sscanf的调用,那就是真实的调用。直接修改可执行文件也能改变二进制炸弹的行为,但对于本次作业,这样的努力并没有多大用处。
有一个重要的限制:不要使用暴力求解!你可以编写一个程序来尝试所有可能的输入,但这不仅非常麻烦,而且不太现实。作业的目的是为了训练你的汇编技能!
准备工作¶
在拆解炸弹之前,我们建议使用下述工具和 gdb 技巧先弄清楚如何可靠地避免爆炸。可以使用任何技术,例如利用 gdb 功能、调整全局程序的状态、修改某些设置、引导炸弹以安全的方式运行等等。一旦弄清楚如何避免爆炸,你就可以放心地探索各个关卡。请注意,炸弹只有在“活动”时才会爆炸,即在终端或 gdb 中执行时。在使用 gdb、 nm、strings、objdump 等工具检查可执行文件时,炸弹不会发生爆炸。
在可执行文件上使用
nm工具(nm bomb)可以打印出可执行文件的“符号表”。符号表包含了函数和全局变量的名称及其地址。这些名字也许能让你对炸弹的结构有所了解。在可执行文件上使用
strings工具(strings bomb)可以打印出可执行文件中包含的所有可打印字符串,包括字符串常量。调查下这些字符串是否与拆除炸弹有关。此后
gdb和objdump将是最有用的工具。使用objdump -d bomb可以打印出可执行文件的汇编代码。阅读并跟踪反汇编代码是获取大部分信息的途径。在不执行程序的情况下,检查这些目标代码是一种称为 deadlisting 的技术。一旦你理清了目标代码的工作过程,你其实可以将其翻译回 C,然后再推断期望的输入。对于简单的代码,这个方法效果非常好,而当代码变得复杂时,这个方法就相对笨拙了。此时就是gdb发挥作用的地方。gdb允许你单步执行汇编指令、检查并更改内存和寄存器、查看运行时栈、反汇编目标代码、设置断点等等。对执行中的炸弹进行现场实验是熟悉汇编级程序的最直接方法。使用 Compiler Explorer 或
gcc等工具,可以验证任何 C 代码的汇编形式。如果不确定某些转换,例如如何访问某种特殊数据结构,break在汇编中如何工作,或者qsort如何调用函数指针等等,你就可以编写一个测试用的 C 代码,并追踪其汇编形式。
GDB 技巧¶
调试器对于本次作业来说绝对是无价的。历次实验中也提供了大量的 gdb 练习。如何最大限度地利用 gdb,下面给出了一些额外的建议。
扩展你的
gdb知识库。历次实验中,已经介绍了一些方便的命令,例如
break、x、print、info、disassemble和stepi/nexti。以下是补充一些其他命令,同样非常有用的:display、set variable、watch、jump、kill、和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 asm和layout reg命令,可以分别打开汇编窗口和寄存器窗口。窗口布局将分为三个部分,上面显示寄存器的值,中间显示汇编指令序列,下面用于输入gdb命令。当以单指令
si步进执行时,寄存器窗口会自动更新值,并高亮显示刚刚更新过的寄存器。汇编窗口将跟随控制流,更新到下一条指令。该布局可以方便地查看程序在机器级别的行为。但遗憾的是,
tui模式并不稳定,偶尔会出现一些错误。如果发生显示错误,可以尝试使用refresh命令进行刷新。退出tui模式可以使用tui disable命令。
测试与提交¶
默认的 sanitycheck 测试用例是一个 ATM 输入和一个报告 input.txt 文件行数的测试用例。另外,custom_tests 文件仅用于 ATM 测试,二进制炸弹不需要进行 sanitycheck 测试。
作业提交方式参考作业 0,可以使用 submit 提交你的代码。