Shellcode 字面上就是被注入到執行中的程式裡,像細胞內的生物病毒一樣接管控制權。
位置無關程式碼#
由於 shellcode 並非真正的可執行程式,我們沒有「宣告記憶體佈局」或「使用其他記憶體段」的餘裕。
我們的指令必須自足,且能在處理器任何狀態下接管控制——這通常稱為位置無關程式碼(position-independent code)。
在 shellcode 中,"Hello, world!" 的位元組必須與組合指令的位元組混在一起(因為沒有可定義或可預測的記憶體段)。只要 EIP 不試圖把字串當指令解讀就沒問題。但要把字串當資料存取,就需要一個指向它的指標——而 shellcode 執行時可能在記憶體任何地方,字串的絕對位址必須相對 EIP 計算。
問題在於:EIP 無法從組合指令直接存取,所以需要某種技巧。
用堆疊指令解決定址問題#
x86 對堆疊有專屬指令:
| 指令 | 說明 |
|---|---|
push <source> | 把來源運算元推入堆疊 |
pop <destination> | 從堆疊彈出一個值存入目的運算元 |
call <location> | 呼叫函式,跳到位址;把 call 之後那條指令的位址推入堆疊以便返回 |
ret | 從函式返回,從堆疊彈出返回位址並跳過去 |
堆疊型漏洞利用正是
call與ret造就的:函式被呼叫時返回位址被推入堆疊、開啟堆疊框架;函式結束時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到結尾的call,call再往回跳到開頭的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 就會像新手一樣向世界問好。