0x01 前言

最近在学习csapp时,发现第三章作业是一个程序叫bomb,要求破解六个步骤拆除炸弹。bomb是一个elf文件,我们用ida64分析,然后打开kali虚拟机远程调试,准备工作就做好了。

0x02 前五个部分

炸弹的前五个部分十分简单。第一部分就是比较一个输入的字符串,而这个字符点开加密函数phase_1就可以找到。

1
Border relations with Canada have never been better.

第二部分是输入6个数字,然后数字前后直接要满足后面一个是前面一个两倍的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall phase_2(__int64 a1)
{
__int64 result; // rax
char *v2; // rbx
int v3; // [rsp+0h] [rbp-38h] BYREF
char v4; // [rsp+4h] [rbp-34h] BYREF
char v5; // [rsp+18h] [rbp-20h] BYREF

read_six_numbers(a1, &v3);
if ( v3 != 1 )
explode_bomb(a1, &v3);
v2 = &v4;
do
{
result = (2 * *(v2 - 1));
if ( *v2 != result )
explode_bomb(a1, &v3);
v2 += 4;
}
while ( v2 != &v5 );
return result;
}

输入首项为1,公比为2的等比数列前六项即可。

1
1 2 4 8 16 32

第三部分是输入两个数字,然后第一个数字作为switch的索引来查找一个数字,再把这个数字和输入的第二个数字比较,相同即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
__int64 __fastcall phase_3(__int64 a1)
{
__int64 v1; // rdx
__int64 v2; // rcx
__int64 result; // rax
int v4; // [rsp+8h] [rbp-10h] BYREF
int v5; // [rsp+Ch] [rbp-Ch] BYREF

if ( __isoc99_sscanf(a1, "%d %d", &v4, &v5) <= 1 )
explode_bomb(a1, "%d %d", v1, v2);
switch ( v4 )
{
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(a1, "%d %d", v1, v2);
}
if ( result != v5 )
explode_bomb(a1, "%d %d");
return result;
}

我这里随便找一个组合输入

1
7 327

第四部分是一个二分查找的递归函数,满足的条件是return 0,这里发现只要a1小于一次中值v3,result就会被赋值0,之后递归回溯时就会把result=0返回出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//加密函数
__int64 __fastcall phase_4(__int64 a1)
{
__int64 v1; // rdi
__int64 result; // rax
unsigned int v3; // [rsp+8h] [rbp-10h] BYREF
int v4; // [rsp+Ch] [rbp-Ch] BYREF

if ( __isoc99_sscanf(a1, "%d %d", &v3, &v4) != 2 || v3 > 0xE )
explode_bomb(a1, "%d %d");
v1 = v3;
result = func4(v3, 0LL, 14LL);
if ( result || v4 )
explode_bomb(v1, 0LL);
return result;
}
//递归函数
__int64 __fastcall func4(__int64 a1, __int64 a2, __int64 a3)
//a2初始是0,a3初始是14
{
int v3; // ecx
__int64 result; // rax

v3 = (a3 - a2) / 2 + a2;
if ( v3 > a1 )
return 2 * func4(a1, a2, (v3 - 1));
result = 0LL;
if ( v3 < a1 )
return 2 * func4(a1, (v3 + 1), a3) + 1;
return result;
}

这里需要输入两个值,第一个值v4必为0,第二个值v3我为了保险起见,输入了一个0,让上述二分查找里a1,也就是v3传进去的值能一直小于中值,然后result赋值为0接着返回。

1
0 0

第五部分是个查找表的加密,输入长度为6的字符串,然后取每个字符的二进制前四位掩码,作为数组array_3449的下标,依次和字符串flyers比较。这里通过找表发现下标顺序是9 F E 5 6 7,只要依次输入低四位是这些数字的字符就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
unsigned __int64 __fastcall phase_5(__int64 a1, const char *a2)
{
__int64 i; // rax
char v4[8]; // [rsp+10h] [rbp-18h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-10h]

v5 = __readfsqword(0x28u);
if ( string_length() != 6 )
explode_bomb(a1, a2);
for ( i = 0LL; i != 6; ++i )
v4[i] = array_3449[*(a1 + i) & 0xF];
v4[6] = 0;
if ( strings_not_equal(v4, "flyers") )
explode_bomb(v4, "flyers");
return __readfsqword(0x28u) ^ v5;
}
//数组array_3449(索引表)
.rodata:00000000004024B0 ; char array_3449[16]
.rodata:00000000004024B0 6D array_3449 db 6Dh ; DATA XREF: phase_5+37↑r
.rodata:00000000004024B1 61 db 61h ; a
.rodata:00000000004024B2 64 db 64h ; d
.rodata:00000000004024B3 75 db 75h ; u
.rodata:00000000004024B4 69 db 69h ; i
.rodata:00000000004024B5 65 db 65h ; e
.rodata:00000000004024B6 72 db 72h ; r
.rodata:00000000004024B7 73 db 73h ; s
.rodata:00000000004024B8 6E db 6Eh ; n
.rodata:00000000004024B9 66 db 66h ; f
.rodata:00000000004024BA 6F db 6Fh ; o
.rodata:00000000004024BB 74 db 74h ; t
.rodata:00000000004024BC 76 db 76h ; v
.rodata:00000000004024BD 62 db 62h ; b
.rodata:00000000004024BE 79 db 79h ; y
.rodata:00000000004024BF 6C db 6Ch ; l

我这里输入 69 6F 6E 65 66 67

1
ionefg

0x03 最后一部分

最后一部分调了我一个傍晚,发现是个链表加密

image-20240309192021180

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
__int64 __fastcall phase_6(__int64 a1)
{
int *v1; // r13
int v2; // r12d
int v3; // ebx
char *v4; // 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 num[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[40]; // [rsp+50h] [rbp-28h] BYREF

v1 = num;
read_six_numbers(a1, num);
v2 = 0;
while ( 1 )
{
if ( (*v1 - 1) > 5 ) // 第一个大于6
explode_bomb(a1, num);
if ( ++v2 == 6 )
break;
v3 = v2;
do
{
if ( *v1 == num[v3] ) // 如果第一个和最后一个相等
explode_bomb(a1, num);
++v3;
}
while ( v3 <= 5 ); // v3=6
++v1;
} // 检查6个数字是否互不相等
v4 = num;
do
{
*v4 = 7 - *v4;
v4 += 4;
}
while ( v4 != &v16 ); // 7-输入数字
for ( i = 0LL; i != 24; i += 4LL )
{
v8 = num[i / 4];
if ( v8 <= 1 )
{
v6 = &node1;
}
else
{
v7 = 1;
v6 = &node1;
do
{
v6 = v6[1];
++v7;
}
while ( v7 != v8 );
}
*(&v17 + 2 * i) = v6; // v17是各节点地址数组,循序是输入的1到6的顺序
}
v9 = v17; // 第一个
v10 = &v18; // 第二个
for ( j = v17; ; j = v12 )
{
v12 = *v10; // 第二个的值
*(j + 8) = *v10;
v10 += 8; // 第三个
if ( v10 == v19 )
break;
}
*(v12 + 8) = 0LL;
v13 = 5;
do
{
result = **(v9 + 8); // result的值是该节点指向的节点的值
if ( *v9 < result )
explode_bomb(a1, v19);
v9 = *(v9 + 8); // 设置为下一个节点
--v13;
}
while ( v13 );
return result;
}

这坨式主要的逻辑就是,输入1到6的数字,然后取关于3.5的对称数,接着根据1到6的代表节点1到6,来重置链表的顺序,最后进行一个某节点与下一个节点的比较,若小于则程序退出,于是我们就要使得链表的顺序重置为从大到小排列。(其实是非常简单的逻辑,第一次见链表式存储的逆向给我卡了,调试后才发现)

根据逆向原理,我们得先找到排序然后再取数的对称。找到6个节点各自的值

1
2
3
4
5
6
1    2  3    4    5    6
014c a8 039c 02b3 01dd 01bb
//排序
3 4 6 5 2 1
//取对称
4 3 2 1 6 5

这样就把炸弹拆除了。

0x04 后续

其实这个程序,我感觉作者的本意应该是让我们在机器语言或者汇编语言层面调试分析,奈何ida工具太强大了。