CSAPP的lab2-bomblab
这个lab2真的做了很久,做的久,解析写的也久,还抽了部分打题的时间写的,这个lab是真的有意思
前置知识:
- gdb使用指北
- 汇编语言的基本语法
- 链表
- 递归函数
- 耐得住诱惑
phase_1
1 | 0x0000000000400ee0 <+0>: sub $0x8,%rsp |
phase_1算是热身的一关,主要就是要发现到0x402400这个特殊的内存地址,毕竟默认下第一个参数是%rdi,那么第二个参数就是%rsi,有充分的理由怀疑,是在
1 | p(char*)0x402400 |
当热身了.
1 | 40135c: 0f b6 03 movzbl (%rbx),%eax ; 将 %rbx 指向的字节加载到 %eax 中,并进行零扩展为32位 |
phase_2
1 | 0x0000000000400efc <+0>: push %rbp ; 将 %rbp 寄存器的值推送到栈上 |
这是一个简单的倍增循环,需要知道stack的概念.
一开始能够简单的确定stack上的第一个元素与1是相等的
然后把第二个元素的地址加载到rbx上,因为里面存的是int,int是4个bit,所以每次加四,然后把前一个值加倍看他是否与当前值相等.
故六次循环后我们会发现每次循环能不断推断出第一个,第二个,直到第六个元素,为倍增关系
故答案: 1 2 4 8 16 32
代码混淆解释
1 | rbx=rsp+4 //lea 0x4(%rsp),%rbx |
phase_3
人工打了跳转标记
1 | 0000000000400f43 <phase_3>: |
gpt解读
1 | 0000000000400f43 <phase_3>: |
_isoc99_sscanf
1 | 其转换了含有几个整数的字符串则返回值是几 |
0x0(%rsp)
是栈顶位置,通常是函数的返回地址。0x4(%rsp)
是栈顶位置向下偏移4字节的位置,可能是一个局部变量或参数。
在这么多条分支里面找到一条能成立的就可以了
1 | jmpq *0x402470(,%rax,8) |
这个函数是重点,这是跳转表间接实现switch操作,先确定输入的第一个变量在[0,7]之间,首先用gdb指令确定*0x402470的值,假定其会跳转到第一个分支,即令第一个变量等于0,刚好能跳转到第一个分支,那想要炸弹不爆炸只能%eax=207,所以其中一种答案就算出来了
答案 0 207
phase_4
1 | ;设第一个数字为a,第二个数字为b |
现在我们要确定数字a,b,已知b=0,故要确定a的值
补充资料
1 | rdi:第一个参数 |
1 | ;rdi=a;edx=e;esi=0 |
观察如何让rax=0;
把它按行翻译为c++代码,然后跑一遍就好了
1 | //int rdi=x,edx=a1,esi=a2,rcx=tmp |
测试代码
1 |
|
然后我们就能够很清楚的知道四个数字:0 1 3 7
所以答案有四种:
- 0 0
- 1 0
- 3 0:
- 7 0
phase_5
1 | 0000000000401062 <phase_5>: !# %fs:0x28 -> 3678849592732380416 这是一个保护堆栈的值 |
啥逼玩意能通过偏移量达到flyers,取末尾四位,在来一遍这个地址
把这两个地址的所含字符串输出出来
1 | mov $0x40245e,%esi |
能找到这两个字符串
问题串:aduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?
转换后的串:flyers
1 | 40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx !# ecx = *rbx + *rax = *rdi + *rax 即第rax个字符 |
即每次取输入字符串的一个字符,取其低四位,以此为索引,在问题串中找对应偏移量的字符,存入stack中,循环六次后,和转换后的串一比较,相等即通过
偏移公式:
1 | movzbl 0x4024b0(%rdx),%edx;0x4024b0是问题串的首位置 |
然后对着ascII码表算一算
- 9>>Y
- 15>>o
- 14>>n
- 5>>e
- 6>>f
- 7>>g
答案Yonefg(其中一种)
phase_6
设数串为a,b,c,d,e,f
1 | 0x00000000004010fc <+8>: sub $0x50,%rsp |
找出了题目给的的数组(结构体的值)
1 | (gdb) p 0x0000014c |
小于等于:降序
得到数据: 3 4 5 6 1 2
然后再把它恢复到7-int之前则是 4 3 2 1 6 5
注意这个链表,发现他有三个值,一个是val,一个是order,一个是指向下一个的指针,例如node1.next==>node2
1 | struct node{ |
所以捋顺一遍思路
- 通过双层循环,判断6个数是[1,6]中的,并且各不相同
- 对六个数字取7的补,得到a1,b1,c1,d1,e1,f1
- 再根据取补后的数字,搬迁链表,把他按一个新的顺序排列
- 之后程序会对新的链表旅顺,让他能够遍历
- 判断下面的数都大于上面的数
第3点是第五点的关键,我们知道链表一开始的排列,要把它安排成从上往下增大的形式,所以我们第一个要把924对应的node3丢过去,有规律可知,当a1=3时会找到node3,把他搬迁过去,以此类推知道了a1-f1的顺序分别是3 4 5 6 1 2
再取补就可得到原数组 4 3 2 1 6 5
secret_phase
在网上看了看发现了,这是phase_defused的反汇编函数,然后看他有一个secret_phase的调用
1 | 00000000004015c4 <phase_defused>: |
我们通过defuse_phase知道了secret的调用并且猜到了其中一个s密码,现在要找剩下的两个%d,然后发现他调用的寄存器时phase_4的,所以他们用的是同一个输入,我们直接在答案7 0 后面加一个 DrEvil ,这样就成功的进入这个secret_phase了
secret_phase
1 | 0000000000401242 <secret_phase>: |
这里面strtol的函数原型是
1 | long int strtol(const char *str,char **endptr,int base)//分别是rdi,rsi,rdx |
这里面edx=10,所以strtol会对一串字符串读取其中的前面一段连续数字,然后转化成十进制
在根据10,11行可得知eax内的值<=1000
分析可知,fun_7的返回值必须得是2才能通过
fun_7
1 | 0000000000401204 <fun7>: |
我直接挑一个地方让esi的值等于edi的值不就好了,应为fun7函数里面esi的值不变,涉及到esi的只有比较,所以我找个地方,把rax整成2,然后找到那个时候的edi的值就行了
输入命令,发现是一个链表
1 | 0x6030f0 <n1>: 0x0000000000000024 0x0000000000603110 |
大家通过代码的翻译,能梳理出以下几点
目的:这个递归是对链表的递归,每一个节点都有两个指针,所以每个节点都对应着另外两个节点,我们把这个节点根据指针的顺序画出一颗树,把指针1放左边,指针二放右边,得到如下一张图.
注:左下是指针一的节点,右边=下是指针2的节点
然后梳理一遍代码规则(val为节点的值,num为输入的数字)
- 若val<num ,走右边的节点,return 时的返回值要*2+1
- 若val>num,走左边的节点,return 时的返回值要*2
- 若val=num,return 0
然后我们一步一步的令num=every val,把return的值算出来,确定是2的情况就好了
答案:0x14 和 0x16