在勉强读完第四章后(流水线后面的HCL细节实现都略过没看了),决定开始尝试传说中的lab,datalab与进制表示的骚操作相关,有点无趣,于是直接开始lab2 bomb(二进制炸弹实验)
Bomb Lab 介绍
这个 lab 给了一个名为 bomb
的程序文件,还有一个名为 bomb.c
的文件是题目要求和 bomb
实现的代码框架,代码是不完整的,因此无法编译。题目要求是运行 bomb 后输入六个 phase ,输入正确 bomb 程序才能继续运行,输入错误就会 bomb!
实验材料可以从这里获取,找到 Self-Study Handout
就可以下载材料了,而 Writeup
并不是题解,是实验的要求说明
下载后:
tar -xvf bomb.tar
cd bomb/
可以随便玩玩炸弹(bushi),根据材料可以知道 CMU 的学生是在实验机房完成实验,并且每引爆一次炸弹就会在实验成绩上 -0.5 (上限是20),从这个角度上讲,也就是说我们做实验最好也少触发炸弹
首先是基本操作:
objdump -d ./bomb > ./bomb.asm
此外,接下来实验会多次使用到 GDB 调试工具,附上 CSAPP 的 GDB 学习材料(笔者接下来也是从 0 开始尝试使用 GDB)
第一关
phase_1 很 easy,直接 IDA F5 出结果
也可以尝试使用 GDB,
rdi
是传入 phase_1
的第一个参数,也是传入 string_not_equal
的第一个参数,因此这段汇编里面没有使用 rdi
,而 esi
则是传入 string_not_equal
的第二个参数,因此我们去读 esi
指向的地址,就得到了想要的字符串
第二关
第二关就嗯读汇编了,IDA 逆出来的也看不懂,read_six_numbers
提示需要读入 6 个数
读入后,把 (rsp)
和 0x1
比较,必须相等才不会引爆炸弹,之后 rbp
赋值为 rsp + 18
,rbx
为 rsp + 4
,add eax, eax
操作会使每次的数翻倍,rbx
每次 +4 后移。其实就循环,要求 rsp
里面存的数必须依次是 1, 2, 4, 8, 16, 32
一开始我尝试的输入是 32, 16, 8, 4, 2, 1
, GDB 观察发现反了,再换成 1, 2, 4, 8, 16, 32
,在 GDB 调试的时候却死活告诉我只读入了 5 个数,懵逼
无奈之下,只好把输入文件的数改成 1, 2, 4, 8, 16, 32, 64
,在 GDB 下提示通过了关卡2。
(后来根据经验我发现如果使用文件输入的话,最后一个字符后需要加一个换行,否则可能读不进去,不懂为啥)
芜湖,终于解出第二关,GDB 搞了好久
第三关
第3关应该是个跳表,switch
结构,IDA F5 后如下:
__int64 __fastcall phase_3(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+8h] [rbp-10h] BYREF
int v3; // [rsp+Ch] [rbp-Ch] BYREF
if ( (int)__isoc99_sscanf(a1, "%d %d", &v2, &v3) <= 1 )
explode_bomb();
switch ( v2 )
{
case 0:
result = 207LL;
break;
case 1:
result = 311LL;
break;
case 2:
result = 707LL;
break;
case 3:
result = 256LL;
break;
case 4:
result = 389LL;
break;
case 5:
result = 206LL;
break;
case 6:
result = 682LL;
break;
case 7:
result = 327LL;
break;
default:
explode_bomb();
}
if ( (_DWORD)result != v3 )
explode_bomb();
return result;
}
其实从上面已经可以看出答案了
分析汇编,可以知道需要输入2个数,并且要求输入的第一个数不能大于7,于是先尝试输入
1 255
然后调试观察调到哪里
(这里好像就是因为末尾没有换行,所以输入的255变成了25,怪)
观察到往 eax
里面写入值后,就把值打印出来,可以发现,它需要的值是 311
输入 1 311,成功通过了第三关
第四关
第四关是递归函数,顿时头皮发麻,IDA F5 后如下:
__int64 __fastcall phase_4(__int64 a1)
{
__int64 result; // rax
unsigned int v2; // [rsp+8h] [rbp-10h] BYREF
int v3; // [rsp+Ch] [rbp-Ch] BYREF
if ( (unsigned int)__isoc99_sscanf(a1, "%d %d", &v2, &v3) != 2 || v2 > 0xE )
explode_bomb();
result = func4(v2, 0LL, 14LL);
if ( (_DWORD)result || v3 )
explode_bomb();
return result;
}
汇编代码如下:
000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
40101a: be cf 25 40 00 mov $0x4025cf,%esi
40101f: b8 00 00 00 00 mov $0x0,%eax
401024: e8 c7 fb ff ff call 400bf0 <__isoc99_sscanf@plt>
401029: 83 f8 02 cmp $0x2,%eax
40102c: 75 07 jne 401035 <phase_4+0x29>
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp)
401033: 76 05 jbe 40103a <phase_4+0x2e>
401035: e8 00 04 00 00 call 40143a <explode_bomb>
40103a: ba 0e 00 00 00 mov $0xe,%edx
40103f: be 00 00 00 00 mov $0x0,%esi
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi
401048: e8 81 ff ff ff call 400fce <func4>
40104d: 85 c0 test %eax,%eax
40104f: 75 07 jne 401058 <phase_4+0x4c>
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056: 74 05 je 40105d <phase_4+0x51>
401058: e8 dd 03 00 00 call 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 ret
无论是分析 IDA 的 c代码还是汇编代码,都可以知道,需要读入两个数,第2个数不能大于 0xE
,然后调用 func4(v1, 0, 14)
,传入参数在汇编里写得很明白
调用的 func4
是递归函数,IDA F5后,改了点变量名后结果如下:
汇编的最后告诉我们最后返回值一定得为0,并且读入的第二个数也得是0,于是 7 0 便是一组可行解
第五关
第五关我做的最顺利,IDA F5 后改了点变量名,得到如下:
unsigned __int64 __fastcall phase_5(char *input)
{
__int64 i; // rax
char v3[8]; // [rsp+10h] [rbp-18h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-10h]
v4 = __readfsqword(0x28u);
if ( (unsigned int)string_length(input) != 6 )
explode_bomb();
for ( i = 0LL; i != 6; ++i )
v3[i] = array_3449[input[i] & 0xF];
v3[6] = 0;
if ( (unsigned int)strings_not_equal(v3, "flyers") )
explode_bomb();
return __readfsqword(0x28u) ^ v4;
}
逻辑很清晰,把输入的字符串 input[i] & 0xF
得到 array_3449
的下标,最后要求 v3
为 flyers
。 array_3449
如下:
对照着顺序写了个简单的 exp.c
,用于看 ASCII 码的低 16 位
草稿纸上就是 flyers
---> 9FE567
---> ionefg
的过程,尝试解出的答案
Nice!!!一次通过 phase_5
第六关
第六关是 Boss,涉及到链表、排序、结构体,太难了,看了一些题解也是比较模糊,汇编代码也很长,下面是 IDA F5 后尽力得到的结果,从 67 行开始的内容就不太明白了:
__int64 __fastcall phase_6(char *input)
{
int *a; // r13
int cnt; // er12
int v3; // ebx
int *c; // rax
unsigned __int64 i; // rsi
_QWORD *v6; // rdx
int v7; // eax
int v8; // ecx
__int64 v9; // rbx
char *v10; // rax
__int64 j; // rcx
__int64 v12; // rdx
int v13; // ebp
__int64 result; // rax
int b[6]; // [rsp+0h] [rbp-78h] BYREF
char v16; // [rsp+18h] [rbp-60h] BYREF
__int64 v17; // [rsp+20h] [rbp-58h]
char v18; // [rsp+28h] [rbp-50h] BYREF
char v19; // [rsp+50h] [rbp-28h] BYREF
a = b;
read_six_numbers((__int64)input, (__int64)b);
cnt = 0;
while ( 1 ) // 读入的b[6]要求两两不同,且<=6
{
if ( (unsigned int)(*a - 1) > 5 )
explode_bomb();
if ( ++cnt == 6 )
break;
v3 = cnt;
do
{
if ( *a == b[v3] )
explode_bomb();
++v3;
}
while ( v3 <= 5 );
++a;
}
c = b;
do
{
*c = 7 - *c; // b[i] = 7 - b[i]
++c;
}
while ( c != (int *)&v16 );
for ( i = 0LL; i != 24; i += 4LL )
{
v8 = b[i / 4];
if ( v8 <= 1 )
{
v6 = &node1;
}
else
{
v7 = 1;
v6 = &node1;
do
{
v6 = (_QWORD *)v6[1];
++v7;
}
while ( v7 != v8 );
}
*(__int64 *)((char *)&v17 + 2 * i) = (__int64)v6;
}
v9 = v17;
v10 = &v18;
for ( j = v17; ; j = v12 )
{
v12 = *(_QWORD *)v10;
*(_QWORD *)(j + 8) = *(_QWORD *)v10;
v10 += 8;
if ( v10 == &v19 )
break;
}
*(_QWORD *)(v12 + 8) = 0LL;
v13 = 5;
do
{
result = **(unsigned int **)(v9 + 8);
if ( *(_DWORD *)v9 < (int)result )
explode_bomb();
v9 = *(_QWORD *)(v9 + 8);
--v13;
}
while ( v13 );
return result;
}
大概是读入6个数,要求是6的排列,然后每个数 x 变成 6 - x
然后进行了一通我不想看的排序等操作,最后要求链表从大到小排序,上面查看了链表里面的值,从大到小是:
3 4 5 6 1 2
再 7 - x 一下,得到答案
4 3 2 1 6 5
至此,成功通过了实验
实验的最后结果 ,一种可能的input.txt
如下:
Border relations with Canada have never been better.
1 2 4 8 16 32
1 311
7 0
ionefg
4 3 2 1 6 5
小结
- 感觉文件末尾需要多一个换行,否则读不到数
- IDA技术不够,虽然翻了一下IDA的使用技巧和手册还杯水车薪,不过庆幸的是 IDA 里面的很多“单词”大概能看懂了,比学 x86-64 汇编之前好很多
- 使用 GDB 可以在没有源代码的情况下动态调试,像跳表、结构体里面的内容等好像不调试你也就看不到。IDA据说也有动态调试功能,可惜我还不会
- 还有个彩蛋,隐藏关卡,以后再说吧
参考资料
https://wdxtub.com/csapp/thick-csapp-lab-2/2016/04/16/
https://github.com/LRL52/CSAPP-Labs/blob/master/notes/bomb.md
https://zhuanlan.zhihu.com/p/28422249
Comments | NOTHING