CSAPP LAB2 BombLab
给一个可执行文件,需要按顺序输入6个字串,每个字串是一个小炸弹的密码。通过反汇编找出每个字串的内容。
考察汇编语言,gdb的使用。
关于GDB的使用
终端下输入
gdb
进入GDB,使用file <文件名>
加载二进制文件,加载后只是读取符号表,并不执行。使用set args <参数>
给程序设置参数,可以为空(我遇到了没设置参数,但参数不为空的情况>_<,所以还是清一下参数比较好)。quit
命令退出gdb。run
命令执行程序,kill
命令杀死程序。step <数字i>
为执行i条指令,类似单步执行,如果不填参数默认为1,类似vs的F11。nexti
为执行一行,不进入函数,类似vs的F10。continue
为执行到下一断点,类似vs的F5。finish
为跳出函数,类似vs的Shift+F11。disas <参数>
为反汇编指令,根据参数不同:- 参数为函数名,则反汇编对应函数
disas main
- 参数为一个地址,则反汇编对应地址附近的函数
disas 0x400e4e
- 参数为函数名,则反汇编对应函数
print <参数>
为查看指令,根据参数不同可以查看寄存器,内存等:参数为寄存器名,输出寄存器内容,按十进制输出:
print $rax
;按十六进制输出:print /x $rax
可以使用GDB的”@”操作符查看连续内存,”@”的左边是第一个内存的地址的值,”@”的右边则你你想查看内存的长度。例如,对于如下代码:
int arr[] = {2, 4, 6, 8, 10}
,可以通过如下命令查看arr前三个单元的数据:
(gdb) p *arr@3
$2 = {2, 4, 6}
examine /<n/f/u> <addr>
命令来查看内存地址中的值:- n 表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。
- f 表示显示的格式,如果是字符串,则用s,如果是数字,则可以用i。
- u 表示从当前地址往后请求的字节数,默认是4个bytes。(b单字节,h双字节,w四字节,g八字节)
<addr>
表示一个内存地址。
炸弹1
先看main函数,发现可以指定一个文件作为stdin,便于储存每个密码。炸弹是由6个小炸弹串联起来,需要依次解除。那么先看炸弹1。
/* Hmm... Six phases must be more secure than one phase! */
input = read_line(); /* Get input */
phase_1(input); /* Run the phase */
phase_defused(); /* Drat! They figured it out!
* Let me know how they did it. */
printf("Phase 1 defused. How about the next one?\n");
read_line
读入一行文字,作为参数交到phase_1里面,反汇编phase_1:
Dump of assembler code for function phase_1:
0x0000000000400ee0 <+0>: sub $0x8,%rsp
0x0000000000400ee4 <+4>: mov $0x402400,%esi
0x0000000000400ee9 <+9>: callq 0x401338 <strings_not_equal>
0x0000000000400eee <+14>:test %eax,%eax
0x0000000000400ef0 <+16>:je 0x400ef7 <phase_1+23>
0x0000000000400ef2 <+18>:callq 0x40143a <explode_bomb>
0x0000000000400ef7 <+23>:add $0x8,%rsp
0x0000000000400efb <+27>:retq
End of assembler dump.
首先说明一下,函数开始会给自己分配栈空间,减小栈指针,在结束(8行)又会释放自己的栈空间,增加栈指针。又知道main调用这个函数时传了一个参数,他在1号参数寄存器%rdi
中,第三行将0x402400
存入寄存器%esi
,是2号参数寄存器,在调用strings_not_equal
函数。表明传入两个字符串,判断他们相不相等。%eax
是返回值寄存器,test+je的组合表明为0则跳转。即字串相等返回0,释放栈,返回main;不等返回1,爆炸。
所以查看比较字串的内容,x/s 0x402400
,得到第一题答案:Border relations with Canada have never been better.
炸弹2
与上面同样的思路,反汇编phase_2
:
Dump of assembler code for function phase_2:
0x0000000000400efc <+0>: push %rbp
0x0000000000400efd <+1>: push %rbx
0x0000000000400efe <+2>: sub $0x28,%rsp
0x0000000000400f02 <+6>: mov %rsp,%rsi
0x0000000000400f05 <+9>: callq 0x40145c <read_six_numbers>
0x0000000000400f0a <+14>: cmpl $0x1,(%rsp)
0x0000000000400f0e <+18>: je 0x400f30 <phase_2+52>
0x0000000000400f10 <+20>: callq 0x40143a <explode_bomb>
0x0000000000400f15 <+25>: jmp 0x400f30 <phase_2+52>
0x0000000000400f17 <+27>: mov -0x4(%rbx),%eax
0x0000000000400f1a <+30>: add %eax,%eax
0x0000000000400f1c <+32>: cmp %eax,(%rbx)
0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41>
0x0000000000400f20 <+36>: callq 0x40143a <explode_bomb>
0x0000000000400f25 <+41>: add $0x4,%rbx
0x0000000000400f29 <+45>: cmp %rbp,%rbx
0x0000000000400f2c <+48>: jne 0x400f17 <phase_2+27>
0x0000000000400f2e <+50>: jmp 0x400f3c <phase_2+64>
0x0000000000400f30 <+52>: lea 0x4(%rsp),%rbx
0x0000000000400f35 <+57>: lea 0x18(%rsp),%rbp
0x0000000000400f3a <+62>: jmp 0x400f17 <phase_2+27>
0x0000000000400f3c <+64>: add $0x28,%rsp
0x0000000000400f40 <+68>: pop %rbx
0x0000000000400f41 <+69>: pop %rbp
0x0000000000400f42 <+70>: retq
End of assembler dump.
第二三行是保存被调用者保存寄存器,第四行分配栈空间,24‘25’26行同理。%rsi
是2号参数寄存器,将栈指针传入。然后调用read_six_numbers
函数,表明从输入的字串(一直在一号参数寄存器)读出6个int,放在栈指针开始的6*4=24(0x18)字节空间内。需要反汇编一下read_six_numbers
函数,会看到里面调用c99_sscanf
函数,第二个参数是个字符串%d %d %d %d %d %d
,这里略过。
(%rsp)
表示访问栈指针处地址指向内存的元素,就是scanf进来的第一个数。7\8\9行表示判断第一个数是不是1,不是就爆炸。跳到了20行。
其实接下来进入一个循环,20\21\22行是初始化循环。%rbx
内存当前地址,%rbp
存退出循环的判定条件。到11行,将(%rbx
-4)的值载入寄存器%eax
,回想初始化的时候将%rbx
初始化为栈指针+4,即第二个int的位置,减4就是上一个int。12行将上一个int乘2,13行与当前int比较,不等就爆炸。16行前移%rbx
,指向下一个int。17行与终止条件比较,注意初始化时%rbp
被初始化为0x18
,十进制是24,表示6个int,不是十进制的18!!!
所以看到,第一个数是1,后面每个是前一个的二倍,所以答案是1 2 4 8 16 32
。之后恢复被调用者保存的寄存器,释放栈,返回。
炸弹3
反汇编phase_3
:
Dump of assembler code for function phase_3:
0x0000000000400f43 <+0>: sub $0x18,%rsp
0x0000000000400f47 <+4>: lea 0xc(%rsp),%rcx
0x0000000000400f4c <+9>: lea 0x8(%rsp),%rdx
0x0000000000400f51 <+14>: mov $0x4025cf,%esi
0x0000000000400f56 <+19>: mov $0x0,%eax
0x0000000000400f5b <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000400f60 <+29>: cmp $0x1,%eax
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39>
0x0000000000400f65 <+34>: callq 0x40143a <explode_bomb>
0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp)
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106>
0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax
0x0000000000400f75 <+50>: jmpq *0x402470(,%rax,8)
0x0000000000400f7c <+57>: mov $0xcf,%eax
0x0000000000400f81 <+62>: jmp 0x400fbe <phase_3+123>
0x0000000000400f83 <+64>: mov $0x2c3,%eax
0x0000000000400f88 <+69>: jmp 0x400fbe <phase_3+123>
0x0000000000400f8a <+71>: mov $0x100,%eax
0x0000000000400f8f <+76>: jmp 0x400fbe <phase_3+123>
0x0000000000400f91 <+78>: mov $0x185,%eax
0x0000000000400f96 <+83>: jmp 0x400fbe <phase_3+123>
0x0000000000400f98 <+85>: mov $0xce,%eax
0x0000000000400f9d <+90>: jmp 0x400fbe <phase_3+123>
0x0000000000400f9f <+92>: mov $0x2aa,%eax
0x0000000000400fa4 <+97>: jmp 0x400fbe <phase_3+123>
0x0000000000400fa6 <+99>: mov $0x147,%eax
0x0000000000400fab <+104>: jmp 0x400fbe <phase_3+123>
0x0000000000400fad <+106>: callq 0x40143a <explode_bomb>
0x0000000000400fb2 <+111>: mov $0x0,%eax
0x0000000000400fb7 <+116>: jmp 0x400fbe <phase_3+123>
0x0000000000400fb9 <+118>: mov $0x137,%eax
0x0000000000400fbe <+123>: cmp 0xc(%rsp),%eax
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134>
0x0000000000400fc4 <+129>: callq 0x40143a <explode_bomb>
0x0000000000400fc9 <+134>: add $0x18,%rsp
0x0000000000400fcd <+138>: retq
End of assembler dump.
看到7行调用sscanf
,得知前面是在为调用做准备。使用x/s 0x4025cf
命令查看字串得到%d %d
,表明是读入两个int。分别存在(%rdx)=(%rsp+8)
与(%rcx)=(%rsp+c)
处。8\9行判断sscanf的返回值,就是读入的int个数。11行判断第一个参数不能大于7,用jg
的无符号数判断,是为了防止负数。
到了14行,jmpq *0x402470(,%rax,8)
,这是一个switch语句,使用比例变址的基址寻址。*0x402470表示实际的基止是该地址的数据,查看该地址的指向的数据,使用:x/s *0x402470
命令,得到0x400f7c <phase_3+57>: "\270", <incomplete sequence \317>
,表明从phase_3+57开始。我们看到,这是一系列的case语句,每个将一个值移入%eax
,在33行将第二个参数与%eax
比较,不等就爆炸。
所以我们得出炸弹3的一个解:0 207
。
炸弹4
反汇编phase_4
,有:
Dump of assembler code for function phase_4:
0x000000000040100c <+0>: sub $0x18,%rsp
0x0000000000401010 <+4>: lea 0xc(%rsp),%rcx
0x0000000000401015 <+9>: lea 0x8(%rsp),%rdx
0x000000000040101a <+14>: mov $0x4025cf,%esi
0x000000000040101f <+19>: mov $0x0,%eax
0x0000000000401024 <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000401029 <+29>: cmp $0x2,%eax
0x000000000040102c <+32>: jne 0x401035 <phase_4+41>
0x000000000040102e <+34>: cmpl $0xe,0x8(%rsp)
0x0000000000401033 <+39>: jbe 0x40103a <phase_4+46>
0x0000000000401035 <+41>: callq 0x40143a <explode_bomb>
0x000000000040103a <+46>: mov $0xe,%edx
0x000000000040103f <+51>: mov $0x0,%esi
0x0000000000401044 <+56>: mov 0x8(%rsp),%edi
0x0000000000401048 <+60>: callq 0x400fce <func4>
0x000000000040104d <+65>: test %eax,%eax
0x000000000040104f <+67>: jne 0x401058 <phase_4+76>
0x0000000000401051 <+69>: cmpl $0x0,0xc(%rsp)
0x0000000000401056 <+74>: je 0x40105d <phase_4+81>
0x0000000000401058 <+76>: callq 0x40143a <explode_bomb>
0x000000000040105d <+81>: add $0x18,%rsp
0x0000000000401061 <+85>: retq
End of assembler dump.
同样的套路,知道sscanf读入两个int。第一个必须0<=x<=14
。然后调用函数func4
,带三个参数,分别是之前驶入的第一个,0,14。先继续往下读,发现func4
的返回值必须为0,且在19行,输入的第二个参数要是0。
反汇编func4
,发现是个递归函数,真的头大。不过我试着避免递归,发现可以得到答案。
Dump of assembler code for function func4:
0x0000000000400fce <+0>: sub $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax
0x0000000000400fd4 <+6>: sub %esi,%eax
0x0000000000400fd6 <+8>: mov %eax,%ecx
0x0000000000400fd8 <+10>: shr $0x1f,%ecx
0x0000000000400fdb <+13>: add %ecx,%eax
0x0000000000400fdd <+15>: sar %eax
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx
0x0000000000400fe2 <+20>: cmp %edi,%ecx
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36>
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx
0x0000000000400fe9 <+27>: callq 0x400fce <func4>
0x0000000000400fee <+32>: add %eax,%eax
0x0000000000400ff0 <+34>: jmp 0x401007 <func4+57>
0x0000000000400ff2 <+36>: mov $0x0,%eax
0x0000000000400ff7 <+41>: cmp %edi,%ecx
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57>
0x0000000000400ffb <+45>: lea 0x1(%rcx),%esi
0x0000000000400ffe <+48>: callq 0x400fce <func4>
0x0000000000401003 <+53>: lea 0x1(%rax,%rax,1),%eax
0x0000000000401007 <+57>: add $0x8,%rsp
0x000000000040100b <+61>: retq
End of assembler dump.
列出寄存器表:
%eax | %ecx | %edi | %esi | %edx | |
---|---|---|---|---|---|
x | 0 | ||||
0x0400fce <+0>: | sub $0x8,%rsp | ||||
0x0400fd2 <+4>: | mov %edx,%eax | 14 | |||
0x0400fd4 <+6>: | sub %esi,%eax | 14 | |||
0x0400fd6 <+8>: | mov %eax,%ecx | 14 | |||
0x0400fd8 <+10>: | shr $0x1f,%ecx | 0 | |||
0x0400fdb <+13>: | add %ecx,%eax | 14 | |||
0x0400fdd <+15>: | sar %eax | 7 | |||
0x0400fdf <+17>: | lea (%rax,%rsi,1),%ecx | 7 | |||
0x0400fe2 <+20>: | cmp %edi,%ecx | ||||
0x0400fe4 <+22>: | jle 0x400ff2 \func4+36> | x<=7 | |||
0x0400fe6 <+24>: | lea -0x1(%rcx),%edx | ||||
0x0400fe9 <+27>: | callq 0x400fce \ | ||||
0x0400fee <+32>: | add %eax,%eax | ||||
0x0400ff0 <+34>: | jmp 0x401007 \ | ||||
0x0400ff2 <+36>: | mov $0x0,%eax | 0 | |||
0x0400ff7 <+41>: | cmp %edi,%ecx | ||||
0x0400ff9 <+43>: | jge 0x401007 \ | x>=7 | |||
0x0400ffb <+45>: | lea 0x1(%rcx),%esi | ||||
0x0400ffe <+48>: | callq 0x400fce \ | ||||
0x0401003 <+53>: | lea 0x1(%rax,%rax,1),%eax | ||||
0x0401007 <+57>: | add $0x8,%rsp | ||||
0x040100b <+61>: | retq |
在加粗处,可以发现,只要让x=7,是可以避免递归的。且返回值%eax
也是0。所以我们得到了答案,7 0
。
炸弹5
反汇编phase_5
:
Dump of assembler code for function phase_5:
0x0000000000401062 <+0>: push %rbx
0x0000000000401063 <+1>: sub $0x20,%rsp
0x0000000000401067 <+5>: mov %rdi,%rbx
0x000000000040106a <+8>: mov %fs:0x28,%rax
0x0000000000401073 <+17>: mov %rax,0x18(%rsp)
0x0000000000401078 <+22>: xor %eax,%eax
0x000000000040107a <+24>: callq 0x40131b <string_length>
0x000000000040107f <+29>: cmp $0x6,%eax
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112>
0x0000000000401084 <+34>: callq 0x40143a <explode_bomb>
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112>
0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx
0x000000000040108f <+45>: mov %cl,(%rsp)
0x0000000000401092 <+48>: mov (%rsp),%rdx
0x0000000000401096 <+52>: and $0xf,%edx
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1)
0x00000000004010a4 <+66>: add $0x1,%rax
0x00000000004010a8 <+70>: cmp $0x6,%rax
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>
0x00000000004010ae <+76>: movb $0x0,0x16(%rsp)
0x00000000004010b3 <+81>: mov $0x40245e,%esi
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi
0x00000000004010bd <+91>: callq 0x401338 <strings_not_equal>
0x00000000004010c2 <+96>: test %eax,%eax
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119>
0x00000000004010c6 <+100>: callq 0x40143a <explode_bomb>
0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1)
0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119>
0x00000000004010d2 <+112>: mov $0x0,%eax
0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41>
0x00000000004010d9 <+119>: mov 0x18(%rsp),%rax
0x00000000004010de <+124>: xor %fs:0x28,%rax
0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140>
0x00000000004010e9 <+135>: callq 0x400b30 <__stack_chk_fail@plt>
0x00000000004010ee <+140>: add $0x20,%rsp
0x00000000004010f2 <+144>: pop %rbx
0x00000000004010f3 <+145>: retq
End of assembler dump.
9\10行知道,输入一个字符串,长度为6。13到21行是个循环,先跳过,看22到25行。25行调用strings_not_equal
函数,22\23\24行为调用做准备,%rdi
和%esi
分别是一号二号参数,查看一下二号参数的值:x/s 0x40245e
,得到flyers
。如果返回值0,这两个字串相等,就跳出,否则爆炸。
现在来看循环,19\20行知道,%rax
是控制循环的变量。在第四行将输入字串的指针给了%rbx
,13行把指向字串的内容给%ecx
,14\15\16行倒腾一阵,最终就是取了输入字母的二进制后四位。涉及一些mov指令的填充问题,细节参考csapp
。17行采用了一个基址寻址,将输入字母的二进制后四位作为偏移,加到基址0x4024b0
上,给%edx
。18行将新生成的字串拷到栈区,使用基址比例变址寻址。
所以理清一下这一段,就是输入字串每个字母的二进制后四位作为偏移量,从基址0x4024b0
中取字母,构成新的字串,与flyers
比较,相等就ok。所以我们看一下x/s 0x4024b0
的内容:maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
。找出flyers
的偏移,分别为9 15 14 5 6 7
,那么输入的字串只要后四位符合就好,一个答案是ionefg(0x49,0x4f,0x4E,0x45,0x46,0x47)
。
炸弹6
这个好像是个链表排序,有点麻烦就没细想,参考%%%大佬。
秘密关卡
如何发现?看bomb.c
,phase_6
后面并没有输出,但是解除炸弹是有输出的。说明秘密在phase_defused
函数里面。