对Windows Defender中虚拟机的逆向分析

微软的虚拟机跟qemu 的tcg类似,都是动态翻译.从上图可以看出,先翻译成IL中间码,然后再翻译成native代码。

OPCODE

DT il使用了0x40个

#define iCo_add        0x00
#define iCo_or         0x01
#define iCo_adc        0x02
#define iCo_sbb        0x03
#define iCo_and        0x04
#define iCo_sub        0x05
#define iCo_xor        0x06
#define iCo_mov        0x07
#define iCo_lea        0x08
#define iCo_xchg       0x09
#define iCo_inc        0x0A
#define iCo_dec        0x0B
#define iCo_mul        0x0c
#define iCo_imul       0x0D
#define iCo_div        0x0E
#define iCo_idiv       0x0F

#define ELF_SET_OP    0x10

#define iCo_setn     0x10
#define iCo_setna    0x11
#define iCo_setb     0x12
#define iCo_setnb    0x13
#define iCo_setz     0x14
#define iCo_setnz    0x15
#define iCo_setbe    0x16
#define iCo_setnbe   0x17
#define iCo_sets     0x18
#define iCO_setns    0x19
#define iCo_setp     0x1a
#define iCo_setnp    0x1B
#define iCo_setl     0x1c
#define iCo_setnl    0x1D
#define iCo_setle    0x1E
#define iCo_setnle   0x1F

#define SHIFT_OP     0x20

#define iCo_rol        0x20
#define iCo_ror        0x21
#define iCo_rcl        0x22
#define iCo_rcr        0x23
#define iCo_shl        0x24
#define iCo_shr        0x25
#define iCo_sal        0x26
#define iCo_sar        0x27

#define BIT_OP        0x28
/*8*(opc-0x28) +0xA3*/
#define iCo_bt         0x28
#define iCo_bts        0x29
#define iCo_btc        0x2A
#define iCo_btr        0x2B
#define iCo_bsf        0x2C
#define iCo_bsr        0x2D

#define iCo_jopz       0x30
#define iCo_fpu        0x3c
#define iCo_lop        0x3d
#define iCo_tx         0x3e
#define iCo_esc        0x3F

#define iCo_RF         0x40 #读EFLAGS
#define iCo_WF         0x80 #写EFLAGS

以x86为例,所有的add 都翻译成iCo_add指令。 Dt IL中只有一个跳转指令:iCo_jopz, 所有的FPU指令通过传递escID给iCo_fpu来调用。
而一些特殊指令比如CPUID之类使用native代码模拟,传递escID给iCo_esc来调用该函数。

不像x86/x64 opcode,不同的寻址方式 不同的操作大小需要用不同的指令.
DT IL统一使用tag_operand结构体来表示不同的类型

IL格式

IL 固定长度4bit

byte 0: opcode
byte 1: 操作数1 ID
byte 2: 操作数2 ID
byte 3: 目标操作数ID

比如 0x01020300 ,最低位为 0x00,即opcode iCo_add,其中操作数1为0x3,即寄存器ebx ,操作数2为 0x2,即edx,目标操作数为0x00,即ecx
最后该IL解释为iCo_add ebx,edx,ecx也就是ecx = ebx + edx

操作数

opcode的操作数都为索引,指向类型tag_operand;

union tag_operand {
    struct {
        /* 0x0000 */ uint32_t d0;
        /* 0x0004 */ uint32_t d1;
    }; /* size: 0x0008 */
    /* 0x0000 */ uint64_t d64;
}; /* size: 0x0008 */

内置操作数

为了方便模拟,把一些常见的数值或者寄存器写入固定值。

const tag_operand x86_init_operands[] = {
        {0x20000, 0x00}, /*0x00 eax*/
        {0x20000, 0x04}, /*0x01 ecx*/
        {0x20000, 0x08}, /*0x02 edx*/
        {0x20000, 0x0C}, /*0x03 ebx*/
        {0x20000, 0x10}, /*0x04 esp*/
        {0x20000, 0x14}, /*0x05 ebp*/
        {0x20000, 0x18}, /*0x06 esi*/
        {0x20000, 0x1C}, /*0x07 edi*/

        {0x10000, 0x00}, /*0x08 ax*/
        {0x10000, 0x04}, /*0x09 cx*/
        {0x10000, 0x08}, /*0x0A dx*/
        {0x10000, 0x0C}, /*0x0B bx*/
        {0x10000, 0x10}, /*0x0C sp*/
        {0x10000, 0x14}, /*0x0D bp*/
        {0x10000, 0x18}, /*0x0E si*/
        {0x10000, 0x1C}, /*0x0F di*/

        {0x00000, 0x00}, /*0x10 al*/
        {0x00000, 0x04}, /*0x11 cl*/
        {0x00000, 0x08}, /*0x12 dl*/
        {0x00000, 0x0C}, /*0x13 bl*/
        {0x00000, 0x01}, /*0x14 ah*/
        {0x00000, 0x05}, /*0x15 ch*/
        {0x00000, 0x09}, /*0x16 dh*/
        {0x00000, 0x0D}, /*0x17 bh*/

        {0x10000, 0x20}, /*0x18 es*/
        {0x10000, 0x22}, /*0x19 cs*/
        {0x10000, 0x24}, /*0x1A ss*/
        {0x10000, 0x26}, /*0x1B ds*/
        {0x10000, 0x28}, /*0x1C fs*/
        {0x10000, 0x2A}, /*0x1D gs*/


        {0x30005, 0x64}, //0x1E
        {0x30005, 0x6C}, //0x1F

        {0x20000, 0x44}, /*0x20 es_base*/
        {0x20000, 0x48}, /*0x21 cs_base*/
        {0x20000, 0x4C}, /*0x22 ss_base*/
        {0x20000, 0x50}, /*0x23 ds_base*/
        {0x20000, 0x54}, /*0x24 fs_base*/
        {0x20000, 0x58}, /*0x25 gs_base*/

        {0x20001, 1},
        {0x20001, 0xFFFFFFFF},

        {0x00000, 0x30}, /*0x28 FL*/
        {0x10000, 0x30}, /*0x29 FX*/
        {0x20000, 0x30}, /*0x2A EFX*/
        {0x00000, 0x31}, /*0x2B FH*/

        {0x00000, 0x34}, /*0x2c t8l*/
        {0x10000, 0x34}, /*0x2d t16l*/
        {0x20000, 0x34}, /*0x2e T32l*/
        {0x30000, 0x34}, /*0x2F T64*/
        {0x10000, 0x36}, /*0x30 T16h*/
        {0x20000, 0x38}, /*0x31 T32h*/

        {0x00000, 0x3C}, /*0x32 S8l*/
        {0x10000, 0x3C}, /*0x33 S16L*/
        {0x20000, 0x3C}, /*0x34 S32L*/
        {0x30000, 0x3C}, /*0x35 S64*/
        {0x10000, 0x3E}, /*0x36 s16H*/
        {0x20000, 0x40}, /*0x37 S32H*/

        {0x10000, 0x2C}, /*0x38 ip*/
        {0x20000, 0x2C}, /*0x39 eip*/
        {0x0000F, 0x00}, /*0x3A null*/
        {0x20001, 0x00}, /*0x3B 0*/
        {0x20000, 0x5C}, /*0x3C map_end*/
};

逆向XX引擎:通用dump

通用dump就是把该程序在虚拟机内执行完后的内存dump下来,然后扫描引擎进行扫描。对于杀软来说,dump下来的数据不需要是否能够运行。

实际上上图中每个区段的数据都是在样本在虚拟机执行完后的进程内存。

遍历在该进程内创建的所有内存,进程创建的内存类型有

  • VMA_USER 用户内存
  • VMA_STACK 栈
  • VMA_HEAP 堆
  • VMA_PENV 存放peb 环境数据
  • VMA_BUILTIN 存放api模拟代码和通过tdl配置构造的pe文件

我们只需要 VMA_USER类型的内存和可执行的VMA_HEAP类型的内存

下列代码为遍历所有创建的内存并将相关信息存如结构体dump_section.

struct dump_section {
    _QWORD rva;
    int vsize;
    int psize;
    int flag;
    list_head list;
};
 
list_for_each(list, &process->alloced_vma_head) {
    CMemInfo* vma = container_of(list, CMemInfo, list);
 
    region = vma->m_region;
    if (region.mem_start == process->m_main_module->BaseOfDll) {
        //主模块的mz头抹掉,因为通用脱壳后不使用这个mz头
        process->write_mem(process->m_main_module->BaseOfDll, ""21);
    }
 
    if (region.alloc_base != cur_mod->BaseOfDll) {
        auto pageAttr = region.PageAttr;
        if (pageAttr.mem_useage != VMA_USER &&
            //如果类型是堆,则必须是可读可执行的
            (pageAttr.mem_useage != VMA_HEAP || pageAttr.host_map_flag != 6))
            continue;
    }
 
    int vm_size = region.mem_end - region.mem_start;
    int read_size = 0x10000;
    int read_off = 0;
    int end = region.mem_end;
    while (true) {
        read_size = min(vm_size, 0x10000);
        //从尾部开始读
        read_off = end - read_size;
        int readed_size = process->read_mem(read_off, buf, read_size);
        if (readed_size < 0)
            //读取失败,可能内存以及被释放了
            break;
 
        int non_zero_off = readed_size - 1;
        if (non_zero_off < 0)
            continue;
        //尾部的0可能时为了对齐而填充的,找到非0处,从该位置开始到下一个内存区域前被省略
        while (!buf[non_zero_off]) {
            if (--non_zero_off <= 0)
                break;
        }
        if (non_zero_off > 0) {
            dump_section* ts = new dump_section;
            ts->rva = region.mem_start;
            ts->vsize = region.mem_end - region.mem_start;
            ts->psize = read_off + non_zero_off - region.mem_start;
            ts->flag = 0x40000000;
            //设置区段属性
            if (region.PageAttr.proctect & VMA_PAGE_WRITE)
                ts->flag = 0xC0000040;
            if (region.PageAttr.proctect & VMA_PAGE_READ)
                ts->flag |= 0x20000020;
            ++sec_cnt;
            list_add_tail(&ts->list, &head);
 
            break;
        }
 
        end = read_off;
        vm_size -= readed_size;
    }
}

构造PE文件

DOS头固定为下列值

unsigned char genpack_mz_head[224] = {
    0x4D0x5A0x900x000x030x000x000x000x040x000x000x000xFF0xFF0x000x00,
    0xB80x000x000x000x000x000x000x000x400x000x000x000x000x000x000x00,
    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,
    0x000x000x000x000x000x000x000x000x000x000x000x000xE00x000x000x00,
    0x0E0x1F0xBA0x0E0x000xB40x090xCD0x210xB80x010x4C0xCD0x210x540x68,
    0x690x730x200x700x720x6F0x670x720x610x6D0x200x630x610x6E0x6E0x6F,
    0x740x200x620x650x200x720x750x6E0x200x690x6E0x200x440x4F0x530x20,
    0x6D0x6F0x640x650x2E0x0D0x0D0x0A0x240x000x000x000x000x000x000x00,
    0xE60xFA0x3E0xFC0xA20x9B0x500xAF0xA20x9B0x500xAF0xA20x9B0x500xAF,
    0x610x940x5F0xAF0xA60x9B0x500xAF0x610x940x0D0xAF0xB70x9B0x500xAF,
    0xA20x9B0x510xAF0x190x9B0x500xAF0x610x940x0C0xAF0xA30x9B0x500xAF,
    0x610x940x0E0xAF0xA30x9B0x500xAF0x610x940x0F0xAF0xB30x9B0x500xAF,
    0x610x940x300xAF0xE70x9B0x500xAF0x610x940x0A0xAF0xA30x9B0x500xAF,
    0x520x690x630x680xA20x9B0x500xAF0x000x000x000x000x000x000x000x00
};

填充pe头

peheader pehead = {};
//设置pe结构
pehead.signature = 0x4550;
pehead.nobjs = sec_cnt;
pehead.machine = 332;
pehead.objalign = 0x1000;
pehead.filealign = 0x200;
pehead.subsys = 2;
pehead.stackres = 0x40000;
pehead.stackcom = 0x1000;
pehead.heapres = 0x100000;
pehead.heapcom = 0x1000;
pehead.nrvas = 0x10;
 
int sec_size = 0x28 * sec_cnt;
uint32_t min_off = -1;
x86_env* env = &process->cur_thread->_x86_env;
CArc_pe* arc_pe = process->m_main_module->m_cpe;
pehead.flags = 0x10e;
pehead.hdrsize = 0xe0;
if (arc_pe->peHead.flags & 0x2000)
    pehead.flags = 8462;
pehead.magic = 267;
pehead.allhdrsize = (sec_size + 0x3D7) & 0xFFFFFE00;
pehead.imagesize = (sec_size + 0x11D7) & 0xFFFFF000;
int file_addr = (sec_size + 0x3D7) & 0xFFFFFE00;
 
list_head* list;
//计算最小偏移、数据段大小和代码段大小
list_for_each(list, head) {
    dump_section* ds = container_of(list, dump_section, list);
    ds->vsize = (ds->vsize + 4095) & 0xFFFFF000;
    pehead.imagesize += ds->vsize;
    if (ds->rva < min_off) {
        min_off = ds->rva;
    }
    if (ds->flag & 0x40)
        pehead.dsize += ds->vsize;
    if (ds->flag & 0x20)
        pehead.tsize += ds->vsize;
}
pehead.imagebase32 = min_off - ((sec_size + 0x11D7) & 0xFFFFF000);
pehead.text_start = min_off;
pehead.data_start = min_off;
 
//多少个目录结构,最大只能由16个
int nrvars = min(arc_pe->peHead.nrvas, 0x10);
petab mem_pe_tab[0x10] = {};
//读取数据目录表
_QWORD va = process->m_main_module->BaseOfDll + arc_pe->dosHeader.lfanew + 0x78;
if (process->read_mem(va, mem_pe_tab, 0x80) != 0x80)
    return;
petab* genpack_petab = &pehead.expdir;
for (int i = 0; i < 0x10; i++) {
    if (i == 1)//导入表不用管
        continue;
    auto t = &mem_pe_tab[i];
    if (!t->rva)
        continue;
    genpack_petab[i].size = t->size;
    //修复rva地址
    genpack_petab[i].rva = (process->m_main_module->BaseOfDll + t->rva) - pehead.imagebase32;
}

填充pesection数据

pesection* sec_buf = (pesection*)malloc(sec_size);
if (!sec_buf)
    return;
memset(sec_buf, 0, sec_size);
 
auto sec = sec_buf;
int sec_index = 0;
//插入内存中的各个区段到pe文件
list_for_each(list, head) {
    auto ds = container_of(list, dump_section, list);
    snprintf(sec->s_name, 8"libvxf%d", sec_index);
    sec->s_name[7] = 0;
    sec->s_vsize = ds->vsize;
    sec->s_vaddr = ds->rva - pehead.imagebase32;
    sec->s_psize = (ds->psize + 0x1FF) & 0xFFFFFE00;
    sec->s_scnptr = file_addr;
    sec->s_flags = ds->flag;
    if (EIP >= ds->rva && EIP < ds->rva + ds->vsize)
        pehead.entry = EIP - pehead.imagebase32;
 
    file_addr = sec->s_psize + file_addr;
    ++sec_index;
    ++sec;
}

填充区段数据

list_for_each(list, head) {
    auto ts = container_of(list, dump_section, list);
    cur_off = ts->rva;
 
    psize = ts->psize;
    write_off = sec->s_scnptr;
    if (!psize) {
        ++sec;
        continue;
    }
 
    while (1) {
        read_size2 = min(psize, 0x10000);
        read_size = process->read_mem((_QWORD)cur_off, (DWORD*)buf, read_size2);
        if (read_size < 0)
            read_size = 0;
        algin_size = (read_size2 + 0x1FF) & 0xFFFFFE00;
        if (read_size < algin_size)
            memset((void*)(read_size + buf)0, algin_size - read_size);
        write_size = stream->write(write_off, (DWORD*)buf, algin_size);
 
        if (write_size < 0) {
            return;
        }
        cur_off = read_size2 + cur_off;
        write_off += read_size2;
 
        psize -= read_size2;
        if (!psize)
            break;
    }
 
    ++sec;
}

将usercall转换成 __cdecl函数调用

usercall不是标准的调用约定,但是用IDA反编译时经常能看到,应该是IDA特有的

int __usercall sub_10017FF0@<eax>(int a1@<eax>, int a2@<ecx>, _BYTE *a3, _BYTE *a4)

这样的函数声明,这个是IDA自己定义的,这种使用寄存器传参可能是编译时被编译器优化的结果。

如上例子则表示参数a1使用eax传递,参数a2使用ecx传递的。a2和a4是压栈

要hook这类函数的时候,我们可以写一个stub函数将usercall转换成调用__cdecl修饰的函数.

//
int __cdecl sub_10017FF0 (int a1 , int a2, _BYTE *a3, _BYTE *a4)
{
//......具体实现代码
}
//中转stub
static void __declspec(naked) stub_sub_10017FF0()
{
        __asm
        {
                //按照顺序压入参数
                push [esp + 08h] // a4
                push [esp + 08h] // a3
                push ecx // a2
                push eax // a1
                // 调用实际实现hook代码的函数
                call sub_10017FF0
                //寄存器传参的参数也要pop
                add esp, 4 //  
                pop ecx // a2
                //堆栈平衡,这里有俩个参数是压栈的
                add esp, 4 // a3
                add esp, 4 // a4
                retn
        }
}                                                                             

https://github.com/michael-fadely/usercall-hook

mbr结构和代码分析

很久以前分析的时候写的,最近开始学习分析bootkit类的病毒,记录下

bootcode代码

以WIN7为例(bootcode代码,win7到win10都一样),
MBR大小为1个扇区,大小为512(0x200),

第一块选区0x000~0x162为MBR的可执行代码
第二块选区0x163~0x1b2为错误信息显示,查找分区表失败时会在系统显示这些字符
第三块选区0x1be~0x1fd为分区表。

app.any.run网站样本爬虫

以前对 malwr.com 写过样本下载的爬虫,但是现在网站一直实在维护中。最近发现一个网站 app.any.run 这个网站也提供样本下载。

网站有反爬虫,尝试使用scrapy+splash时返回403.使用selenium时发现能返回正确的结构.

想来看看网页长啥样子。

逆向XX引擎:库文件解密

火绒所有的库文件都使用同一种方法解密的,具体解密算法都是在libxsse_30中实现的.
数据库头部数据都是通用的,结构如下:

struct   
database_header
{
    _DWORD DRAV;
    _DWORD SFHY;
    _WORD lowVersion;
    _WORD hiVersion;
    _QWORD timeStamp;
    _DWORD db_attr;
    _DWORD recordCount;
    _DWORD propSize;
    _DWORD origin_size; //未加密前的数据大小
    _DWORD compress_size;//zlib压缩数据的大小
    _DWORD key2[32];
    char dbName[8];
    _DWORD end[4];//0结尾
};

pset.db的如图
YzpL5R.png

逆向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对应的模块是在内存中创建的

debug IDApython script

使用pycharm调试

在网上搜索调试的方法也很久了,也没有找到方法。
无意中在auto_re插件中发现如下这段代码。


RDEBUG = False
# adjust this value to be a full path to a debug egg
RDEBUG_EGG = r'c:\Program Files\JetBrains\PyCharm 2017.1.4\debug-eggs\pycharm-debug.egg'
RDEBUG_HOST = 'localhost'
RDEBUG_PORT = 12321
...

def run():
      .....
      if RDEBUG and RDEBUG_EGG:
            if not os.path.isfile(RDEBUG_EGG):
                idaapi.msg('AutoRE: Remote debug is enabled, but I cannot find the debug egg: %s' % RDEBUG_EGG)
            else:
                import sys

                if RDEBUG_EGG not in sys.path:
                    sys.path.append(RDEBUG_EGG)

                import pydevd
                pydevd.settrace(RDEBUG_HOST, port=RDEBUG_PORT, stdoutToServer=True, stderrToServer=True)

这不和我之前调试kodi插件时一个套路嘛。

  • 安装pydevd模块
  • 用python charm打开run->Edit Configuration新建一个python remote debug调试配置,需要注意ip和端口
  • ida的python解释器默认是没有pydevd的,我们手动加载pycharm-debug.egg.

写一个hello测试下,RDEBUG_HOSTRDEBUG_PORT要和调试配置的一致。
加载脚本,命中断点时会在pydevd.settrace停下,ida会被挂起,然后
就可以像正常python脚本一样调试。


def debug():
    import sys
    RDEBUG_EGG="c:\Program Files\JetBrains\PyCharm 2017.1.4\debug-eggs\pycharm-debug.egg" #看你的pycharm安装路径
    sys.path.append(RDEBUG_EGG)
    pydevd.settrace(RDEBUG_HOST, port=RDEBUG_PORT, stdoutToServer=True, stderrToServer=True)


class MyPlugin_t(idaapi.plugin_t):
    flags = idaapi.PLUGIN_HIDE
    comment = "Test"
    help = ""
    wanted_name = "Test"
    wanted_hotkey = ""

    def init(self):
        return idaapi.PLUGIN_KEEP

    def run(self, arg):
        debug() 
        print "hello,hit break" #在这下个断点

    def term(self):
        pass

def PLUGIN_ENTRY():
    return MyPlugin_t()

使用wingIDE调试

在wingIDE目录找到wingdbstub.py并拷贝到IDA plugin目录中。

我们需要在脚本中指定wingIDE的目录,
找到下面这行,把None改成对应的路径。

WINGHOME = None

打开IDE,左下角有个小虫子图标,点击选择”Accept Debug Connections”.
最后我们需要在在调试代码加上两行代码,还是拿前面的为例。

新建一个project,选择hex-rays IDA,在这个工程打开需要调试的脚本,下好断点,ida加载要调试的脚本,后面和用pycharm是一样的。


def debug():
    import wingdbstub
    wingdbstub.Ensure()


class MyPlugin_t(idaapi.plugin_t):
    flags = idaapi.PLUGIN_HIDE
    comment = "Test"
    help = ""
    wanted_name = "Test"
    wanted_hotkey = ""

    def init(self):
        return idaapi.PLUGIN_KEEP

    def run(self, arg):
        debug() 
        print "hello,hit break" #在这下个断点

    def term(self):
        pass

def PLUGIN_ENTRY():
    return MyPlugin_t()

其他

理论上支持python remote debug的ide都应该支持调试IDApython脚本。
vs code使用pydevd调试,没试过。vs studio也可以,调试时需要附加IDA进程,很麻烦。

,