适宜读者
:想了解溢出漏洞的小白;熟悉
Linux
环境下的
C
编程,熟悉
Gcc
与
Gdb;
熟悉
Linux
下的
AT&T
汇编;了解
Perl
1.
编程环境
2.
一个溢出漏洞实例
3.
溢出是如何发生的
4.
如何编写及提取
Shellcode
5.
怎样利用溢出漏洞
1.
编程环境
我的测试环境是
Red
Hat 9.0
。当然你也可以使用其他的
Linux
版本,不过在高版本的
Linux
环境下可能会有防溢出机制(比如说
Ubuntu
7.10
);尽管说在这种环境下也许有高人可以做到溢出利用,但这已不属于本文章的范畴。如果你是小白,如果你想一次成功,推荐你先在
RH9
中测试。
在利用漏洞时使用的是
perl
脚本;这里并不需要你有太深的
perl
功底,只要能理解这里使用的几条语句就行了。当然,在你的
Red
Hat
上一定要安装
gcc
、
gdb
和
perl
解释器,这些在安装光盘里都可以找到。
2.
一个溢出漏洞实例
为了在直观上对溢出有个清晰的理解,我们先给出一个非常简单的溢出漏洞实例。首先看一个有溢出漏洞的简单程序
vulnerable.c
:
#
include
<
stdio
.
h
> 程序中在使用
strcpy
函数时,因为没有检测字符串的长度而导致当
argv[1]
串长超过
16
字节时就会出现缓冲区溢出现象。使用
gcc
命令将
vulnerable.c
编译为可执行程序
vulnerable
,命令为
gcc
-o vulnerable vulnerable.c
,如下图所示:
#
include
<
string
.
h
>
int
main
(
int
argc
,
char
*
argv
[])
{
char
buff
[
16
];
strcpy
(
buff
,
argv
[
1
]);
printf
(
"/n%s/n"
,
buff
);
}
为了在本地利用该漏洞,我们需要精心构造 shellcode ,下面是用 perl 写的利用程序 exploit.pl :
#!/usr/bin/perl |
其中,上面提到的两个程序附在附件中(这里需要注意的是 exploit.pl 在创建后需要修改属性才能执行)。上面的 shellcode 是精心构造过的,在后面的构造 shellcode 章节将讲述。
运行 exploit.pl 我们将得到一个 shell ( sh-2.05b$ ):
如果你能得到控制台( sh-2.05b$ ),就代表你已经攻击成功了。下面我们将详细讲述溢出漏洞相关的内容。
3. 溢出是如何发生的
由于函数中局部变量的内存分配是发生在栈( Stack )中的,所以如果在某一个函数中定义了缓冲区变量,则这个缓冲区变量所占用的内存空间是在该函数被调用时所建立的栈里。由于对缓冲区的潜在操作(比如字串的复制)都是从内存低址到高址 的,而内存中所保存的函数调用返回地址往往就在该缓冲区的上方 (高地址)——这是由于栈的特性决定的,这就为覆盖函数的返回地址提供了条件。当我们有机会用大于目标缓冲区大小的内容来向缓冲区进行填充时,就可以改写函数保存在函数栈( Statck )中的返回地址,从而使程序的执行流程随着我们的意图而转移。这是冯诺依曼计算机体系结构的缺陷。
下面将调试一个简单溢出程序 simple_overflow.c 来了解 IA32 架构缓冲区溢出的机制:
#
include
<
stdio
.
h
> |
上面程序中, large 字符串的长度是精心构造的,下面会提到;接下来将编译为 simple_overflow ,并用 gdb 调试,结果如下:
从图中可以看出,执行结束后 eip 已经被改为 0x44434241 ,正好是 ABCD 的 ASCII 码值的倒置( A->0x41 , B->0x42 , C->0x43 , D->0x44 ),这是由于 IA32 ( Intel 架构)默认字节序是 Little_endian 方式 ;而 ABCD 则正是上述程序中 large 数组的最后四个字母。也就是说在主程序调用 strcpy 时,因为 large 长度远长于 small 而导致返回地址被覆盖了。接下来我们反汇编程序,看看 eip 为何会变成 0x44434241 :
可以看到,在 main 函数的最开始设置断点( b *0x08048328 )执行( r )后 esp 指向的内容就是 main 函数的返回地址( 0x42015574 );上面 0x42015571 位置的命令( call *0x8(%ebp) )就是调用执行 main 函数。我们的目标就是覆盖返回地址,这样在 main 函数返回时转入我们的流程。这里,我们记下返回地址存储的地址0xbfffde1c 。
我们关注位置 <main+28> 上的指令( call 0x8048268<strcpy> ):在 linux 环境下,当调用函数时将把参数从右向左压入栈( push 命令)中。在本例中,先压入 0x8049420 (位置 <main+19> ),从下图中可以看到该地址为 large 字符串;接着压入 0xffffffe8(%ebp) (位置 <main+27> ),该地址就是 small 字符串的首地址。
我们用 ebp 减去 small 数组的首地址是 24 ,再加上函数指向最开始要做的保存 ebp 操作(<main+0>)所需要的 4 个字节可以得到 28 。可以验证一下 0xbfffde00 ( small 数组的首地址)加上 28 正好是我们之前记下的 0xbfffde1c ;这也正是为何我们的 large 数组需要 32 ( 28+4 )字节的原因。
那么为何我们明明看到small数组是16个字节,但编译器却给我们开辟了24个字节?这是因为RH9版本中gcc编译对堆栈的局部变量的分配默认以16字节对齐。指令and 0xfffffff0,%esp(<main+6>)正体现了这一点。
如上所示,继续执行到 main 函数返回,最后的 ret 指令让 eip 等于 esp 指向的内容,而此时由于执行了有溢出漏洞的 strcpy 函数,此时 esp 指向的内容已经被我们修改过了( 0x44434241 )。这样, eip 就变成了可以控制的地址了,也就是说我们达到了可控流程的目的。
4.
如何编写及提取
ShellCode
Shellcode
是一段机器指令,用于在溢出之后改变系统正常流程,转而执行
ShellCode
从而完成渗透测试者的功能。
1996
年,
Aleph One
在
Underground
发表的论文给这段代码赋予
ShellCode
的名称,而这个称呼沿用至今。
这里我们将编写一个非常简单的
ShellCode
,它的功能是得到一个命令行,下面是其
C
代码及执行情况:
程序
shellcode
运行后相当于又执行了一个“
/bin/sh
”,接下来用
gdb
调试以查看其关键代码:
程序中关键在于调用了
execve
函数,通过调试可以清楚得看到在调用该函数前将三个参数按从右向左的顺序压入栈中:先在
<main+33>
压入
$0x0
(即
NULL
参数),接着在
<main+38>
压入
$ebp-8
即指向地址
$0x8048408
的指针(即
name
),最后在
<main+39>
压入地址
$0x8048408
(即
name[0]
,也就是
”/bin/sh”
字符串的地址)。接着我们反汇编
execev
函数(需要重新编译
shellcode
,使用静态编译,以避免链接干扰。命令为:
gcc –static –o
shellcode shellcode.c
):
从反汇编代码中可以看到,其中关键使用了一个软中断功能(
<execve+36>
)。我们在在这个指令位置设断,并查看软中断执行前各寄存器的值:
可以看到,
eax
保存
execve
的系统调用号
11
,
ebx
保存
name[0]
(即
”/bin/sh”
),
ecx
保存
name
这个指针,
edx
为
0
。这样执行软中断后就能执行
/bin/sh
得到
Shell
了;接下来,有了以上的分析就可以编写自己的
ShellCode
了,同时验证上面分析结果的正确性。
下面,我们使用在
C
程序中内嵌汇编的方式构造
shellcode
,具体代码如下。有一点要注意,
Linux x86
默认的字节序是
little-endian
,所以压栈的字符串要注意顺序。
通过编译执行,我们成功得到了
shell
命令行(
sh-2.05b$
)。在编写内嵌汇编时一定要注意格式问题;当然最重要的是在执行软中断前一定要使各寄存器的值符合我们之前分析的结果。
此时,编写工作依然没有完结,要记住我们最终的目的是得到
ShellCode
,也就是一串汇编指令;而对于
strcpy
等函数造成的缓冲区溢出,会认为
0
是一个字符串的终结,那么
ShellCode
如果包含
0
就会被截断,导致溢出失败。用
objdump
看看这个
ShellCode
是否包含
0
,命令为:
objdump –d shellcode_asm |
more
。注意在此命令下会反汇编所有包含机器指令的
section
,请自行找到
<main>
段:
从反汇编结果可以看到,有两条指令
”mov
$0x0,%edx”
和
”mov
$0xb,%eax”
包含
0
,需要变通一下。我们使用命令
”xor
%edx,%edx”
替换
”mov
$0x0,%edx”
,使用
”lea
0xb(%edx),%eax”
替换
”mov
$0xb,%eax”
,情况如下:
运行没有问题,再看看这个
ShellCode
有没有包含
0
:
可以看到,所有曾出现
0
的指令全消除了。也许你会说,地址
0x80482fd
上不就有四个
0
么;这里我们需要注意,我们需要提取的
ShellCode
从
0x8048304
到
0x804837a
,所以在此范围内没有
0
。
到此为止,
ShellCode
的编写工作已经完美完成了;剩下的就是抽取及测试工作了,下面给出了一个简单的测试程序:
测试成功。看到上面的
ShellCode
是不是很眼熟?没错,正是在第二章节中我们使用过的
ShellCode
。到此,
ShellCode
的编写及抽取工作已经完成,相信您看到这里一定也能写出属于自己的
ShellCode
了吧:)
5. 怎样利用溢出漏洞
前面我们介绍了溢出是如何产生的,并得到了一个简单的
ShellCode
。接下来我们将讲述一种在本地攻击存在溢出漏洞程序的方法:把
ShellCode
放在环境变量里,从而在攻击程序中精确定位
ShellCode
。下面是示意图:
在执行存在漏洞程序前,我们将
ShellCode
作为环境变量传递给程序,现在关键是如何定位
ShellCode
的地址。关于这个问题,我们先看一下堆栈最开始的使用情况,请看下图:
可以看到栈底是固定的,为
0xc0000000
,向低地址扩展,先是
4
个字节的
0x00
,然后是程序路径,接着是环境变量。使用
gdb
调试
simple_overflow
可以清楚得看到这一点:
从上图可以看到,假如我们精心构造的
ShellCode
能作为环境变量存储在程序路径前的一个环境变量中,那么我们将可以精确得定位
ShellCode
的地址。下面是在第二章节中展示的漏洞程序
vulnerable.c
,不过为了调试方便,我们在程序最后加了一条
getchar()
语句:
下面是我们在第二章节中展示的用
Perl
编写的攻击程序
exploit.pl
:
上面程序中,
shellcode
是我们在第四章节中精心构造过的
ShellCode
,
path
是本机上
vulnerable
可执行程序所在的绝对路径(请依据您本机的情况加以修改);
ret
则精确定位了
ShellCode
所在地址;
new_retword
保存了
ret
地址的长整型值,接着输出该值以便调试;接着设置
shellcode
为环境变量,最后使用
exec
调用漏洞程序,参数为连续
8
个
new_retword
值。
首先,在一个控制台运行攻击程序:
通过输出看到
ShellCode
地址为
0xbfffffc5
,接着我们在另外一个控制台调试:
我们使用
gdb
附上(
Attach
)
vulnerable
程序,查看
0xbfffffc5
地址上的数据;通过对比,我们可以确定就是我们精心构造过的
Shellcode
。
本地缓冲区溢出比较简单,看到这里相信各位都能明白了。
总结
溢出利用总是有特定的环境,
Windows
和
Linux
下有着显著的区别,而且攻击手段也是层出不穷。本文给大家展示了在
Linux
环境下使用环境变量方法攻击漏洞程序的简单实例。通过阅读本文,希望大家能有所收获。
题外话
本文是基于
Xfocus
的《网络渗透技术》一书所写,所以如果在看本文时能对照该书的第二、第三章节相信会更有收获,并且在书中不但阐述了如何在
Linux
x86
环境下利用缓冲区溢出,还有
Win32
环境、
AIX
PowerPC
平台及
Solaris
SRARC
平台下的缓冲区溢出利用技术。