格式化字符串漏洞
格式化漏洞的原理 printf函数在处理参数的时候,每遇到一个%开头的标记,就会根据这个%开头的字符所规定的规则执行,即使没有传入参数,也会认定栈上相应的位置为参数。 每一个格式化字符串的 % 之后可以跟一个十进制的常数再跟一个 $ 符号, 表示格式化指定位置的参数
开始入坑格式化字符串漏洞利用!
格式化字符串利用目的
- 读任意一块内存区域
- 写任意一块内存区域
访问任意位置内存
格式字符串位于栈上. 如果我们可以把目标地址编码进格式字符串,那样目标地址也会存在于栈上,在接下来的例子里,格式字符串将保存在栈上的缓冲区中。
最大的挑战就是想方设法找出 printf 函数栈指针(函数取参地址)到 user_input 数组的这一段距离是多少,这段距离决定了你需要在%s 之前输入多少个%x。
在内存中写一个数字
%n: 该符号前输入的字符数量会被存储到对应的参数中去。格式化字符串输出几个字符,%n就是几,比如printf("%d%n", 1234, &n);此时n就是4. 利用这个方法,攻击者可以做以下事情: 1. 重写程序标识控制访问权限 2. 重写栈或者函数等等的返回地址 然而,写入的值是由%n 之前的字符数量决定的。真的有办法能够写入任意数值么? 1. 用最古老的计数方式, 为了写 1000,就填充 1000 个字符吧。 2. 为了防止过长的格式字符串,我们可以使用一个宽度指定的格式指示器。(比如(%0 数字 x)就会左填充预期数量的 0 符号)
目前做这个格式化字符串题目。也可以在这里下载file和libc.so。 拿到之后先运行程序,是个简单的ftp server,开始需要输入用户名和密码。 用IDA Pro查看反汇编代码,从main函数开始。
1 | int __cdecl __noreturn main(int argc, const char **argv, const char **envp) |
其中ask_username
函数 1
2
3
4
5
6
7
8
9
10
11
12
13char *__cdecl ask_username(char *dest)
{
char src[40]; // [sp+14h] [bp-34h]@1
int i; // [sp+3Ch] [bp-Ch]@1
puts("Connected to ftp.hacker.server");
puts("220 Serv-U FTP Server v6.4 for WinSock ready...");
printf("Name (ftp.hacker.server:Rainism):");
__isoc99_scanf("%40s", src);
for ( i = 0; i <= 39 && src[i]; ++i )
++src[i];
return strcpy(dest, src);
}ask_password
函数为 1
2
3
4
5
6
7
8
9int __cdecl ask_password(char *s1)
{
if ( strcmp(s1, "sysbdmin") )
{
puts("who you are?");
exit(1);
}
return puts("welcome!");
}
1 | int get_file() |
1 | char *put_file() |
1 | int show_dir() |
拿到puts的GOT地址,0x0804a028
。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18➜ fmt_string_write_got objdump -R pwn3
pwn3: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ffc R_386_GLOB_DAT __gmon_start__
0804a060 R_386_COPY stdin
0804a080 R_386_COPY stdout
0804a00c R_386_JUMP_SLOT setbuf
0804a010 R_386_JUMP_SLOT strcmp
0804a014 R_386_JUMP_SLOT printf
0804a018 R_386_JUMP_SLOT bzero
0804a01c R_386_JUMP_SLOT fread
0804a020 R_386_JUMP_SLOT strcpy
0804a024 R_386_JUMP_SLOT malloc
0804a028 R_386_JUMP_SLOT puts1
2
30x804A028=134520872
\x28\xa0\x04\x08%7$s
%8$s\x28\xa0\x04\x08
知识点
格式化漏洞的使用技术
- %N$p:以16进制的格式输出位于printf第N个参数位置的值;
- %N$s:以printf第N个参数位置的值为地址,输出这个地址指向的字符串的内容;
- %N$n:以printf第N个参数位置的值为地址,将输出过的字符数量的值写入这个地址中,对于32位elf而言,%n是写入4个字节,%hn是写入2个字节,%hhn是写入一个字节;
- %Nc:输出N个字符,这个可以配合%N$n使用,达到任意地址任意值写入的目的。
格式化串参数:
转换格式符:d、i、o、u、x用于整数,e、f、g、a用于浮点数,c用于字符,特别留意下面两个:
1、可用%s从目标进程读取内存数据;
2、可用%n把输出字符串长度写入任意地址;
3、可用宽度修饰符修改输出的字符的数量;
4、可用%hn修饰符每次写入16位数值。
格式化字符串参数的姿势 32位
读
'%{}\(x'.format(index) // 读4个字节 '%{}\)p'.format(index) // 同上面 '\({}\)s'.format(index) 写
'%{}\(n'.format(index) // 解引用,写入四个字节 '%{}\)hn'.format(index) // 解引用,写入两个字节 '%{}\(hhn'.format(index) // 解引用,写入一个字节 '%{}\)lln'.format(index) // 解引用,写入八个字节 64位
读
'%{}\(x'.format(index, num) // 读4个字节 '%{}\)lx'.format(index, num) // 读8个字节 '%{}\(p'.format(index) // 读8个字节 '\){}$s'.format(index) 写
'%{}\(n'.format(index) // 解引用,写入四个字节 '%{}\)hn'.format(index) // 解引用,写入两个字节 '%{}\(hhn'.format(index) // 解引用,写入一个字节 '%{}\)lln'.format(index) // 解引用,写入八个字节 %1\(lx: RSI %2\)lx: RDX %3\(lx: RCX %4\)lx: R8 %5\(lx: R9 %6\)lx: 栈上的第一个QWORD
格式化字符串可以覆盖的地址
1、保存的返回地址(栈溢出,用信息泄露的方法来确定返回地址的位置);
2、全局偏移表(GOT),动态重定位对函数;
3、析构函数表(DTORS);
4、C函数库钩子,例如malloc_hook、realloc_hook和free_hook;
5、atexit结构;
6、所有其他的函数指针,例如C++ vtables、回调函数等;
7、Windows里默认未处理的异常处理程序,它几乎总是在同一地址。
0x00 输出利用0x100溢出
是目标地址的四个字节, 在 C 语言中, 告诉编译器将一个 16 进制数 0x10 放于当前位置(占 1 字节)。如果去掉前缀 就相当于两个 ascii 字符 1 和 0 了,这就不是我们所期望的结果了。
注意,使用gdb调试时,每次看到的栈地址可能是不变的,这并不代表系统没有打开ASLR,gdb调试时会自动关闭ASLR