格式化字符串漏洞
格式化字符串漏洞
常用的可利用函数
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
转换指示符
字符 | 类型 | 使用 |
---|---|---|
d | 4-byte | Integer |
u | 4-byte | Unsigneg Integer |
x | 4-byte | Hex |
s | 4-byte prt | String |
c | 1-byte | Character |
长度
字符 | 类型 | 使用 |
---|---|---|
hh | 1-byte | Char |
h | 2-byte | Short int |
l | 4-byte | long int |
ll | 8-byte prt | long long int |
format str 漏洞 实例
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
void main(void)
{
char *format="%s";
char *arg1="hello world !\n";
printf(format,arg1);
}
原理
在x86结构下,格式化字符串的参数是通过栈传递的。
c代码
#include<stdio.h>
void main() {:
printf("%s %d %s", "Hello World!", 233, "\n");
getchar();
}
转换成汇编代码
Dump of assembler code for function main:
0x0804843b <+0>: lea ecx,[esp+0x4]
0x0804843f <+4>: and esp,0xfffffff0
0x08048442 <+7>: push DWORD PTR [ecx-0x4]
0x08048445 <+10>: push ebp
0x08048446 <+11>: mov ebp,esp
0x08048448 <+13>: push ecx
0x08048449 <+14>: sub esp,0x4
0x0804844c <+17>: push 0x8048500
0x08048451 <+22>: push 0xe9
0x08048456 <+27>: push 0x8048502
0x0804845b <+32>: push 0x804850f
0x08048460 <+37>: call 0x8048300 <printf@plt>
0x08048465 <+42>: add esp,0x10
0x08048468 <+45>: call 0x8048310 <getchar@plt>
0x0804846d <+50>: nop
0x0804846e <+51>: mov ecx,DWORD PTR [ebp-0x4]
0x08048471 <+54>: leave
0x08048472 <+55>: lea esp,[ecx-0x4]
0x08048475 <+58>: ret
End of assembler dump.
栈内的情况如图:
指令存储在0xffffcc54附近
printf格式化后的字符串 “hello world! 233 \n ” 字符串存储在0x804b008位置
而事先存储的字符串 hello world则存储在0x804b008位置 \n与233 也在其附近
main函数在调用call printf@plt前通过push将printf的四个参数统一入栈
0x0804844c <+17>: push 0x8048500 // "\n"
0x08048451 <+22>: push 0xe9 // 233
0x08048456 <+27>: push 0x8048502 // "hello world"
0x0804845b <+32>: push 0x804850f // "%s %d %s"
Call printf 图
%x %x %x %3$s
#include<stdio.h>
void main() {
printf("%s %d %s %x %x %x %2$d", "Hello World!", 233, "\n");
}
out:"Hello World! 233 f7f763dc ,ffa328a0 ,0,233"
其中%s 是字符串转换符 %d是数字转换符 %x是16进制转换符 而%2$d则是参数复用符号(栈中格式字符串后12个位置的字符串所在地址的内容以数字的格式输出)
1、如果参数复用符号所复用的变量为数字类型的就转换为16进制输出
2、如果参数复用符号所复用的变量为字符串类型的话 就输出其内存地址
3、设置输出变量的字节数例如%016x,就是以 16字节长度输出内存中第一个参数
即可通过%12%s的格式输出栈中格式字符串后12个位置的字符串所在地址的内容 达到获取指定内存地址内容的效果
格式化字符串漏洞利用
通过提供格式字符串,我们就能够控制格式化函数的行为。
使程序崩溃
格式化字符串漏洞通常要在程序崩溃时才会被发现,所有利用格式化字符串漏洞最简单的方式就是使进程崩溃。在linux中,存储无效的指针会引起进程收到SIGSEGV信号,从而使程序非正常终止并产生核心存储。我们找到核心存储中存储了程序崩溃时的许多重要信息,这些信息或是后续攻击利用的关键。
利用类似下面的格式化字符串即可触发漏洞:
printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s");
1、对于每一个%s,printf()都要从栈中获取一个数字,把该数字视为一个地址,然后打印地址指向的内存内容,直到出现一个null字符。
2、因为不可能获取的每一个数字都是地址,数字所对应的内存可能并不存在。
3、还有可能获得的数字确实是一个地址,但是该地址是被保护的。
查看栈内容
使程序崩溃只是进行验证了漏洞,我们还能利用格式化漏洞来获得内存的内容。
格式化字符串函数会根据格式字符串从栈上取值。由于32位系统上栈是由高地址向低地址增长,而printf()函数的参数是以逆序被压入栈的,所以参数在内存中出现的顺序在printf()调用时的顺序是一样的。
覆写栈内容
1、%n 转换指示符将 %n 当前已经成功写入流或缓冲区中的字符个数存储到地址由参数指定的整数中。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
void main(void)
{
int i;
char str[]="hello";
printf("%s %n\n",str,&i);
printf("%d\n",i);
}
out: hello\n 6
通常情况下,我们要需要覆写的值是一个 shellcode 的地址,而这个地址往往是一个很大的数字。这时我们就需要通过使用具体的宽度或精度的转换规范来控制写入的字符个数,即在格式字符串中加上一个十进制整数来表示输出的最小位数,如果实际位数大于定义的宽度,则按实际位数输出,反之则以空格或 0 补齐(0 补齐时在宽度前加点. 或 0)。如:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
void main(void)
{
int i;
printf("%10u%n\n",i,&i);
printf("%d\n",i);
printf("%.50u%n\n",i,&i);
printf("%d\n",i);
printf("%0100u%n\n",i,&i);
printf("%d\n",i);
}