观察之后还可以发现有所谓的secret_phase
和func7
之类的东西,但是感觉差不太多了也就不太想做...就这样吧
前置姿势/技能
这里记一下做lab需要的前置姿势/技能,尽量不和理论方面的东西重叠吧...
编译实际上就是把源代码变成一堆CPU指令的过程(在这里到这个程度就够了)
GCC使用的是叫ATT
格式的汇编,和Intel
格式的汇编有很多不一样,最显著的就是操作元素的顺序...看文档的时候要小心
编译产生.s
文件:
gcc -Og -S file.c
编译产生.o
文件:
gcc -Og -c file.c
以上-Og
是为了避免过度优化使得程序的执行顺序和结构发生变化
反汇编得到一堆指令:
objdump -d file
gdb打印寄存器内容:
i registers
gdb打印内存内容:
x /<n/f/u> <addr>
n、f、u 为可选参数,其中n 表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容;f 表示显示的格式(b);u 表示将多少个字节作为一个值取出来,如果不指定的话,GDB默认是4个bytes,如果不指定的话,默认是4个bytes。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。
为起始内存地址。
参数 f 的可选值:
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。
参数 u 的可选值:
b 表示单字节
h 表示双字节
w 表示四字节
g 表示八字节
上面的内容摘自GDB查看内存
这个非常有用,事实上知道了这个我才做出了Phase_1,用了这么久gdb还是只会fileqlsrb真是惭愧...
打印PC
寄存器往后5条指令,在做lab3的时候比较有用
x /5i $pc
有些程序运行需要参数,就可以用这个命令来设置
set args xxx
Phase_1
终于做完了第一个QUQ
读代码
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi ; 这里给出了第二个参数,实际上就是要和input比较的字符串常量
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal>
400eee: 85 c0 test %eax,%eax
400ef0: 74 05 je 400ef7 <phase_1+0x17> ; 如果返回值是0则安全
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb>
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 retq
重点在于函数名strings_not_equal和400ee4的位置给出了%esi
我们知道%rdi和%rsi表示传入的前两个参数,而%rdi是输入的串的起始字符的地址,那么%rsi就是目标串的起始字符的地址了
知道了这个地址就可以进GDB扫内存,这里扫出来结果是
(gdb) x /60db 0x402400
0x402400: 66 111 114 100 101 114 32 114
0x402408: 101 108 97 116 105 111 110 115
0x402410: 32 119 105 116 104 32 67 97
0x402418: 110 97 100 97 32 104 97 118
0x402420: 101 32 110 101 118 101 114 32
0x402428: 98 101 101 110 32 98 101 116
0x402430: 116 101 114 46 0 0 0 0
0x402438: 87 111 119 33
很显然到0就停止了,剩下的就是把这个字符序列由ascii码变成字符就完了,现在看看不是太难的...难点在于不要被绕晕
Phase_2
有了第一个铺垫第二个就比较简单了(bushi)
还是先贴代码,这里把关键的循环抠出来了
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp)
400f0e: 74 20 je 400f30 <phase_2+0x34> ; if (%rsp)==1 safe, a[1]=1
400f10: e8 25 05 00 00 callq 40143a <explode_bomb>
400f15: eb 19 jmp 400f30 <phase_2+0x34>
400f17: 8b 43 fc mov -0x4(%rbx),%eax
400f1a: 01 c0 add %eax,%eax
400f1c: 39 03 cmp %eax,(%rbx)
400f1e: 74 05 je 400f25 <phase_2+0x29> ; if 2*a[i]!=a[i+1] bomb
400f20: e8 15 05 00 00 callq 40143a <explode_bomb>
400f25: 48 83 c3 04 add $0x4,%rbx
400f29: 48 39 eb cmp %rbp,%rbx
400f2c: 75 e9 jne 400f17 <phase_2+0x1b>
400f2e: eb 0c jmp 400f3c <phase_2+0x40>
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp
400f3a: eb db jmp 400f17 <phase_2+0x1b>
同样函数名给的提示很多,看过课程视频/看过书都应该知道只有前6个函数参数存在寄存器里,多余的参数都会都按照逆序插入到栈中(why?这样弹出的第一个就是最前面的参数,以此类推)
难点在于搞清楚内存分部和内存里储存的值究竟是什么,以及sscanf这个函数的返回值....
因此只要画出内存的地址&内容表格来就可以很简单地做出来了,答案是一个长为6的满足某些常数项递推条件的数列
Phase_3
感觉这个比前两个都要简单了
观察实现可以发现是对参数%rdi作0~7的switch-case判断,不同的%rdi对应不同的值%eax
炸弹不爆当且仅当%rsi=%eax,因此正确的输入有8对.我试了两个都是可以的,剩下的就没试了(太困了)
关键在这一句
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8) ; jump to mem[0x402470+x*8]
用GDB硬扫就可以了,注意用/8xg+地址来扫
Phase_4
func_4
写出来类似这样子,看上去是个递归实际上观察phase_4的初始条件可以知道最开始带入的x=tx,y=0,z=14
这里tx,ty表示读入的两个数字,再观察到所谓的q<=x且q>=x就是解一个简单的方程的条件,算出答案就可以了
int func4(int x,int y,int z) {
int t = z - y;
int q = ( (unsigned) t) >> 31;
t += q;
t >>= 1;
q = t + y;
if (q <= x) {
if (q >= x) return 0;
return 2 * func4(x, q + 1, z) + 1;
} else {
return 2 * func4(x, y, q - 1);
}
}
phase_4的部分关键在这些地方
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp);if *tx<14 bomb
401033: 76 05 jbe 40103a <phase_4+0x2e>
401035: e8 00 04 00 00 callq 40143a <explode_bomb>
40103a: ba 0e 00 00 00 mov $0xe,%edx;z=14
40103f: be 00 00 00 00 mov $0x0,%esi;y=0
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi;x=*tx
401048: e8 81 ff ff ff callq 400fce <func4>
40104d: 85 c0 test %eax,%eax;if func4(tx,0,14)!=0 bomb
40104f: 75 07 jne 401058 <phase_4+0x4c>
Phase_5
一开始被%fs这个寄存器吓到了,往后看书才知道这个就是所谓的canary机制,在这里可以不用管~
剩下的就不是太难。这个函数主要实现了通过一个循环来获取一个常字符串的某些位置得到一个新的串,并将这个串和串"flyers"进行比较
关键是这个循环部分,看懂就非常简单
401089: eb 47 jmp 4010d2 <phase_5+0x70>
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx;此时rax为0,rbx为str,故rcx=*str
40108f: 88 0c 24 mov %cl,(%rsp);mem[rsp]=str[0]
401092: 48 8b 14 24 mov (%rsp),%rdx;rdx=str[0]
401096: 83 e2 0f and $0xf,%edx;rdx&=15,此时rdx<=15
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx;
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1)
;mem[rsp+0x10+rax]=mem[0x4024b0+rdx]
4010a4: 48 83 c0 01 add $0x1,%rax;rax+=1
4010a8: 48 83 f8 06 cmp $0x6,%rax;
4010ac: 75 dd jne 40108b <phase_5+0x29>;while
Phase_6
最后一题有点难度
首先可以把代码分成四块。
第一部分程序读入了6个数字,并检查它们是否全都不一样(否则就炸)且都在1到6之间
第二部分程序把每个a[i]都变成7-a[i]
第三部分实现了令c[i]=b[a[i]],其中b是一个常量数组,c是一个结果
第四部分实现了判断c是否递减,否则就炸
1、2都很简单,4在搞清楚3之后也很简单
;begin loop;
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx
40117a: 83 c0 01 add $0x1,%eax
40117d: 39 c8 cmp %ecx,%eax
40117f: 75 f5 jne 401176 <phase_6+0x82>
;end loop
观察这段代码,%ecx储存了a[i],它实际上实现了找到b中的第a[i]个数,并取出它的位置储存在%rdx中的功能
其中
4011a4: ba d0 32 60 00 mov $0x6032d0,%edx
给出了b数组的起始位置,那么我们在gdb中扫就可以了,出来的结果是这样的
第一列就是b中的值
这时候再看第四部分,可以发现这是在遍历c中的6个数字观察它们是否按照一定顺序(降序)排好了
现在就很好猜了,第三部分首先用a[i]找到第a[i]个b中的元素,再把它存储到c[i]中。中间有点绕是因为程序对第一个元素做了特殊处理,编译器对默认情况的优化有点诡异...
所以对着图里的数字排序就好了,这样就做完了Phase_6