萬盛學電腦網

 萬盛學電腦網 >> Linux教程 >> 淺入淺出Liunx Shellcode

淺入淺出Liunx Shellcode

class="152898"> /*――――――――――――-
Author:旋木木【B.C.T】[[email protected]]
Date:2008/05/12
Website:www.bugshower.org
――――――――――――*/

一:什麼是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系統調用位於中斷0×80。當執行一個int 0×80指令後,發出一個軟中斷,強制內核停止當前工作來處理中斷。內核首先檢查傳入參數的正確性,然後將下面寄存器的值復制到內核的內存空間,接下來參照中斷描述符表(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 $0×80
movl $1,%eax
movl $0,%ebx
int $0×80
編譯該程序,並查看運行結果:
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 $0×80軟中斷來執行write系統調用。下一步執行了一個exit(0) 操作,將exit系統調用號1傳遞給EAX寄存器,將參數0傳遞給EBX寄存器,然後執行int $0×80來執行系統調用,實現程序的退出。
為了更清晰的驗證我們的系統調用確實被執行了,可以通過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 $0×4,%eax
8048079: bb 01 00 00 00 mov $0×1,%ebx
804807e: b9 98 90 04 08 mov $0×8049098,%ecx
8048083: ba 12 00 00 00 mov $0×12,%edx
8048088: cd 80 int $0×80
804808a: b8 01 00 00 00 mov $0×1,%eax
804808f: bb 00 00 00 00 mov $0×0,%ebx
8048094: cd 80 int $0×80
pr0cess@pr0cess:~$
噢!!!這個SB的程序在
8048074: b8 04 00 00 00 mov $0×4,%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寄存器的構成如下:

在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 $0×4,%al
8048076: b3 01 mov $0×1,%bl
8048078: b9 90 90 04 08 mov $0×8049090,%ecx
804807d: ba 12 00 00 00 mov $0×12,%edx
8048082: cd 80 int $0×80
8048084: b8 01 00 00 00 mov $0×1,%eax
8048089: bb 00 00 00 00 mov $0×0,%ebx
804808e: cd 80 int $0×80
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(
copyright © 萬盛學電腦網 all rights reserved