http://csapp.cs.cmu.edu/3e/labs.html

​ 这个实验是一个逆向实验,主要是逆向代码,理解程序逻辑.主要是静态分析,有一些东西不好理解可以借助动态分析来调试.

​ 题目主要分了六步,以及一个隐藏关卡

阶段一

​ 直接对比字符串即可,

1
2
3
4
5
6
7
8
9
__int64 __fastcall phase_1(__int64 a1)
{
__int64 result; // rax

result = strings_not_equal(a1, "Border relations with Canada have never been better.");
if ( (_DWORD)result )
explode_bomb();
return result;
}

阶段二

​ 读取六个值,第一个值必须为1. 然后循环处理剩下五个值,这里有一条关键语句,对它的理解非常重要

1
result = (unsigned int)(2 * *((_DWORD *)v2 - 1));

​ (_DWORD *)v2这个的意思是把v2这个指针强制转换成 _DWORD类型的指针,然后对v2这个指针-1,其实是-4字节(减去指针指向的数据类型的大小),也就是指向这个指针的前一个数值的指针,然后再加一个 * 得到 这个位置的值,然后再乘2

image-20230506222448681

​ 综上所述,第一个位置为1,下一个位置是上一个位置的两倍,于是答案:1 2 4 8 16 32

阶段三

1
2
if ( (int)__isoc99_sscanf(a1, "%d %d", &v2, &v3) <= 1 )
explode_bomb();

​ 把a1按照空格分开两个数,分别给v2,v3, v2对应的值和v3一样即可,有多个解, 如:0 207

阶段四

​ scanf的返回值是 匹配上的个数,所以阶段三可以只输入v2,那能有解吗??????

​ 这里有个递归调用,result和v3都要为0,v3直接输入0即可,result呢,进func4能发现func4里的v3和我们输出的v2只要相等,就会返回0,v3的值是固定的7,所以v2输入7即可.

​ 有个注意的点就是看清楚条件,这个条件是都需要为0才能过关,刚开始以为都不能为0(思维惯性?)

1
2
if ( (_DWORD)result || v3 )
explode_bomb();

​ 7 0

阶段五

​ 输入一个长度为6的字符串,每个值与0xf进行与操作,然后取array_3449数组中相应的值,看是否与flyers字符串里对应的字符匹配

1
2
3
4
5
6
7
if ( (unsigned int)string_length(a1) != 6 )
explode_bomb();
for ( i = 0LL; i != 6; ++i )
v3[i] = array_3449[*(_BYTE *)(a1 + i) & 0xF];
v3[6] = 0;
if ( (unsigned int)strings_not_equal(v3, "flyers") )
explode_bomb();

​ 逆向过来想的话,先找到array_3449数组中对应的值

1
2
3
4
5
6
7
8
array_3449: ?aduiersnfotvbyl

9 f
15 l
14 y
5 e
6 r
7 s

​ 然后就是 x & 0xf等于这个值即可,&0xf的话,就是取后四位

​ 1001 末四位为9,其余任意 以此类推
​ 1111
​ 1110
​ 0101
​ 0110
​ 0111

​ 一种解:ionefg 解不唯一,只要末位符合即可

阶段六

​ 读入6个整数,都小于7,且每个都不相等,不会是0和负数,因为这样话无符号数会很大,所以就是1 2 3 4 5 6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  v1 = v15;
read_six_numbers(a1, (__int64)v15);
v2 = 0;
while ( 1 )
{
if ( (unsigned int)(*v1 - 1) > 5 )
explode_bomb();
if ( ++v2 == 6 )
break;
v3 = v2;
do
{
if ( *v1 == v15[v3] )
explode_bomb();
++v3;
}
while ( v3 <= 5 );
++v1;
}

​ 注意结合顺序,这里不是读的v1-1,而是v1地址取值后-1

1
if ( (unsigned int)(*v1 - 1) > 5 )

​ 调整顺序

​ 1 2 3 4 5 6 => 6 5 4 3 2 1

​ 1 3 2 4 6 5 => 6 4 5 3 1 2

1
2
3
4
5
6
7
v4 = (char *)v15;
do
{
*(_DWORD *)v4 = 7 - *(_DWORD *)v4;
v4 += 4;
}
while ( v4 != &v16 );

​ 下一步把这些数放到一个链表中,就按照数的顺序,匹配给对应的链表的结点,比如node2就匹配2,v17数组中的顺序,就按照给定的这个顺序来

​ 注意这个node是一个16位的数据,前8位代表了自己的值,后8位(v6[1])连接下一个node(指针,指向下一个node)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for ( i = 0LL; i != 24; i += 4LL )
{
v8 = v15[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;
}

​ 举例来说,加入给定的顺序是 1 2 3 6 5 4 ,经过前面变换后得到6 5 4 1 2 3,会按照对应的顺序,找到它在链表中的结点的位置,也就是6 5 4 1 2 3,赋值给v17

1
2
3
4
5
6
7
8
9
10
11
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;

​ 翻译一下,会发现,这里其实什么也没干….就是单纯为了增加思考量.

​ 在第一次循环中,*(_QWORD *)(j + 8) = *(_QWORD *)v10;,因为j+8就是v18,v10也是v18,相当于v18=v18

​ 后面的迭代这条语句也没用,因为在后面的迭代中,j = v12 = v10, 第一轮中v10=v10+8, 所以在第二次迭代中,这里的语句就成了v10+8 = v10+8, 后面也一样,每一轮的*(_QWORD *)(j + 8) = *(_QWORD *)v10; 两边都是相等的.

1
2
3
4
5
6
7
8
9
10
v13 = 5;
do
{
result = **(unsigned int **)(v9 + 8);
if ( *(_DWORD *)v9 < (int)result )
explode_bomb();
v9 = *(_QWORD *)(v9 + 8);
--v13;
}
while ( v13 );

​ 然后来到最后一步,v9也就是v17,存储着node链表的顺序,

​ 注意这条语句,先将v9加8,也就是获取存储的第二个node的地址,unsigned int **代表指向指针的指针类型,二级指针,因为在v9也就是v17中,存储的是一个数的指针的指针

1
result = **(unsigned int **)(v9 + 8);     

​ 看下面的例子,对这个二级指针解引用,刚开始指针指向0x6032f8,第一次解引用取得0x603300,也就是node结点的的下一个结点的地址,再解引用,得到下一个结点存储的值0x4000002b3

1
2
3
4
5
pwndbg> teles $rbx
00:0000│ rbx 0x6032f0 (node3) ◂— 0x30000039c
01:00080x6032f8 (node3+8) —▸ 0x603300 (node4) ◂— 0x4000002b3

if ( *(_DWORD *)v9 < (int)result )

​ 这里的比较大小,比较的是结点的大小,需要前面的大于后面的,例如

​ 如果是在v9中是1 2 3 4 5 6的顺序的话

​ 取node2 , 需要node2<node1

​ 取node3 , 需要node3<node2

​ 所以要按照node的数值的大小排布一下

1
2
3
4
5
6
7
04:00200x7fffffffe3f0 —▸ 0x6032d0 (node1) ◂— 0x10000014c
05:00280x7fffffffe3f8 —▸ 0x6032e0 (node2) ◂— 0x2000000a8
06:00300x7fffffffe400 —▸ 0x6032f0 (node3) ◂— 0x30000039c
07:00380x7fffffffe408 —▸ 0x603300 (node4) ◂— 0x4000002b3
pwndbg>
08:00400x7fffffffe410 —▸ 0x603310 (node5) ◂— 0x5000001dd
09:00480x7fffffffe418 —▸ 0x603320 (node6) ◂— 0x6000001bb

​ 所以顺序应该是 (从大到小) Node 3 4 5 6 1 2

​ 所以经过转换后的值应该是这个,所以转换前的值是 4 3 2 1 6 5

可以进行爆破,排列组合就720种

1
2
3
4
5
6
7
8
9
10
root@VM-24-10-ubuntu:/home/ubuntu/csapp/boom/bomb# ./bomb file
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
Halfway there!
So you got that one. Try this one.
Good work! On to the next...
4 3 2 1 6 5
Congratulations! You've defused the bomb!

隐藏关卡

1
2
3
4
5
6
7
8
9
10
11
if ( num_input_strings == 6 )
{
if ( (unsigned int)__isoc99_sscanf(&unk_603870, "%d %d %s", &v1, &v2, v3) == 3
&& !(unsigned int)strings_not_equal(v3, "DrEvil") )
{
puts("Curses, you've found the secret phase!");
puts("But finding it and solving it are quite different...");
secret_phase();
}
puts("Congratulations! You've defused the bomb!");
}

​ 可以看到,进入隐藏关卡需要num_input_strings == 6,从read_line函数可以知道,这个值是每过一关+1的,所以也就是过了第六关后开启.

​ 需要获取unk_603870处的值,并且v3需要等于DrEvil,那么我们怎么修改unk_603870的值呢?

​ 在read_line函数中的skip函数中能够看到,我们每次输入的值,其实都是输入到了80*num_input_strings + 0x603780这个位置,

​ 而num_input_strings是每过一关+1的,0x603870 - 0x603780 = 240,也就是过了3关后,第四关写入的值,就写入到这里了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char *skip()
{
char *v0; // rax
char *v1; // rbx

do
{
v0 = fgets((char *)(80LL * num_input_strings + 6305664), 80, infile);
v1 = v0;
}
while ( v0 && (unsigned int)blank_line(v0) );
return v1;
}

6305664 = 0x603780

​ 所以在第四步后面加上DrEvil就可以了7 0 DrEvil 因为在第四步求解的时候是取前两个值,所以不影响正常的解答

​ 然后就进入下一步了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 secret_phase()
{
const char *line; // rax
unsigned int v1; // ebx

line = read_line();
v1 = strtol(line, 0LL, 10);
if ( v1 - 1 > 0x3E8 )
explode_bomb();
if ( (unsigned int)fun7(&n1, v1) != 2 )
explode_bomb();
puts("Wow! You've defused the secret stage!");
return phase_defused();
}

strtol 把参数 line 所指向的字符串根据给定的 base 转换为一个长整数

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall fun7(__int64 a1, __int64 a2)
{
__int64 result; // rax

if ( !a1 )
return 0xFFFFFFFFLL;
if ( *(_DWORD *)a1 > (int)a2 )
return 2 * (unsigned int)fun7(*(_QWORD *)(a1 + 8), a2);
result = 0LL;
if ( *(_DWORD *)a1 != (_DWORD)a2 )
return 2 * (unsigned int)fun7(*(_QWORD *)(a1 + 16), a2) + 1;
return result;
}

​ 最终要拿到2, 可以是2*1 所以先进入上面那个if,然后下面的if, 第三次调用fun7要得0

​ 所以

​ 1.a2先< 0x24 然后 a1+8的值是8,

​ 2.a2要>8进入第二个分支

​ 3.然后通过相等得到0, a1+0x16的话,就得到了0x16, 所以是0x16!

1
2
3
4
5
6
7
8
9
10
11
12
13
root@VM-24-10-ubuntu:/home/ubuntu/csapp/boom/bomb# ./bomb file
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
Halfway there!
So you got that one. Try this one.
Good work! On to the next...
Curses, you've found the secret phase!
But finding it and solving it are quite different...
22
Wow! You've defused the secret stage!
Congratulations! You've defused the bomb!

问题

调试文件,如果没有这个文件呢/?