首先,将自己的实验包从Windows系统中使用scp命令传到Linux虚拟机中。而要想传到Linux虚拟机中,第一步就是要确定Linux虚拟机的IP地址,如 图1:确定Linux虚拟机的IP地址 所示。接着使用scp命令将实验包从Windows系统传送到Linux虚拟机中,如 图2:用scp命令将实验包从Windows系统传送到Linux虚拟机中 所示。接着在Linux虚拟机中,使用"tar xvf 你的实验包的名字"命令将压缩实验包解压缩,如 图3:使用"tar xvf 你的实验包的名字"命令将压缩实验包解压缩 所示。
(图1:确定Linux虚拟机的IP地址)
(图2:用scp命令将实验包从Windows系统传送到Linux虚拟机中)
(图3:使用"tar xvf 你的实验包的名字"命令将压缩实验包解压缩)
此时若查看README文件,会发现我们得不到任何有用的信息,即使去查看bomb.c文件中的几行注释,也得不到有价值的信息。那么我们该怎么办呢?不要慌张,根据西工大无论是计院还是网安院开设的计基实验课的源文件的出处,我们去这些源文件的源头,CSAPP的官网上查看到了关于这次实验的介绍,它的大致内容如 图4:了解第二次实验的大致内容 所示。当看到很多很多的专有名词时,不要害怕,有个印象就可以,后面会慢慢细讲。此时既然我们手头有了bomb.c的源文件,不妨先去分析分析,看看能得到什么有用的东西。对bomb.c文件的讲解如 图5:bomb.c文件前28行的内容,图6:bomb.c文件中第28行到第52行的内容,图7: bomb.c文件中第53行到第77行的内容,图8:bomb.c文件中第78行及其之后的内容 所示。
二进制炸弹是由一系列阶段组成的程序。每个阶段都希望您在stdin上键入一个特定的字符串。如果你键入了正确的字符串,那么这个阶段就会被解除引信,炸弹就会进入下一个阶段。否则,炸弹会通过打印“BOOM!!”然后终止而爆炸。当每一阶段都拆除炸弹之后,炸弹最终即会被自动拆除。
每次你的炸弹爆炸时,它都会通知实验室的服务器,你会在最终分数中损失1/2分(最多20分的分数)。所以引爆炸弹会有后果。你一定要小心!
有许多工具旨在帮助您了解程序是如何工作的,以及当他们不工作的时候哪里出了问题。以下是一些你可能会发现对分析炸弹有用的工具的列表,以及关于如何使用它们的提示:
1. gdb,是一个几乎在每个平台上都可用的命令行调试器工具。你可以逐行跟踪程序,检查内存和寄存器,查看这两个源代码和汇编代码(我们不会为您提供大部分炸弹的源代码),设置断点,设置内存监视点,并编写脚本。
2. objdump -t,这将打印出炸弹的符号表。符号表包括所有函数的名称和炸弹中的全局变量,炸弹调用的所有函数的名称及其地址。你通过查看函数名称可以学到一些东西!
3. objdump -d,用这个来分解炸弹中的所有代码。您也可以只查看单个函数。阅读汇编代码可以告诉你炸弹是如何工作的。
4. strings,此实用程序将显示炸弹中的可打印字符串。
(图4:了解第二次实验的大致内容)
(图5:bomb.c文件前28行的内容。 第1行到第21行都是注释,讲的都是不重要的内容;第23行到第26行使用了4个include语句,声明了4份头文件stdio.h,stdlib.h,support.h,phases.h。)
(图6:bomb.c文件中第28行到第52行的内容。 第28行到第32行是注释,讲的内容都不重要;第34行表示定义一个全局性的文件指针infile;第36行表示定义main函数,这里main函数有两个参数,第一个参数是int型的变量argc,第二个参数是指针数组argv,这个argv数组中的每一个元素都是一个指向字符串的指针。对于事先已由该bomb.c文件编译出的bomb文件而言,如果在命令行中使用"./bomb"命令执行bomb可执行文件,那么argc的值为1,表示argv指针数组中只有一个元素,argv[0]的值指向"bomb"这个字符串。如果在命令行中使用"./bomb 1.txt 2.txt 3.txt"命令执行bomb可执行文件,那么argc的值为4,表示argv指针数组中有4个元素,argv[0]的值指向"bomb"这个字符串,argv[1]的值指向"1.txt"这个字符串,argv[2]的值指向"2.txt"这个字符串,argv[3]的值指向"3.txt"这个字符串;回过头,第38行表示定义一个字符串指针input;第45、46行表示,如果argc的值为1,也就是在命令行窗口执行bomb文件时使用的命令是"./bomb",那么infile这个全局性的文件指针指向stdin这个文件,也就是指向标准输入文件。在这里我们在键盘上按下的字符都首先被存放在了stdin这个文件里。)
(图7: bomb.c文件中第53行到第77行的内容。第53到第57行表示,如果argc的值为2,也就是说,在命令行窗口中执行bomb.c可执行文件的命令是"./bomb 1.txt",那么看第54行,如果再以只读的方式打开argv[1]这个字符串指针所指的文件的时候打不开,也就是fopen不给infile返回一个指向文件的指针,那么infile就为空指针,那么就会提示你"... Error: Couldn't open... ",接着退出bomb文件的执行;第61行表示,如果在Linux虚拟机中执行bomb可执行文件的命令是"./bomb 1.txt 2.txt"甚至"./bomb 1.txt 2.txt 3.txt",那么bomb可执行文件不知道该使用哪个文件作为答案,只好输出"Usage: %s [<input_file>]"来委婉提示你,然后便会退出;接着第67行初始化炸弹,这一点不重要;第69、70行输出两句话,也不重要;第73行,"input = read_line();",表示从infile这个文件指针所指的文件中,读取1行到input这个字符串指针所指的字符串当中,如果我们在命令行中执行bomb可执行文件的命令是"./bomb",那么infile这个文件指针指向的就是stdin这个标准输入文件,也就是我们从键盘中的输入;第74行表示将input这个字符串指针所指的字符串交由phase_1函数处理,如果通过的话,会保持沉默并且默认你通过执行第75行;第75行的作用暂时还不清楚,但是并不重要;第77行输出一段恭喜你的话,表示你已经成功破解了第一阶段的炸弹。)
(图8:bomb.c文件中第78行及其之后的内容。 后面的几个阶段就是重复第一阶段的内容。)
事已至此,我们无法再从bomb.c文件中得到更多的消息。那么怎么才能破解这个炸弹呢?注意,虽然bomb.c文件已经无法告知我们更多的信息,但是bomb这个可执行文件就不一定了,但是bomb这个可执行文件用记事本打开的结果是一堆乱码,我们怎么能获取更多的消息呢?我们不妨对bomb文件进行反汇编处理,以得到控制生成bomb可执行文件的汇编代码,并期待从汇编代码中获取一些信息,如 图9:对bomb文件进行反汇编处理,以得到控制生成bomb可执行文件的汇编代码 所示。接着双击bomb.s文件以查看bomb.s文件的内容,如 图10:双击bomb.s文件以查看bomb.s文件的内容 所示。
(图9:对bomb文件进行反汇编处理,以得到控制生成bomb可执行文件的汇编代码。使用"objdump -d bomb > bomb.s"命令获取控制生成bomb可执行文件的汇编代码,并将其保存在bomb.s文件当中)
(图10:双击bomb.s文件以查看bomb.s文件的内容)
然后按下"Ctrl+F"快捷键,打开搜索栏,如 图11:按下按下"Ctrl+F"快捷键以打开搜索栏 所示。这个搜索栏的颜色有点浅,不太好找,所以要仔细点。
(图11:按下按下"Ctrl+F"快捷键以打开搜索栏)
接着在搜索栏中输入"phase_1",如 图12:在搜索栏中输入"phase_1" 所示。接着往下翻,定位到到真正的phase_1函数部分,如 图13:往下翻定位到到真正的phase_1函数部分 所示。在这里注意一点,bomb.c文件中的函数名,诸如main,phase_1,phase_2,phase_3... ...等,是与通过objdump命令反汇编生成的文件中的函数名是严格的相同的!!!
(图12:在搜索栏中输入"phase_1")
(图13:往下翻定位到到真正的phase_1函数部分)
为了帮助我们更好的理解函数phase_1的汇编代码,我们选择通过使用gdb这款工具,动态的执行函数phase_1,看看这些汇编代码真实的效果是什么样子的。毕竟有句古话,叫作“是骡子是马拉出来遛遛”。首先,在命令行窗口中使用命令"gdb ./bomb"将bomb这个可执行文件作为gdb处理的对象,如 图14:在命令行窗口中使用命令"gdb ./bomb"将bomb这个可执行文件作为gdb处理的对象 所示。下一步就是打断点,在哪里打断点呢?在判断炸弹是否爆炸的时候打断点,这样子无论我们给出的答案字符串是否正确,我们都能够让程序停留在判断炸弹是否爆炸上,进而通过一些分析得出该怎么做,才能不让炸弹爆炸。那么结合这道题,我们选择在phase_1这个函数处打断点,命令是"b phase_1",如 图15:在phase_1这个函数处打断点,命令是"b phase_1" 所示。为了查看我们所打下的断点,使用"info b"命令,查看我们已经打下的断点,如 图16:为了查看我们所打下的断点,使用"info b"命令,查看我们已经打下的断点 所示。接着就应该运行bomb这个可执行文件了,使用命令"r"开始执行bomb这个可执行文件,如 图17:使用命令"r"开始执行bomb这个可执行文件 所示。
(图14:在命令行窗口中使用命令"gdb ./bomb"将bomb这个可执行文件作为gdb处理的对象)
(图15:在phase_1这个函数处打断点,命令是"b phase_1")
(图16:为了查看我们所打下的断点,使用"info b"命令,查看我们已经打下的断点)
(图17:使用命令"r"开始执行bomb这个可执行文件)
这时,我们开始犹豫了。为什么会让我们现在就输入答案字符串呢?我们现在对于答案字符串一无所知,现在让我们输入答案,不是让我们送命嘛?但是不要害怕不要胆怯,只要能够保证断点打在了phase_1函数上,输入答案之后,程序就能够停留在判断炸弹是否爆炸这一步上。重要的事情强调三遍:
只要能够保证断点打在了phase_1函数上,输入答案之后,程序就能够停留在判断炸弹是否爆炸这一步上。
只要能够保证断点打在了phase_1函数上,输入答案之后,程序就能够停留在判断炸弹是否爆炸这一步上。
只要能够保证断点打在了phase_1函数上,输入答案之后,程序就能够停留在判断炸弹是否爆炸这一步上。
如 图18:因为断点打在了phase_1函数上,所以随便输入字符串"111"做试验 所示。
此时观察命令行给出了很多很多的反馈,一时间看不过来,甚至有些焦虑。但是不要害怕,耐下性子一步一步来。我们先看第一栏,如 图19:观察第一栏REGISTERS 所示。
(图18:因为断点打在了phase_1函数上,所以随便输入字符串"111"做试验)
(图19:观察第一栏REGISTERS)
第一栏REGISTERS表示寄存器的值,如 图19 中黄色框内的内容所示,其中我们一般重点关注的内容只有%eax、%ebx、%esp这三个寄存器。下面我们来分别讲解每一行的内容:寄存器%eax的值为0x5655b760,后面括号内的input_strings表示寄存器%eax的值是一个地址,并且是我们从键盘输入的字符串的地址,最后面的0x313131 /* '111' */就表示这个字符串是'111',那么0x313131又是怎么回事呢?原来31就是‘1’的ASCII码值,至于为什么是0x0031 3131,而不是0x3131 3100,留到评论区中作者再给出详细解释。大家也可以通过在当前命令行内,输入命令"x 0x5655b760"来查看内存中地址为0x5655b760的内容,如 图20:在当前命令行内,输入命令"x 0x5655b760"来查看内存中地址为0x5655b760的内容 所示;第二行,寄存器%ebx的值为0x5655af64,是一个_GLOBAL_OFFSET_TABLE_,这一点不需要我们去关注,大家千万不要卡在这里!!!一定要往下走!!!;第三行,寄存器%ecx的值为0x4;第四行,寄存器%edx的值为0x1;第五行,寄存器%%edi的值为0xf7fb3000;第六行,寄存器%esi的值为0xffffd1f4,而内存中地址为%esi的值的内容,也就是内存中地址为0xffffd1f4的值,是0xffffd399,而正如 图19 所示,0xffffd399也是一个地址,是一个字符串的地址。而这个字符串是什么呢?可以通过如 图20:使用"x 十六进制地址"命令来查看特定内存地址中的数 中的命令来查看。第七行,寄存器%ebp的值为0xffffd148,表示栈底指针指向0xffffd148这块内存地址;第八行,寄存器%esp的值为0xffffd11c,表示栈顶指针指向0xffffd11c这块内存地址,而内存中这块地址中,存放的数是函数phase_1的返回地址!!!;第九行,寄存器%eip的值为0x56556665,而寄存器%eip还有一个别名,叫作PC,程序计数器,记录着下一条指令的地址,其值为0x56556665,就表示下一条指令的地址为0x56556665。
(图20:在当前命令行内,输入命令"x 0x5655b760"来查看内存中地址为0x5655b760的内容)
(图21:使用"x 十六进制地址"命令来查看特定内存地址中的数。其中ASCII码值0x2f对应'/',0x68对应'h',0x6f对应'o',0x6d对应'm'... ...)
接着,我们来观察第二栏,"DISASM",这一栏会显示即将要执行的最近若干条汇编指令。如 图22:观察第二栏"DISASM" 所示。因为每次通过"ni"命令执行一条汇编指令时,第二栏"DISASM"的内容一点也不会变,除了表示下一条指令的绿色的箭头所指的指令不一样以外。所以我们看一眼就可以。
(图22:观察第二栏"DISASM")
然后观察第三栏"STACK",栈帧。如 图23:观察第三栏"STACK" 所示,不要被它给吓到,只需要知道第三栏表示栈帧就可以。至于第四栏,则不在我们关心的范围内,所以在这里不去分析它。
(图23:观察第三栏"STACK")
这时,我们开始阅读phase_1函数的汇编代码。phase_1函数的汇编代码如 图25:phase_1函数的汇编代码 所示。为了便于分析,我们先记录下来执行第<phase_1>行汇编代码前各寄存器与栈帧的情况,如 图26:执行第<phase_1>行汇编代码之前各寄存器与栈帧的情况 所示。
(图25:phase_1函数的汇编代码。)
(图26:执行第<phase_1>行汇编代码之前各寄存器与栈帧的情况)
接着在该命令行中输入"ni",单步执行一条汇编指令,也就是执行掉第<phase_1>行汇编代码"endbr32",而执行后的结果如 图27:执行第<phase_1>行汇编代码之后各寄存器与栈帧的情况 所示。
(图27:执行第<phase_1>行汇编代码之后各寄存器与栈帧的情况。在这里不需要去理会它的意思,它不会对我们需要关心的东西造成任何影响。也不需要可以去观察,观察有哪些寄存器发生了变化,或者栈帧发生了什么变化)
接着在该命令行中输入"ni",单步执行一条汇编指令,也就是执行掉第<phase_1+4>行汇编代码"push ebx",而执行后的结果如 图28:实际验证——执行第<phase_1+4>行汇编代码之后各寄存器与栈帧的情况 所示。此时phase_1函数的栈帧如 图29:纸上分析——执行完第<phase_1+4>行后,phase_1函数的栈帧 所示。
(图28:实际验证——执行第<phase_1+4>行汇编代码之后各寄存器与栈帧的情况。第<phase_1+4>行,表示将寄存器%ebx的值保存在当前函数的栈帧中,也就是保存在phase_1函数的栈帧中,与此同时,栈顶指针%esp的值也会默默的加4。为什么要将寄存器%ebx的值保存在当前函数的栈帧当中呢?因为GNU规定了寄存器%ebx是一个调用者保存的寄存器,对于该题而言,main函数调用了phase_1函数,phase_1函数是调用者,那么phase_1如果想使用寄存器%ebx,就必须先保存寄存器%ebx的值。)
内存地址 | 内存地址中的数 | 注释 | 指向这块内存的寄存器 |
0xffffd118 | 0x5655af64 | 旧的%ebx的值 | %esp |
(图29:纸上分析——执行完第<phase_1+4>行后,phase_1函数的栈帧 这时结合图29和图28中执行第<phase_1+4>行汇编代码之后栈帧的情况,我们会发现,函数phase_1函数的栈帧从0xffffd118开始,逐渐向下延申,而图28中0xffffd11c、0xffffd120、0xffffd124... ...等虽然也显示出来了,但是注意它们不是函数phase_1的栈帧,而是调用函数phase_1的函数main的栈帧!!!虽然这个函数中并没有使用到寄存器%ebp这个栈底指针,但是假如它使用了寄存器%ebp作为栈底指针,那么寄存器%ebp应一直指向0xffffd118。观察图28中执行第<phase_1+4>行汇编代码之后栈帧的情况,我们发现0xffffd11c保存着phase_1的返回地址,0xffffd120保存着传给phase_1的第一个参数,而正好符合我们在计基课上学到的%ebp+4=返回地址,%ebp+8=第一个入口参数!!!)
接着第<phase_1+5>行,将寄存器%esp的值减去0x10,也就是减去16,这就代表着什么呢?这就代表着,原来寄存器%esp指向的是0xffffd118这块内存地址,而现在它指向的是0xffffd108这块内存地址,如 图30:纸上分析——执行完第<phase_1+5>行之后,phase_1函数的栈帧 所示。通过执行"ni"命令观察执行第<phase_1+5>行后各寄存器和栈帧的情况,发现正如我们我们所想,如 图31:实际验证——执行"ni"命令观察执行第<phase_1+5>行后各寄存器和栈帧的情况 所示。
内存地址 | 内存地址中的数 | 注释 | 指向这块内存的寄存器 |
0xffffd118 | 0x5655af64 | 旧的%ebx的值 | |
0xffffd114 | |||
0xffffd110 | |||
0xffffd10c | |||
0xffffd108 | %esp |
(图30:纸上分析——执行完第<phase_1+5>行之后,phase_1函数的栈帧)
(图31:实际验证——执行"ni"命令观察执行第<phase_1+5>行后各寄存器和栈帧的情况)
第<phase_1+8>行,调用了一个不知道是用来做什么的函数,然而它无关紧要,所以接着看第<phase_1+13>行,让寄存器%ebx的值加上0x48f2。这时phase_1函数的栈帧不受影响,而相应寄存器的值却受到了影响,如 图32:纸上分析——执行完第<phase_1+13>行之后,phase_1函数的栈帧和相关寄存器的值 所示。而为了验证我们的想法,我们连续在命令行中使用两次"ni"命令,第一次以执行第<phase_1+8>行,第二次以执行第<phase_1+13>行,如 图33:实际验证——连续在命令行中使用两次"ni"命令,第一次以执行第<phase_1+8>行,第二次以执行第<phase_1+13>行,观察各寄存器和栈帧的情况 所示。
内存地址 | 内存地址中的数 | 注释 | 指向这块内存的寄存器 |
0xffffd118 | 0x5655af64 | 旧的%ebx的值 | |
0xffffd114 | |||
0xffffd110 | |||
0xffffd10c | |||
0xffffd108 | %esp |
寄存器名称 | 寄存器中的值 |
%esp | 0xffffd108 |
%ebx | 0x5655af64 |
(图32:纸上分析——执行完第<phase_1+13>行之后,phase_1函数的栈帧和相关寄存器的值)
(图33:实际验证——连续在命令行中使用两次"ni"命令,第一次以执行第<phase_1+8>行,第二次以执行第<phase_1+13>行,观察各寄存器和栈帧的情况)
接着第<phase_1+19>行,先计算寄存器%ebx的值减去0x2e20h,接着将这个差作为地址,在内存中找这块地址,然后拿到这块地址中的值,把这块地址中的值放入到寄存器%eax当中。如 图34:纸上分析——执行完第<phase_1+19>行之后,phase_1函数的栈帧和相关寄存器的值 所示。而为了验证我们的想法,我们在命令行中使用"ni"命令,执行第<phase_1+19>行,如 图35:实际验证——执行第<phase_1+19>行后观察各寄存器和栈帧的情况 所示。
内存地址 | 内存地址中的数 | 注释 | 指向这块内存的寄存器 |
0xffffd118 | 0x5655af64 | 旧的%ebx的值 | |
0xffffd114 | |||
0xffffd110 | |||
0xffffd10c | |||
0xffffd108 | %esp |
寄存器名称 | 寄存器中的值 |
%esp | 0xffffd108 |
%ebx | 0x5655af64 |
%eax | 0x56558144 |
(图34:纸上分析——执行完第<phase_1+19>行之后,phase_1函数的栈帧和相关寄存器的值)
(图35:实际验证——执行第<phase_1+19>行后观察各寄存器和栈帧的情况)
接着第<phase_1+25>行,将寄存器%eax的值推入函数phase_1的栈帧中,不要忘记,push命令都带有一个默认的行为,就是将栈顶指针%esp的值减去4。所以这时函数phase_1的栈帧以及相关寄存器的值如 图36:纸上分析——执行完第<phase_1+25>行之后,phase_1函数的栈帧和相关寄存器的值 所示。为了验证我们的想法,我们在命令行中使用"ni"命令,执行第<phase_1+25>行,如 图37:实际验证——执行第<phase_1+25>行后观察各寄存器和栈帧的情况 所示。
内存地址 | 内存地址中的数 | 注释 | 指向这块内存的寄存器 |
0xffffd118 | 0x5655af64 | 旧的%ebx的值 | |
0xffffd114 | |||
0xffffd110 | |||
0xffffd10c | |||
0xffffd108 | |||
0xffffd104 | 0x56558144 | push进栈帧的%eax的值 | %esp |
寄存器名称 | 寄存器中的值 |
%esp | 0xffffd104 |
%ebx | 0x5655af64 |
%eax | 0x56558144 |
(图36:纸上分析——执行完第<phase_1+25>行之后,phase_1函数的栈帧和相关寄存器的值)
(图37:实际验证——执行第<phase_1+25>行后观察各寄存器和栈帧的情况)
接着第<phase_1+26>行,先计算寄存器%esp的值加上0x1c,然后把这个和作为地址,在内存中找这块地址,然后拿到这块地址中的值,把这块地址中的值push进函数phase_1的栈帧中,如 图38:纸上分析——执行完第<phase_1+26>行之后,phase_1函数的栈帧和相关寄存器的值 所示。为了验证我们的想法,我们在命令行中使用"ni"命令,执行第<phase_1+26>行,如 图39:实际验证——执行第<phase_1+26>行后观察各寄存器和栈帧的情况 所示。
内存地址 | 内存地址中的数 | 注释 | 指向这块内存的寄存器 |
0xffffd118 | 0x5655af64 | 旧的%ebx的值 | |
0xffffd114 | |||
0xffffd110 | |||
0xffffd10c | |||
0xffffd108 | |||
0xffffd104 | 0x56558144 | push进栈帧的%eax的值 | |
0xffffd100 | 0x5655b760 | 第<phase_1+26>行push进栈帧的%esp+0x1c这块地址0xffffd120的值,也就是0x5655b760 | %esp |
寄存器名称 | 寄存器中的值 |
%esp | 0xffffd100 |
%ebx | 0x5655af64 |
%eax | 0x56558144 |
(图38:纸上分析——执行完第<phase_1+26>行之后,phase_1函数的栈帧和相关寄存器的值)
(图39:实际验证——执行第<phase_1+26>行后观察各寄存器和栈帧的情况)
接着第<phase_1+30>行,需要调用一个在剩下几个阶段里都非常重要的函数"strings_not_equal"。但是考虑到如果在这篇文章中展开讲"strings_not_equal"的话,会加长篇幅,所以在这里我们概括来讲,就是函数phase_1调用函数strings_not_equal之后一瞬间,会先选择寄存器%esp所指的内存中的内容(%esp所指的内存中的内容是个指针)所指的字符串,接着选择寄存器%esp+4所指的内存中的内容(%esp所指的内存中的内容是个指针)所指的字符串,然后比较这两个字符串是否相等,如果相等,则将专门用来保存函数调用的结果的寄存器%eax的值设置为0,若不相等,则设置为1。但是我们查看函数phase_1的汇编代码,如 图40:查看函数phase_1的汇编代码 所示,如果第511行执行后%eax为1,那么第512行不用考虑,第513行执行test %eax %eax时,记住test %eax %eax这条汇编指令的结果等于%eax == 0,而显然%eax == 0的值是0,进而test %eax %eax的值是0,进而会被第514行视为不相等,进而执行跳跃操作跳到第518行,进而会引爆炸弹。所以我们的思路就是,怎么样才能让第511行执行后%eax为0,也就是如何才能使得两个字符串相等。这时答案就已经呼之欲出了,我们只有输入"I turned the moon into something I call a Death Star.",才能使得炸弹最终不爆炸。
(图40:查看函数phase_1的汇编代码)