Shellcode 的位元組其實是特定架構的機器指令,所以它是用組合語言寫的。寫組合語言與寫 C 不同,但許多原則相似。

系統呼叫:程式與核心的介面#

作業系統在核心中管理輸入、輸出、程序控制、檔案存取、網路通訊等。編譯後的 C 程式,最終透過對核心的**系統呼叫(system call)**來完成這些工作。不同作業系統有不同的系統呼叫集合。

C 用標準函式庫求得便利與可攜性:一個用 printf() 的 C 程式能為許多系統編譯,因為函式庫知道各架構適當的系統呼叫。但組合語言依定義就綁定特定架構,可攜性不可能——沒有標準函式庫,只能直接做核心系統呼叫。

strace 追蹤一個編譯後的 helloworld 程式,會看到它做的每個系統呼叫。開頭那些是為程式設定環境與記憶體,但真正輸出字串的是 write() 系統呼叫:

write(1, "Hello, world!\n", 13Hello, world!) = 13
exit_group(0)                                = ?

檔案描述元#

write() 的引數 fd1,是特殊的標準檔案描述元。

檔案描述元幾乎用於 Unix 的一切:輸入、輸出、檔案存取、網路 socket……它像寄物櫃給你的號碼牌——開一個檔案描述元就像寄放外套、拿到一個之後可用來取回的號碼。

前三個檔案描述元是自動使用的(定義於 /usr/include/unistd.h):

描述元常數用途
0STDIN_FILENO標準輸入
1STDOUT_FILENO標準輸出
2STDERR_FILENO標準錯誤

用組合語言做 Linux 系統呼叫#

每個 Linux 系統呼叫都有編號(列於 /usr/include/asm-i386/unistd.h),在組合語言中以編號引用,例如 __NR_exit 是 1、__NR_write 是 4、__NR_execve 是 11。

helloworld.c 用組合語言重寫,只需 movint 兩種指令:

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.outglobal _start 告訴連結器指令從哪開始。

這個小程式能動,但它還不是 shellcode——因為它不自足、必須經過連結。