Shellcode 的位元組其實是特定架構的機器指令,所以它是用組合語言寫的。寫組合語言與寫 C 不同,但許多原則相似。
系統呼叫:程式與核心的介面#
作業系統在核心中管理輸入、輸出、程序控制、檔案存取、網路通訊等。編譯後的 C 程式,最終透過對核心的**系統呼叫(system call)**來完成這些工作。不同作業系統有不同的系統呼叫集合。
C 用標準函式庫求得便利與可攜性:一個用
printf()的 C 程式能為許多系統編譯,因為函式庫知道各架構適當的系統呼叫。但組合語言依定義就綁定特定架構,可攜性不可能——沒有標準函式庫,只能直接做核心系統呼叫。
用 strace 追蹤一個編譯後的 helloworld 程式,會看到它做的每個系統呼叫。開頭那些是為程式設定環境與記憶體,但真正輸出字串的是 write() 系統呼叫:
write(1, "Hello, world!\n", 13Hello, world!) = 13
exit_group(0) = ?
檔案描述元#
write() 的引數 fd 為 1,是特殊的標準檔案描述元。
檔案描述元幾乎用於 Unix 的一切:輸入、輸出、檔案存取、網路 socket……它像寄物櫃給你的號碼牌——開一個檔案描述元就像寄放外套、拿到一個之後可用來取回的號碼。
前三個檔案描述元是自動使用的(定義於 /usr/include/unistd.h):
| 描述元 | 常數 | 用途 |
|---|---|---|
| 0 | STDIN_FILENO | 標準輸入 |
| 1 | STDOUT_FILENO | 標準輸出 |
| 2 | STDERR_FILENO | 標準錯誤 |
用組合語言做 Linux 系統呼叫#
每個 Linux 系統呼叫都有編號(列於 /usr/include/asm-i386/unistd.h),在組合語言中以編號引用,例如 __NR_exit 是 1、__NR_write 是 4、__NR_execve 是 11。
把 helloworld.c 用組合語言重寫,只需 mov 與 int 兩種指令:
x86 指令的運算元可以是數值、記憶體位址或處理器暫存器。
mov在兩個運算元間複製值(Intel 語法:先目的、後來源)。int送中斷訊號給核心;在 Linux 中,int 0x80告訴核心做系統呼叫:
- EAX 指定要做哪個系統呼叫。
- EBX、ECX、EDX 分別存放第一、二、三個引數。
section .data ; 資料段
msg db "Hello, world!", 0x0a ; 字串與換行
section .text ; 文字段
global _start ; ELF 連結的預設進入點
_start:
; write(1, msg, 14)
mov eax, 4 ; write 是 syscall #4
mov ebx, 1 ; stdout 是 1
mov ecx, msg ; 字串位址
mov edx, 14 ; 字串長度 14
int 0x80 ; 呼叫核心
; exit(0)
mov eax, 1 ; exit 是 syscall #1
mov ebx, 0 ; 成功離開
int 0x80用
nasm -f elf把它組譯成目的檔,再用連結器ld產生可執行的a.out。global _start告訴連結器指令從哪開始。這個小程式能動,但它還不是 shellcode——因為它不自足、必須經過連結。