目录
概览
- 理解程序(控制语句、函数、返回值、堆栈结构)是如何运行的
- 掌握GDB调试工具和objdump反汇编工具
本实验设计为一个黑客拆解二进制炸弹的游戏。我们仅给黑客(同学)提供一个二进制可执行文件bomb_64和主函数所在的源程序bomb_64.c,不提供每个关卡的源代码。程序运行中有6个关卡(6个phase),每个关卡需要用户输入正确的字符串或数字才能通关,否则会引爆炸弹(打印出一条错误信息,并导致评分下降)!
要求同学运用GDB调试工具和objdump反汇编工具,通过分析汇编代码,找到在每个phase程序段中,引导程序跳转到“explode_bomb”程序段的地方,并分析其成功跳转的条件,以此为突破口寻找应该在命令行输入何种字符串来通关。
本实验需解决Phase_1(15分)、Phase_2(15分)、Phase_3(15分)、Phase_4(15分)、Phase_5(15分)、Phase_6(10分)。通过截图+文字的形式把实验过程写在实验报告上,最后并撰写实验结论与心得(15分)。
环境:
- 计算机(Intel CPU)
- Linux64位操作系统(Ubuntu 17)
- GDB调试工具
- objdump反汇编工具
过程及内容:
输入$ objdump -d bomb_64 > 1.txt,进行反汇编并将结果输出到1.txt。(图1)
图表 1 反汇编
-
第一关
【主要思路】
- 在1.txt中找到和第一关相关的汇编代码,发现如果要不引爆(即不运行至400e82 call1 <explode_bomb>),需要执行上一句的跳转指令,而跳转条件是%rax为0,往上查看可以发现%rax存放的是调用<strings_not_equal>函数的返回值,即判断字符串是否不相等函数,推测应该是2个字符串相等时返回0,具体哪两个字符串需要进一步打开函数分析。(图2)
图表 2 第一关汇编代码分析
- <strings_not_equal>函数汇编代码如下(图3),经过分析验证了我们的猜想,同时可知2个字符串分别为输入字符串(图4),内存0x401af8处字符串,所以只需要找到该字符串就是过关答案。
图表 3 <strings_not_equal>函数汇编代码分析
图表 4 main.c里知输入为参数1
- 将上述汇编代码改写为C语言代码如下(有改动)
//传入2字符串,判断是否不相等
int strings_not_equal(char* s1, char* s2) {
//2字符串长度不相等时返回1
if (string_length(s1) != string_length(s2)) {
return 1;
}
//逐字符比较
int i = 0;
while (s1[i])
{
if (s1[i] != s2[i]) {
return 1;//不等返回1
}
i++;
}
return 0;//相等返回0
}
//input为输入字符串,存放于%rdi寄存器
void phase_1(char* input) {
//于内存0x40123d处取字符串"Science isn't about why, it's about why not?"
//调用strings_not_equal函数判断输入与该字符串是否不相等
int res=strings_not_equal(input, "Science isn't about why, it's about why not?");
if (!res) {
explode_bomb();//不等引爆炸弹
}
return;
}
可通过gdb获得存放在0x401af8处字符串。(图5)
图表 5 gdb查看内存里字符串
另外我通过IDA软件也查看到相应字符串“1 ”。(图6)
图表 6 IDA查看字符串
过关成功(图7)
图表 7 第一关成功
-
第二关
【主要思路】
- 在1.txt中找到和第一关相关的汇编代码,发现如果要不引爆(即不运行至400e82 call1 <explode_bomb>),需要执行上一句的跳转指令,而跳转条件是%rax为0,往上查看可以发现%rax存放的是调用<strings_not_equal>函数的返回值,即判断字符串是否不相等函数,推测应该是2个字符串相等时返回0,具体哪两个字符串需要进一步打开函数分析。(图2)
图表 2 第一关汇编代码分析
- <strings_not_equal>函数汇编代码如下(图3),经过分析验证了我们的猜想,同时可知2个字符串分别为输入字符串(图4),内存0x401af8处字符串,所以只需要找到该字符串就是过关答案。
图表 3 <strings_not_equal>函数汇编代码分析
图表 4 main.c里知输入为参数1
- 将上述汇编代码改写为C语言代码如下(有改动)
void red_six_numbers(char* input,int* num) {
//调用输入函数,参数包括输入,格式(存于0x401eb2),数组指针
int n=__isoc99_sscanf(input,"%d %d %d %d %d %d", num, num+1, num+2, num+3, num+4, num+5);
if (n <= 5) {
//如果数字小于6个爆炸
explode_bomb();
}
return;
}
void phase_2(char *input) {//参数为数组指针
int num[6];
read_six_numbers(input,num);//数组输入
int sum = 0;
int i = 0;
do {
if (num[i] != num[i + 3]) {
explode_bomb();//相邻3个不同或相邻相同就爆炸
}
sum += num[i];
i++;
} while (i < 3);
if (!sum) {
explode_bomb();//和为0爆炸
}
return;
}
图表 11 第二关过关
-
第三关
【主要思路】
- 经gdb或者IDA查询内存0x401ebe为”%d %d”,可确定输入为2个整数。(图12)
图表 12 gdb和IDA输入格式查询
- 由跳转表推测处为switch语句,(gdb或IDA)查询该跳转表(首地址为0x401b60)可得各种情况跳转地址。(图13)
图表 13 gdb和IDA查询跳转表
- 得出不同情况的值。(图14)
图表 14 第三关汇编代码分析
- 需要输入对应情况编号和结果才可以过关,共8组答案。(表15)
case |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
res |
0x217 |
0x39e |
0xd6 |
0x153 |
0x77 |
0x160 |
0x397 |
0x19c |
dec |
535 |
926 |
214 |
339 |
119 |
352 |
919 |
412 |
图表 15 8组答案
图表 16 过关
汇编代码转C语言代码如下(有改动):
void phase_3(char* input) {
int a, value,res;//待输入的序号和值,对应答案
//调用输入函数,参数包括输入,格式(存于0x401ebe)为2整数,存于a和value
int n = __isoc99_sscanf(input, "%d %d", a,value);
if (n <= 1||a>7) {
//如果输入小于2个或序号a大于7非法,爆炸
explode_bomb();
}
switch (a)
{
case 0:
res = 0x217;
break;
case 1:
res = 0x39e;
break;
case 2:
res = 0xd6;
break;
case 3:
res = 0x153;
break;
case 4:
res = 0x77;
break;
case 5:
res = 0x160;
break;
case 6:
res = 0x397;
break;
case 7:
res = 0x19c;
break;
default:
explode_bomb();
return;
}
if (res != value) {
explode_bomb();
}
return;
}
-
第四关
【主要思路】
- 相关代码如下,需要查看内存0x401ec1处的值确定输入格式,调用了函数且要求返回值为55才可过关。(图17)
图表 17 第4关汇编代码分析
- gdb查看内存0x401ec1处的值为”%d”确定输入为1个整数。(图18)
图表 18 输入格式
- 函数代码如下(图19),发现为递归函数,递归停止条件为参数小于2,推出递归式(斐波那契)
图表 19 递归函数代码分析
可推算要让结果为55,则
结果为8+1=9。
汇编代码转化为c语言代码如下(有改动)
int func4(int x) {
int res = 1;//返回值初始为1
if (x <= 1) {
return res;//终止条件
}
return func4(x - 1) + func(x - 2);
}
void phase_4(char* input) {
int x;
//调用输入函数,参数包括输入
//格式(存于0x401ec1)为整数,存于x
int n = __isoc99_sscanf(input, "%d",x);
if (n != 1 || x<= 0) {
//如果输入不唯一或x<=0非法,爆炸
explode_bomb();
}
if (func4(x) != 55) {
explode_bomb();//调用递归函数不为55爆炸
}
return;
}
图表 20 第四关过关
-
第五关
【主要思路】
- 输入为两整数,需要gdb(图21)或IDA(图22)查询数组,得为16个整型数组。
图表 21 gdb查询数组
图表 22 IDA查询数组
- 代码如下,发现输入为数组下标和跳转的12次的总和,每次的值为下一次的下标,直到找到15停下,并且要求循环次数为12,而输入就是可以时最终12次跳转结果满足条件的首下标和跳转的11个数总和。(图23)
图表 23 第五关代码分析
查询数组为:num[16] = {10, 2, 14, 7, 8, 12, 15, 11, 0, 4, 1, 13, 3, 9, 6, 5}
对应下标表格如下:
下标 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
值 |
10 |
2 |
14 |
7 |
8 |
12 |
15 |
11 |
0 |
4 |
1 |
13 |
3 |
9 |
6 |
5 |
图表 24 数组
反向推理如下:15 <- 6 <- 14 <- 2 <- 1 <- 10 <- 0 <- 8 <- 4 <- 9 <- 13 <- 11 <- 7
即7 -> 11 -> 13 -> 9 -> 4 -> 8 -> 0 -> 10 -> 1 -> 2 -> 14 -> 6 -> 15 (图23)
图表 25 反向推理
- 所以输入7可过关,总和为11 + 13 + 9 + 4 + 8 + 0 + 10 + 1 + 2 + 14 + 6 + 15 = 93,输入7和93过关。(图26)
图表 26 第5关过关
汇编代码转c语言代码如下,部分改动:
void phase_5(char* input) {
int num, sum;//待输入的下标与跳转11个数总和,对应答案
//调用输入函数,参数包括输入,格式(存于0x401ebe)为2整数,存于num和sum
int n = __isoc99_sscanf(input, "%d %d", num, sum);
if (n <= 1 || num > 15) {
//如果输入小于2个或下标num大于15非法,爆炸
explode_bomb();
}
int sum0 = 0;//sum0记录跳转11个数的总和,初始为0
int i = 0;//i记录跳转次数,初始为0
int num0 = num;//备份初始下标,再循环中不断更新
//存于内存0x401ba0的16个数数组
int array[16] = { 10, 2, 14, 7, 8, 12, 15, 11, 0, 4, 1, 13, 3, 9, 6, 5 };
do {
i++;//跳转次数加1
num0 = *(array+num0);//数组取数,更新为下一次跳转下标
sum0 += num0;
} while (i != 12);
if (num0 != 15||sum!=sum0) {
explode_bomb();//跳转12次后结果不为15或输入的和不对则爆炸
}
return;
}
-
第六关
【主要思路】
- 找到相关代码如下,结合代码出现的node0头结点和第六关的考察寻址,以及fun6不断地址加8取值再加8取值,初步怀疑fun6函数考察链表相关知识,那么我根据头结点(内存0x602780处)找到相关链表。(图27)
图表 27 第六关代码分析
- 通过gbd和IDA查阅链表如下:
图表 28 gdb和IDA查阅链表如下
根据fun6函数,可知骑在降序排序并返回排序好对应的头结点,设置断点(图29)于函数返回处并查看输入300函数返回后的链表,确实降序。结合返回后代码,说明要是输入的数位于数组中第4大数,而原本第3,4大数为600和673,即要输位于[600,679](图30)
图表 29 设置断点
图表 30 fun6函数降序排序,输入601六关过关
结论:
可以通过反汇编指令获得反汇编代码。在本次拆弹实验,对于汇编代码的查看可以先找到炸弹语句,反推出避免爆炸条件。可以根据函数名初步确定函数功能,再到具体代码进行详细分析。根据输入函数的格式可以初步确定思路内容方向。由跳转表语句或者数组语句可确定相关结构方向。又如不断取值再寻址可确定为链表结构。内存数据可以通过gdb查看。Gdb还可查看跳转表,数组,链表等相关结构。可灵活运用gdb的断点调试功能确定猜测以及函数功能。
心得体会:
通过该次关于汇编代码拆弹的趣味实验,我初步了解了汇编代码在实际一个小工程的运行流程。进一步加深了相关知识点的理解。如内存数据的存取,函数的调用,参数寄存器的设置,栈帧中关于寄存器的保护、临时变量、数组创建的实现的理解,跳转表、数组、链表的具体实现有了较为深刻的理解。同时也加深了关于gdb工具的学习使用,受益良多。