逆向XX引擎系列(一) 构造系统dll文件

dump沙箱系统dll文件

使用IDA打开libxsse.dll,进程设置如下图,然后开始调试。

通过顺序找到如下函数。

libxsse_31这个函数内解密libxscore.bundle,有几个重要的dll是存放在这个文件内,其中libvxf.dll的导出函数libvxf_alloc
是用来初始化虚拟机环境的。并且模拟了一些系统API的实现。

包含如下四个文件

libvxf.dll 虚拟机相关
libnat.dll 用来跟nxeng.sys虚拟化执行驱动通讯
libdt.dll 动态翻译执行,使用的是qemu的tcg
libexscan.dll

我在逆向某个api模拟实现的时候发现该api对应的模块是在内存中创建的

PE结构填充

将pe文件从内存中dump下来,使用IDA查看,按照函数大小从大到小排序。发现这里所有的函数都只是一个stub.

随便找两个函数对比下,发现啥?给eax的赋值不一样,有个int 80中断调用,这里调用这个函数真正的实现函数。eax的值应该是个中断调用号,
如何调用dll的导出函数以及中断的实现以后再说。

接下来看它是怎样构造PE文件的。

首先来看dos头。构造的vfs文件dos头如下,

因为这部分没有多少需要修改,构造PE文件的时候使用的是固定值

使用hiew结构查看pe结构。

这里部分结构是直接在代码里硬编码的,因为不是经常被修改。除了tsize、dsize、data_start、text_start和imageSize等等需要计算外,其他都是写死的数值。和上面使用hiew查看的一样。

都是根据tdl的数据来计算的,因此我们需要先找到kernel32.dll的tdl数据(在某个函数内存dump出来就ok了)。导出的api的太多了(kernel32的959个导出函数),省略掉一些。如下可从内存中直接dump出来json文件。

{
    "base": "0x7c800000",
        "ordbase" : 1,
        "taddr_gap" : "0x3000",
        "tsize_adj" : "0x74400",
        "fixup" : [
    { "origin": "ph32", "offset" : 140, "data" : "0008D3DC" },
    { "origin": "ph32", "offset" : 136, "data" : "00A00800" },
    { "origin": "ph32", "offset" : 164, "data" : "805C0000" }
        ],
        "data" : [
            "8BFF558BEC568B3500E0FD7F57FF75188D4510FF751450585858585EC9E8110000005669727475616C50726F7465637445780059B872647363CD81"
        ],
        "sctab" : [
            { "name": "ActivateActCtx", "proto" : "lHl" },
            { "name": "AddAtomA", "proto" : "ls" },
            { "name": "AddAtomW", "proto" : "lS" },
            { "name": "AddConsoleAliasA", "proto" : " lll" },
                ......
            { "name": "UNIMPLEMENTED_SYSCALL", "proto" : " " }
        ]
}
  1. imageBase
pe.image.imagebase32 =  tdl->base;
  1. text_start (base of code)

默认是0x1000,kernel32的taddr_gap是0x3000,因此是0x4000,不过暂时不明白为啥要这样做,扩展text地址?。

     DWORD text_start = 0x1000;
     if (a1->taddr_gap)
     {
         text_start = a1->taddr_gap + 0x1000;
     }
  1. tsize (text size)
DWORD sctabCount = a1->sctabCount - a1->x64_only;
DWORD tSize = 0;
DWORD text_size = 0;
DWORD stubFunSize = sctabCount << 6;
if ((signed int)(unsigned __int16)((_WORD)stubFunSize) <= 0xA000)
    stubFunSize = stubFunSize & 0xFFFFEC00 | 0xEC00;
if (a1->tsize_adj)
    tSize = a1->tsize_adj + stubFunSize;
else
    tSize = stubFunSize + 0x40000;

为啥sctabCount<<6就可以计算出text的大小呢?前面用ida看代码的时候也看到了,所有的函数都是个stub,大小是0x40,不足的部分用0xcc和0x90补充了。

  1. Data_start (base of data)

data段就接着在text段后面。

pe.data_start = aligin_text_vsize + text_start;
  1. Data size

数据段的大小计算有点麻烦,tdl的data_size是上图data节点的那串二进制数据长度。0x110为导入表大小,0x28导出表结构大小

if (a1->sctabCount > 0)
{
    int i = 0;
    sctab* s = NULL;
    const char *proto;
    DWORD tmplen;
    const char* tmp;
    DWORD v75 = text_start + 2;//+2?
    do {
        s = &a1->ssctab[i];
        if (!(s->flag & 0x8)) //x64_only
        {
            proto = s->proto;
            //主要是看有没有`符号。
            if (proto)
            {
                tmp = strchr(proto, '`');
                if (tmp)
                    data_size += strlen(tmp + 1) + 1;
            }
            if (s->name)
            {
                tmplen = strlen(s->name);
                data_size += tmplen + 1;
                APIStrtotalLen += tmplen + 1;
                if (is_kernel32 && !exitThewadRva && !strcmp(s->name, "ExitThread"))
                    exitThewadRva = v75;
                if (s->flag < 0) // init_func
                    initfun_rva = v75;
            }
        }
        ++i;
        v75 += 0x40;
    } while (i < a1->sctabCount);
}

为啥要加上10*sctabCount? 4+4+2=10;

api字符串 rva地址表4字节

函数表rva4字节

导出ID 2字节

  1. ImageSize

    代码段rva地址+代码段按内存对齐后的大小+对齐后的数据段按内存的大小就是整个文件在内存中的大小。
    代码段rva就是前面所有的结构数据的大小,不需要另外计算了。

    DWORD aligin_data_vsize = alignup(data_size, 0x1000);
    pe.imagesize = pe.data_start + aligin_data_vsize;
  2. Entry 入口点
    如果没指定入口点,则默认是0xf07f.

    //有初始化标志
    //{ "name": "USER32_DllMain", "proto": "lHll", "init_func": true },
    if (initfun_rva)
    {
     pe.entry = initfun_rva;
    }
    else
    {
     pe.entry = a1->entry;
     if (!pe.entry)
         pe.entry = 0xF07F;
    }
  3. fileSize
    数据段和代码段的大小按文件对齐加起来就是文件大小。

    DWORD   aligin_text_psize = alignup(text_size, 0x200);// ((text_size + 0x1FF) & 0xFFFFFE00);
    DWORD fileSize = section[1].s_scnptr + section[1].s_psize;
    
  4. 区段数据

//初始化代码段数据
strcpy(sec[0].s_name, ".text");
*(_WORD*)&sec[0].s_name[6] = 0;

sec[0].s_flags = PEST_EXEC | PEST_READ | PEST_TEXT;
sec[0].s_vaddr = text_start;
sec[0].s_vsize = algin_tvsize;
sec[0].s_scnptr = 0x400;
sec[0].s_psize = algin_tpsize;

//初始化数据段数据
strcpy(sec[1].s_name, ".data");
*(_WORD*)&sec[1].s_name[6] = 0;

sec[1].s_vsize = data_vsize;
sec[1].s_vaddr = sec[0].s_vsize + sec[0].s_vaddr;
sec[1].s_psize = data_psize;
sec[1].s_scnptr = sec[0].s_psize + sec[0].s_scnptr;
sec[1].s_flags = PEST_READ | PEST_WRITE | PEST_DATA;

////初始化资源段数据
strcpy(sec[2].s_name, ".rsrc");
*(_WORD*)&sec[2].s_name[6] = 0;

sec[2].s_vaddr = data_vstart + sec[1].s_vsize;
sec[2].s_scnptr = data_pstart + sec[1].s_psize;
sec[2].s_flags = PEST_READ | PEST_DATA;

//初始化资源段数据
strcpy(sec[3].s_name, ".reloc");
sec[3].s_name[7] = 0;

sec[3].s_vaddr = sec[2].s_vaddr;
sec[3].s_scnptr = sec[2].s_scnptr;
sec[3].s_flags = PEST_READ | PEST_DISCARD | PEST_DATA;
if (is_kernel)
    sec[1].s_vsize = sec[1].s_vsize + 20000;
  1. expdir

    peexpdir exp = {};
    exp.dllname = algin_tvsize + text_start;
    exp.naddrs = x86_api_cnt;
    exp.nnames = x86_api_cnt;
    
    exp.adrtab = pehead.expdir.rva + sizeof(peexpdir);
    
    exp.namtab = 4 * x86_api_cnt + exp.adrtab;
    exp.datetime = 0x48025BE1;
    exp.ordbase = tdl->ordbase;
    exp.ordtab = exp.namtab + 4 * x86_api_cnt;

    头部已经都构造好了,直接拷贝

    //从mz头到区段数据都已经计算好
    memset(pe_buf, 0, file_size);
    memcpy(pe_buf, mz_head, 0xE0u);
    memcpy(pe_buf + 0xE0, &pehead, 0xF8u);
    memcpy(pe_buf + 0x1D8, sec, 0xA0u);
    
  2. 函数stub
    开始填充函数代码

    //开始填充pe相关结构
    char* fun_stub_buf = pe_buf + 0x400;
    sc_index = 0;
    if (tdl->sctabCount > 0) {
    char* ep_fun_buf = fun_stub_buf + 0x2;
    api_index = 0;
    do {
        auto sc_t = &tdl->sctabs[api_index];
        int tdl_flag = sc_t->tdl_flag;
        if (tdl_flag & 8) {//x64_only
            ++api_index;
            ++sc_index;
            continue;
        }
    
        if (!sc_t->proto.data()) {
            //没有函数声明
            fun_stub_buf += 0x40;
            ep_fun_buf += 0x40;
    
            ++api_index;
            ++sc_index;
            continue;
        }
    
        proto = *sc_t->proto.data() != '*' ? sc_t->proto.data() : sc_t->proto.data() + 1;
    
        if (!proto) {
            fun_stub_buf += 0x40;
            ep_fun_buf += 0x40;
    
            ++api_index;
            ++sc_index;
            continue;
        }
        //QUIT_CALL函数改成跳转到exitThread函数执行
        if (is_kernel && sc_t->name.data() &&
            exitThread_rva &&
            !strcmp(sc_t->name.data()"QUIT_CALL")) {
            *ep_fun_buf = 0x50;
            ep_fun_buf[1] = 0xE8u;
            *(_DWORD*)(ep_fun_buf + 2) = (_DWORD)((char*)pe_buf + exitThread_rva - (_DWORD)ep_fun_buf - text_start + 0x400 - 6);
        }
        else {
            param_cnt = get_param_cnt(sc_t->proto.data());
    
            //填充函数stub
            if (tdl_flag & 4) {//tiny
                *(_DWORD*)&tiny_stub[3] = (WORD)sc_index | (tdl->tdl_index << 16);
                *(_WORD*)&tiny_stub[10] = 4 * param_cnt;
                memcpy(fun_stub_buf, tiny_stub, 0x40);
                sc_t->chainStub_gap = 5;
            }
            else {
                *(_DWORD*)&stub[13] = (WORD)sc_index | (tdl->tdl_index << 16);//填充中断号
                *(_WORD*)&stub[37] = 4 * param_cnt;
                memcpy(fun_stub_buf, stub, 0x40);
                sc_t->chainStub_gap = 0x20;
            }
            //函数stub的入口数据
            if (!sc_t->ep32.empty() && sc_t->ep32.size() <= 10) {
                memcpy(ep_fun_buf, &sc_t->ep32[0], sc_t->ep32.size());
            }
        }
    
        fun_stub_buf += 0x40;
        ep_fun_buf += 0x40;
    
        ++api_index;
        ++sc_index;
    } while (sc_index < tdl->sctabCount);
    }

函数代码都是根据下面这个模板来改的

                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   nop  
                   mov         eax,0  ;中断号
                   call        stub_1 
               exit_int:
                   ret  
                   int         3  
                stub_1:
                   push        0FFFFFFFFh  
                   call        exit_int
                   add         esp,8  
                   int         80h  
                   ret         0 ;改成参数个数

中断号就是tdl(dll配置)索引和sc(api描述)索引计算出来的,在中断处理程序里就可以通过这个找到要模拟的api

*(_DWORD*)&stub[13] = (WORD)sc_index | (tdl->tdl_index << 16);//填充中断号
//存放的函数地址表
_DWORD* addtab_buf = (_DWORD*)((char*)expdir_buf + 0x28);
//存放导出函数名的rva地址
_DWORD* namerva_buf = (_DWORD*)((char*)addtab_buf + 4 * exp.naddrs);
//存放导出函数索引的buf
_WORD* ordbase_buf = (_WORD*)((char*)namerva_buf + 4 * x86_api_cnt);
//存放函数名
char* nname_buf = (char*)((char*)ordbase_buf + 2 * x86_api_cnt);
//存放引用dll函数
char* ref_dll_buf = (char*)(nname_buf + st_name_len);

int ord_base = 0;
uint32_t nnames = 0;

ntzw_fun_index = 0;
int api_name_len = 0;

if (tdl->sctabCount > 0) {
    i = 0;
    ordbase_buf = (_WORD*)ordbase_buf;
    fun_rva = text_start + 2;
    do {
        auto sc_t = &tdl->sctabs[i];
        int tdl_flag = sc_t->tdl_flag;

        if (tdl_flag & 8) {
            ++i;
            ++ntzw_fun_index;
        }

        if (sc_t->proto.data() && (sl = (char*)strchr(sc_t->proto.data()'`')) != 0) {
            //填充引用dll字符串
            addtab_buf[ord_base] = ref_dll_buf - data_pbuf + data_vstart;
            strcpy(ref_dll_buf, sl + 1);
            ref_dll_buf += strlen(sl + 1) + 1;
        }
        else {
            //填充函数地址
            addtab_buf[ord_base] = fun_rva;
        }
        if (sc_t->name.data()) {
            /*计算函数名的rva地址 */
            ++nnames;
            *namerva_buf = nname_buf - data_pbuf + data_vstart;
            ++namerva_buf;
            //填充函数名
            api_name_len = strlen(sc_t->name.data());
            memcpy((void*)nname_buf, sc_t->name.data(), api_name_len);
            nname_buf += api_name_len + 1;
            //填充导出序号
            *ordbase_buf = ord_base;

            ++ordbase_buf;
            if (bNtdll) {
                //设置NT*和ZW*的函数地址
                if (sc_t->name.data()[0] != 'N' || sc_t->name.data()[1] != 't') {
                    if (sc_t->name.data()[0] == 'Z' && sc_t->name.data()[1] == 'w') {
                        j = sc_index;

                        if (j < ntzw_fun_index) {
                            for (j; j < ntzw_fun_index; j++) {
                                auto sc_t2 = &tdl->sctabs[j];
                                if (!sc_t->name.data() || *sc_t->name.data() != 78 || sc_t->name.data()[1] != 116)
                                    break;
                                if (!strcmp(sc_t->name.data() + 2, sc_t2->name.data() + 2)) {
                                    addtab_buf[ord_base] = sc_t->fun_rva;
                                    break;
                                }
                            }
                        }
                    }
                }
                else if (!sc_index) {
                    //是nt函数,
                    sc_index = ntzw_fun_index;
                }
            }
        }

        fun_rva += 0x40;
        sc_t->fun_rva = addtab_buf[ord_base++];

        ++i;
        ++ntzw_fun_index;
    } while (ntzw_fun_index < tdl->sctabCount);
}
文章目录
  1. 1. dump沙箱系统dll文件
  2. 2. PE结构填充
,