Shellcode 字面上就是被注入到執行中的程式裡,像細胞內的生物病毒一樣接管控制權。

位置無關程式碼#

由於 shellcode 並非真正的可執行程式,我們沒有「宣告記憶體佈局」或「使用其他記憶體段」的餘裕。

我們的指令必須自足,且能在處理器任何狀態下接管控制——這通常稱為位置無關程式碼(position-independent code)

在 shellcode 中,"Hello, world!" 的位元組必須與組合指令的位元組混在一起(因為沒有可定義或可預測的記憶體段)。只要 EIP 不試圖把字串當指令解讀就沒問題。但要把字串當資料存取,就需要一個指向它的指標——而 shellcode 執行時可能在記憶體任何地方,字串的絕對位址必須相對 EIP 計算。

問題在於:EIP 無法從組合指令直接存取,所以需要某種技巧。

用堆疊指令解決定址問題#

x86 對堆疊有專屬指令:

指令說明
push <source>把來源運算元推入堆疊
pop <destination>從堆疊彈出一個值存入目的運算元
call <location>呼叫函式,跳到位址;把 call 之後那條指令的位址推入堆疊以便返回
ret從函式返回,從堆疊彈出返回位址並跳過去

堆疊型漏洞利用正是 callret 造就的:函式被呼叫時返回位址被推入堆疊、開啟堆疊框架;函式結束時 ret 彈出返回位址並跳回。覆寫堆疊上的返回位址,就能控制執行。

同樣的架構可反過來解決「定址內嵌字串」的問題:

call mark_below   ; 跳到字串之後的指令
db "Hello, world!", 0x0a, 0x0d
mark_below:
  pop ecx         ; 把返回位址(字串指標)彈進 ecx
  mov eax, 4      ; write syscall #
  mov ebx, 1      ; stdout
  mov edx, 15     ; 字串長度
  int 0x80        ; write(1, string, 15)
  mov eax, 1      ; exit syscall #
  mov ebx, 0
  int 0x80

用 GDB 調查:null 位元組問題#

把上面的 shellcode 放進環境變數注入 notesearch,卻崩潰了。用 core dump 搭配 GDB 調查(root 下 ulimit -c unlimited 允許產生 core 檔)。

檢視 shellcode 所在的記憶體,指令看起來不對:

一般字串由 null 位元組終止,但這裡 shell 好心地幫我們移除了這些 null 位元組——這卻徹底破壞了機器碼的意義。shellcode 常以字串形式(用 strcpy() 等)注入程序,這類函式一遇 null 位元組就停止,在記憶體中留下不完整、無法使用的 shellcode。

為了讓 shellcode 平安通過傳輸,必須重新設計成完全不含 null 位元組。

移除 null 位元組#

call 的距離問題#

第一個 null 位元組來自 call:它依第一個運算元向前跳 19(0x13)位元組,而像 19 這樣的小值需以前導零填補,產生 null 位元組。

一個解法利用二補數:小的負數會有前導位元開啟、產生 0xff 位元組。所以用負值往回跳,該指令的機器碼就不含 null 位元組。標準做法是:先向下 jmp 到結尾的 callcall 再往回跳到開頭的 pop

用小暫存器與 xor 清零#

其餘 null 位元組可靠理解暫存器寬度消除。EAX 等 32 位元暫存器的低 16 位可作 AX 存取,AX/BX/CX/DX 的個別位元組可作 8 位元的 AL/AH 等存取:

B8 04 00 00 00   mov eax,0x4   ; 含 null
66 B8 04 00      mov ax,0x4    ; 含 null
B0 04            mov al,0x4    ; 無 null!

AL 把正確的最低位位元組放進暫存器不會產生 null 位元組,但暫存器上面三個位元組可能仍有任何值。shellcode 要接管別的程序,若要 32 位元值正確,就得先把整個暫存器清零——而這同樣必須不用 null 位元組。

清零暫存器最乾淨的方式是 xor:任何值與自己 XOR 都得 0,且不修改處理器旗標sub eax,eax 也得 0 但會改旗標):

31 C0   xor eax,eax   ; 兩位元組清零 eax,不含 null

搭配 inc/dec 讓 shellcode 更小:

jmp short one
two:
  pop ecx
  xor eax, eax      ; 清零 eax 全 32 位元
  mov al, 4         ; write syscall #4 放進低位元組
  xor ebx, ebx
  inc ebx           ; ebx = 1,stdout
  xor edx, edx
  mov dl, 15        ; 字串長度
  int 0x80
  mov al, 1         ; exit syscall #1(上三位元組仍是零)
  dec ebx           ; ebx 減回 0
  int 0x80
one:
  call two          ; 往回呼叫避免 null 位元組
  db "Hello, world!", 0x0a, 0x0d

hexdump -C ... | grep 00 確認無 null 位元組後,這個 shellcode 就可用了——注入漏洞利用,notesearch 就會像新手一樣向世界問好。