guest@blog.cmj.tw: ~/posts $

Shell Code 101


\x66\xbf\x04\x00\xb0\x3c\x0f\x05

Shell Code 開發流程:

  1. 準備好要產生 shell code 的範例 C 程式碼
  2. Compile binary,其中 CFLAGS 需要帶有 -static
  3. 使用工具 (e.g. objdump) 反組譯 binary 得到 assembly code
  4. 從 ASM 中得到所需要的核心程式碼:ShellCode.s
  5. Compile ASM (e.g. as),並且測試程式是否符合需求
  6. 反組譯 binary 得到 machine code

當你的 shell code 完成之後,一定要測試是否可以執行。 可以用一個簡單的 C 程式,測試你的 shell code 是否滿足設計上的需求。

const char shellcode[] = "Your shell code";
int main(int argc, char *argv) {
    (*(void (*)()) shellcode)();
    return 0;
}

第一個 Shell Code

第一個 Shell Code 絕對不是 Hello World!。對我來說,第一個程式一定是可以 compile 並且正確執行的程式。所以想像中第一個程式應該完全沒有作用的空函數:

/* Compile by - gcc -Wall -O0 -g -static -o test test.c */
int main() {
    return 0;
}

接下來使用工具 (objdump -S test) 來得到反組譯後的 assembly code, 就可以看到一大串的程式碼,其中我們只關心如何正確的離開這個程式。 經由一連串的搜尋之後我們找到 _exit 是最後離開的邏輯。

在 x86-based 64-bit 的環境下,呼叫 system call 的 asm code 為 syscall, 他會根據當時的 EAX 的值來決定使用哪一個 system call。這個值也可以看 kernel 的 syscall table 來得知。

0000000000430970 <_exit>:
430970:	48 63 f7             	movslq %edi,%rsi
430973:	49 c7 c2 d0 ff ff ff 	mov    $0xffffffffffffffd0,%r10
43097a:	41 b9 e7 00 00 00    	mov    $0xe7,%r9d
430980:	41 b8 3c 00 00 00    	mov    $0x3c,%r8d
430986:	eb 19                	jmp    4309a1 <_exit+0x31>
430988:	0f 1f 84 00 00 00 00 	nopl   0x0(%rax,%rax,1)
43098f:	00
430990:	48 89 f7             	mov    %rsi,%rdi
430993:	44 89 c0             	mov    %r8d,%eax
430996:	0f 05                	syscall
# ... Skip ...

看ㄧ下產生出來的 ASM code,我們知道在 syscall 執行前需要填入 eax 跟 rdi 兩個 register 當中。移除掉不需要的程式碼後,我們只需要將預計使用的回傳值, 塞入 rdi 當中,跟 _exit 所使用的 syscall number 填入 eax當中即可 。精簡後 assembly code 可以改為:

mov		$0,  %di
mov		$60, %al
syscall

最後 compile 精減過後的 ASM:

as -o test.o test.s
ld -o test test.o

我們執行 test 這個程式發現他會馬上結束,看回傳值 (echo $?) 也符合預期。 最後可執行的 Shell Code 如下:

const char shellcode[] = \
    "\x66\xbf\x00\x00"	/* mov    $0x0,%di  */ \
    "\xb0\x3c"			/* mov    $0x3c,%al */ \
    "\x0f\x05";			/* syscall */

int main(void)
{
     (*(void (*)()) shellcode)();
    return 1;
}