作业 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
提交你的代码。