CSAPP:BombLab
通读main函数,可以轻易发现每个阶段是调用phase_n
函数进行。
Phase 1
1 | phase_1 |
main
函数调用phase_1
时从read_line
函数读取了一行输入,将char *
存入了rdi
中。strings_not_equal
的第一个参数也是通过rdi
传递,所以phase_1
没有修改rdi
。
然后phase_1
从rodata
段,也就是系统mmap
程序到内存的位置,读取字符串,将指针放入esi
中,作为strings_not_equal
的第二个参数。strings_not_equal
函数负责比较字符串,具体实现省略,主要覆盖了一下几种情况的比较
- 首先判断字符串是否等长
- 然后对比每个字符串,直到字符串结束
strings_not_equal
在字符串不相等的情况下返回1,相等的情况下返回0。test
指令对其进行判断,如果是零,则跳转到返回块,如果不是,炸弹爆炸。
Phase 2
1 | ; __int64 __fastcall phase_2(__int64) |
首先调用了read_six_numbers
函数,获得六个数字。如果第一个数字不是1,则引爆炸弹。
如果是1,跳转到0x400F30
的代码块。
实际上单独的那一个jmp指令,永远不是执行,因为explode_bomb
会使进程退出。但是编译器无法判断,所以会添加上一个兜底的跳转。代码结构如下所示(仅为概念演示)
1 | if (first_number != 1) |
通过lea
获取了栈上的空间,然后跳转到0x400F17
的代码块。rbx
指向当前遍历到的数字(初始是第二个),把上一个的结果复制到eax
中,eax
加上自身,也就是乘以2,和当前的数字比较,不同则引爆炸弹。
循环块不深入解释
Phase 3
1 | jpt_400F75 ; jump table for switch statement |
p3是一个经典的switch
结构,sscanf
复制读取两个数字,第一个数字作为jtable
的偏移,第二个数字在case
中比较,由于有8个case
,所以有8个答案
Phase 4
1 | ; __int64 __fastcall func4(__int64, __int64, int) |
p4调用一个递归函数,读取两个数字,第一个作为递归的参数,第二个与递归结果做运算。两者取或结果为0则通过,不为0则爆炸。
可以得到递归期望返回一个0,第二个参数也应该是0。
还原递归的代码如下
p4中指定了a2初始为0,a3初始为14
1 | int func4(int a1, int a2 = 0, int a3 = 14) |
分析递归三要素可以得到答案,或者直接编译这个函数进行暴力枚举,也可以得到答案。
其实还可以通过dlopen
打开这个二进制文件,然后dlsym
可以拿到这个extern C
函数,直接调用,不需要还原代码。
Phase 5
1 | array_3449 ; DATA XREF: phase_5+37↑r |
p5读取6个字符长度的字符串,将这个字符串每一位与0xF
做与运算,得到的结果在array_3449
中作为偏移,最后得到一个字符串,并和flyers
这个字符串进行对比,相同则通过,不同则引爆炸弹。
主要逻辑在0x40108B
代码块rax
作为循环变量,每次循环加一,遍历长度为5,加到6终止循环。rdx
作为array_3449
的偏移,计算结果会保存到这个寄存器。栈加上0x10
是新字符串储存的位置。strings_not_equal
前面已经分析过,不再分析。
Phase 6
1 | node1 |
p6也读取了6个数字。
前面看起来很吓人,其实就是判断你输入的数据有没有重复或者超过了6。用了两个嵌套的循环,很常规的算法。loc_401153
才是真正代码开始的地方。
首先还是一个循环,这个循环遍历了整个输入的数组,然后将7减去这些数字,得到一个与原来的输入在数轴上关于3.5对称的新数组。
接着又是一个循环,循环变量在rsi
中,rsi
从0变到24,步进为4,使用时又除以4,实际上就是rsi
从0变到6。rsi
作为新数组的偏移。
接着将数组中的当前数字移入ecx
中,如果ecx
小于等于1,则给edx赋值为node1在内存中的地址。如果大于1,则又是一个循环,给edx
赋值为node1
的地址,然后把node1+8
赋值给edx
,直到这个循环变量(eax
)等于ecx
。
通过观察上面的操作,可以发现node{n}
第一个数据为8字节长的数字,第二个为8字节长的偏移,也就是下一个node
,实际上就是一个链表。
知道了这是一个链表,下面就好做了。
每次rsi
的循环每次都会在栈上保存按照新数组顺序找到的node{n}
接下来这个循环对得到的新链表的next
字段进行了重构,使其指向物理地址的下一个node{n}
。
接下来的循环则对新链表进行遍历,ebp
作为循环变量,从5减到0,node
只有6个,刚好遍历到倒数第二个时循环结束。
这个循环首先将当前遍历的node
的下一个读取到eax
,再和当前的进行比较,如果下一个比当前的大,则引爆炸弹。
所以最后可以得到答案就是一个数组,将7减去每一位,得到新的索引,通过新的索引得到新的链表,这个链表需要是递增的。
Secret Phase
1 | n1 |
隐藏关我感觉解密比入口简单,入口隐藏比较深。这个阶段我更建议用gdb去debug phase_defused
。这里进行静态分析,就不走捷径了。
首先在整个程序的汇编中其实就可以找到secret_phase这个函数,也能看到,只有在phase_defused
中有调用,那么入口肯定在phase_defused
中。
在phase_defuesd中判断了num_input_strings
是否等于6,这是一个全局变量,在bss
段,每次调用read_line
成功都会将这个值加上1。
在phase_defused
中读取了0x603870
的数据,但是查找别的地方,没有出现这个地址。目光再回到read_line
,有一段关键的代码。
1 | mov edx, cs:num_input_strings |
这段代码将num_input_strings
乘以5(lea
),再左移4位,也就是乘以2^4
,得到80*num_input_strings
,最后加上0x603780
。
稍微计算一下可以得到当num_input_strings
等于3的时候,得到的结果刚好是0x603870
。
而read_line
返回的就是上面的结果,这个结果作为调用每一个阶段的参数,所以触发隐藏阶段就要在第三个阶段的答案后面加上对应的触发。
这个触发是在phase_defused
中判断的,与p1一样,能看到这个触发应该是一个字符串DrEvil
。
分析pahse_deused
,读取了一个数字,传递给fun7
,而fun7
也是一个递归函数,直接还原代码。
分析递归三要素得到答案,也可以像上面提到的编译函数暴力枚举,或者通过dlopen
外部调用拿到n1
的地址和fun7
的地址进行暴力枚举。
其实观察一下可以发现n1是一个二叉树的节点,整个二叉树一共三层,不过看不出来二叉树也能解出答案 ; )
1 | int fun7(BinaryTreeNode* a1, int a2) |
答案
/
是多个答案的分割符,选择其中一个即可
1 | Border relations with Canada have never been better. |