首先,将自己的实验包从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 [
(图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函数的汇编代码 所示。为了便于分析,我们先记录下来执行第
(图25:phase_1函数的汇编代码。)
(图26:执行第
接着在该命令行中输入"ni",单步执行一条汇编指令,也就是执行掉第
(图27:执行第
接着在该命令行中输入"ni",单步执行一条汇编指令,也就是执行掉第
(图28:实际验证——执行第
内存地址 | 内存地址中的数 | 注释 | 指向这块内存的寄存器 |
0xffffd118 | 0x5655af64 | 旧的%ebx的值 | %esp |
(图29:纸上分析——执行完第
接着第
内存地址 | 内存地址中的数 | 注释 | 指向这块内存的寄存器 |
0xffffd118 | 0x5655af64 | 旧的%ebx的值 | |
0xffffd114 | |||
0xffffd110 | |||
0xffffd10c | |||
0xffffd108 | %esp |
(图30:纸上分析——执行完第
(图31:实际验证——执行"ni"命令观察执行第
第
内存地址 | 内存地址中的数 | 注释 | 指向这块内存的寄存器 |
0xffffd118 | 0x5655af64 | 旧的%ebx的值 | |
0xffffd114 | |||
0xffffd110 | |||
0xffffd10c | |||
0xffffd108 | %esp |
寄存器名称 | 寄存器中的值 |
%esp | 0xffffd108 |
%ebx | 0x5655af64 |
(图32:纸上分析——执行完第
(图33:实际验证——连续在命令行中使用两次"ni"命令,第一次以执行第
接着第
内存地址 | 内存地址中的数 | 注释 | 指向这块内存的寄存器 |
0xffffd118 | 0x5655af64 | 旧的%ebx的值 | |
0xffffd114 | |||
0xffffd110 | |||
0xffffd10c | |||
0xffffd108 | %esp |
寄存器名称 | 寄存器中的值 |
%esp | 0xffffd108 |
%ebx | 0x5655af64 |
%eax | 0x56558144 |
(图34:纸上分析——执行完第
(图35:实际验证——执行第
接着第
内存地址 | 内存地址中的数 | 注释 | 指向这块内存的寄存器 |
0xffffd118 | 0x5655af64 | 旧的%ebx的值 | |
0xffffd114 | |||
0xffffd110 | |||
0xffffd10c | |||
0xffffd108 | |||
0xffffd104 | 0x56558144 | push进栈帧的%eax的值 | %esp |
寄存器名称 | 寄存器中的值 |
%esp | 0xffffd104 |
%ebx | 0x5655af64 |
%eax | 0x56558144 |
(图36:纸上分析——执行完第
(图37:实际验证——执行第
接着第
内存地址 | 内存地址中的数 | 注释 | 指向这块内存的寄存器 |
0xffffd118 | 0x5655af64 | 旧的%ebx的值 | |
0xffffd114 | |||
0xffffd110 | |||
0xffffd10c | |||
0xffffd108 | |||
0xffffd104 | 0x56558144 | push进栈帧的%eax的值 | |
0xffffd100 | 0x5655b760 | 第 |
%esp |
寄存器名称 | 寄存器中的值 |
%esp | 0xffffd100 |
%ebx | 0x5655af64 |
%eax | 0x56558144 |
(图38:纸上分析——执行完第
(图39:实际验证——执行第
接着第
(图40:查看函数phase_1的汇编代码)