欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[分享]某vmp壳原理分析笔记

[分享]某vmp壳原理分析笔记

某vmp壳原理分析笔记

分析的样本为某数字公司最新免费壳子。之前的壳子已经被很多大佬分析了,这篇笔记的主要目的是比较详细的分析下该vmp壳子的原理,数字壳子主要分为反调试,linker,虚拟机三部分。笔记结构如下:

  • 反调试

    • 时间反调试
    • rtld_db_dlactivity反调试
    • traceid反调试
    • 端口反调试
  • linker部分:用来加载第二个so

    • 装载
    • 创建soinfo
    • 链接
    • dump second so
  • 虚拟机部分:解释执行保护的代码

    • dump dex
    • onCreate分析
    • 虚拟机入口

准备部分

因为懒得hook系统函数,每次过反调试就要手工更改寄存器的值,所以用IDAPython来模拟手工调试,下面是一些辅助函数,其中addBrk根据模块名和偏移地址下断点,fn_f7, fn_f8, fn_f9分别模拟f7, f8, f9.

def getModuleBase(moduleName):

    base = GetFirstModule()

    while (base != None) and (GetModuleName(base).find(moduleName) == -1):

        base = GetNextModule(base)

    if base == None:

        print "failed to find module: " + moduleName 

        return None

    else:

        return base

def addBrk(moduleName, functin_offset):

    base = getModuleBase(moduleName)

    AddBpt(base + functin_offset)

def fn_f7():

    idaapi.step_into()

    GetDebuggerEvent(WFNE_SUSP | WFNE_SUSP, -1) 

def fn_f8():

    idaapi.step_over()

    GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, -1) 

def fn_f9():

    idaapi.continue_process()

    GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, -1) 

def delBrk(moduleName, function_offset):

    base = getModuleBase(moduleName)

    DelBpt(base + function_offset)

反调试

时间反调试

反调试的第一处就是时间反调试,在反调试的开始调用time获取时间,在结束再调用一次time,当差值大于3就会触发反调试。因为使用脚本过反调试,所以两次时间差肯定是小于3的,所以只是用脚本简单的输出时间就可以了。0x19FC是time在libjiagu.so的plt段地址。

    #add breakpoint at time

    addBrk("libjiagu.so", 0x19FC)

    fn_f9()

    delBrk("libjiagu.so", 0x19FC)

    print("first call time")

    lr = GetRegValue("LR")

    AddBpt(lr)

    fn_f9()

    r0 = GetRegValue("R0")

    DelBpt(lr)

    print("first get time: %x" % (r0))

rtld_db_dlactivity反调试

rtld_db_dlactivity函数:这个函数默认情况下为空函数,这里的值应该为0,而当有调试器时,这里会被改为断点指令,所以可以被用来做反调试。libjiagu.so的sub_1E9C就是用来查找rtld_db_dlactivity函数的地址的。sub_1E9C的代码就不贴了。过这个反调试我们就可以在sub_1E9C返回后,获取到rtld_db_dlactivity的地址,然后将其修改为nop指令0x46C0。

    #add breakpoint at sub_1E9C 

    addBrk("libjiagu.so", 0x1E9c)

    fn_f9()

    delBrk("libjiagu.so", 0x1E9c)

    print("call sub_1E9C")

    #add breakpoint at return from sub_1E9C

    lr = GetRegValue("LR")

    AddBpt(lr)

    fn_f9()

    DelBpt(lr)

    #get rtld_db_dlactivity address

    r0 = GetRegValue("R0")

    print("rtld_db_dlactivity address is: %x" % (r0 - 1))

    #set rtld_db_dlactivity nop

    PatchDword(r0 - 1, 0x46c0)

tracepid反调试

tracepid反调试:当我们使用Ptrace方式跟踪一个进程时,目标进程会记录自己被谁跟踪。过tracepid反调试可以在调用strtol后,将其返回值修改为0.

    #add breakpoint at strtol

    addBrk("libjiagu.so", 0x1A80)

    fn_f9()

    delBrk("libjiagu.so", 0x1A80)

    print("call strtol")

    lr = GetRegValue("LR")

    #add breakpoint at return from strtol

    AddBpt(lr)

    fn_f9()

    DelBpt(lr)

    r0 = GetRegValue("R0")

    print("get trace id: %x" % (r0))

    SetRegValue(0, "R0")

端口反调试

检测IDA的默认端口23946是否被占用,通过跟踪,可以发现sub_6DD0()函数判断23946端口是否被占用。原理就是查看/proc/net/tcp,过端口反调试,就可以通过更改sub_6DD0的返回值。

    #add break point at sub_6DD0

    addBrk("libjiagu.so", 0x6DD0)

    fn_f9()

    lr = GetRegValue("LR")

    print("call sub_6DD0")

    delBrk("libjiagu.so", 0x6DD0)

    AddBpt(lr)

    fn_f9()

    SetRegValue(0, "R0")

    DelBpt(lr)

时间反调试

在反调试的最后还会调用一次time,计算差值,因为使用脚本过得,所以时间差值肯定小于3,这里只是简单输出一下时间

    #add break point at time in  sub_6EF8

    addBrk("libjiagu.so", 0x19FC)

    fn_f9()

    delBrk("libjiagu.so", 0x19FC)

    print("second call time")

    lr = GetRegValue("LR")

    AddBpt(lr)

    fn_f9()

    DelBpt(lr)

    r0 = GetRegValue("R0")

    print("second get time: %x" % (r0))

    #SetRegValue(r0 + 2, "R0")

Linker部分

这部分的主要作用是手工实现加载链接第二个so,并且调用第二个的so的JNI_Onload方法。因为篇幅问题,只列出一些核心的步骤,具体的调试的方法是在case 31的BLX LR处下断点,跟踪sub_7BBC函数,就可以分析linker部分。既然是手工实现linker,那linker的主要功能就必不可少:装载和链接。下面就对应libjiagu.so中的函数来分析具体的装载与链接过程。分析的起点为sub_3DBC, sub_3DBC中调用了sub_4780,sub_3D60,sub_33F8,分别对应装载,创建soinfo,链接三个过程。首先定义两个后面会用到的数据结构,这两个数据结构的来源是通过动态调试分析出的,是后续很多函数的参数类型,和libjiagu.so的真实实现可能会有出入。

struct EncryptSoInfo {

void *encryptSo;             //指向压缩加密的so

int sizeOfEncryptSo;         //压缩加密so的大小

void *uncompressedSo;        //解压后的so位置

int sizeOfUncompressSo;      //解压后的so大小

int num;                     //表示某种类型,在后面会用到一次,具体含义不知道

char name[4];                //第二个so的名字

char data[124];

};

//类似于标准linker中的ELFReader

struct MElfReader {

void * soinfo 

Byte[52] header;             // offset 4

int num_program_header;      //段表的个数 offset 56

ProgreamHeaderTable *pht;    //段表 offset 60

int unkonw;                  // offset 64

void *load_base;               //segment 起始地址 offset 68

int size; //offset 72

void *load_bias;         //offset 76

};

装载

这一步骤libjiagu.so的实现和标准linker的实现十分相似。先说下标准linker的做法,创建ElfReader对象,通过 ElfReader 对象的 Load 方法将 SO 文件装载到内存。

//标准linker的装载

bool ElfReader::Load() {

  return ReadElfHeader() &&      //读取elf header

         VerifyElfHeader() &&    //验证elf header

         ReadProgramHeader() &&  //读取program header

         ReserveAddressSpace() &&  //分配空间

         LoadSegments() &&         //按照program header的指示装载segments 

         FindPhdr();               //找到装载后的phdr

}

下面看下libjiagu.so的实现,只是少了验证elf header的环节。

int __fastcall sub_4780(MELFReader *a1, EncryptSoInfo *a2)

{

  Byte *v2; // r5@1

  Byte *v3; // r4@1

  int result; // r0@3

  v2 = a2;

  v3 = a1;

  if ( a2 && sub_40B0(a1, a2)   //读取elf header

          && sub_41A8(v3, v2)   //读取program header

          && sub_4424((int)v3, (int)v2) && //分配空间

           sub_4448(v3, v2) )    //按照program header的指示装载segments 

    result = sub_46CC(v3);      //找到装载后的phdr

  else

    result = 0;

  return result;

}

load ELF Header

装载的第一步就是加载ELF Header,这一过程比较简单,直接看代码

int __fastcall sub_40B0(MELFReader *a1, EncryptSoInfo *a2)

{

  Byte *v2; // r4@1

  const void *base; // r1@1

  int v4; // r4@3

  Byte *v6; // r5@5

  _DWORD *v7; // r0@5

  _DWORD *v8; // r6@5

  v2 = a2;

  base = (const void *)*((_DWORD *)a2 + 2); //a2->uncompressedSo

  if ( base

    && *((_DWORD *)v2 + 3) > 0x34u   //a2->int sizeOfUncompressSo

    && (v6 = a1 + 4,

     memcpy(a1 + 4, base, 0x34u),  //拷贝加密的后的ELF Header

     v7 = sub_6534(*((_DWORD *)v2 + 40)), //新建解密对象v7

     (v8 = v7) != 0)

    && sub_58F0((int)v7, (int)v6, 52)     //初始化解密对象

    && (v4 = (*(int (__fastcall **)(_DWORD *))(*v8 + 12))(v8)) != 0 //解密加密后的so 

    )

  {

    (*(void (__fastcall **)(_DWORD *))(*v8 + 4))(v8); //析构解密对象

  }

  else

  {

    v4 = 0;

  }

  return v4;

}

load program header

这一过程也比较简单,利用上一步读取的program header,得到program header数量,然后分配空间,拷贝加密后到的program header table到分配的空间,然后解密。

int __fastcall sub_41A8(MELFReader *a1, EncryptSoInfo *a2)

{

  Byte *v2; // r5@1

  Byte *v3; // r4@1

  size_t v4; // r0@2

  size_t v5; // r6@3

  void *v6; // r0@3

  _DWORD *v7; // r0@4

  _DWORD *v8; // r5@4

  int v9; // r4@6

  v2 = a2;

  v3 = a1;

  if ( a2

    && (v4 = *((_WORD *)a1 + 24), //program header 数量

    *((_DWORD *)v3 + 14) = v4, ((v4 - 1) & 0xFFFF) <= 0x7FF)

    && (v5 = 32 * v4, *((_DWORD *)v3 + 16) = 32 * v4, v6 = calloc(v4, 0x20u), // 分配空间

     (*((_DWORD *)v3 + 15) = v6) != 0)

    && (memcpy(v6, (const void *)(*((_DWORD *)v2 + 2) + *((_DWORD *)v3 + 8)), v5), //拷贝段表

        v7 = sub_6534(*((_DWORD *)v2 + 41)),  

        (v8 = v7) != 0)

    && sub_58F0((int)v7, *((_DWORD *)v3 + 15), *((_DWORD *)v3 + 16))

    && (v9 = (*(int (__fastcall **)(_DWORD *))(*v8 + 12))(v8)) != 0 ) //解密段表

  {

    (*(void (__fastcall **)(_DWORD *))(*v8 + 4))(v8);

  }

  else

  {

    v9 = 0;

  }

  return v9;

}

计算加载所需的空间并分配空间

ELF文件中所有类型为LOAD的segment都需加载到内存,这一步骤主要是计算加载所需要的空间并分配空间。这里需要说明一下loadbias,因为so可以指定加载基址,但指定的基址可能不是页对齐,所以实际加载的基址可能会和指定基址有个差值,loadbias用来保存这个差值。但一般so都是可以加载到任意地址的,所以loadbias的值一般就是so实际加载的基址。

signed int __fastcall sub_43A8(MELFReader *a1)

{

  Byte *v1; // r4@1

  int size; // r0@1

  signed int result; // r0@2

  _BYTE *v4; // r5@3

  _BYTE *v5; // r0@3

  int v6; // r5@4

  void *addr; // [sp+Ch] [bp-14h]@1

  v1 = a1;

  //计算加载所需的空间并且将加载的基址保存在addr中

  size = sub_4264(*((_DWORD *)a1 + 15), *((_DWORD *)a1 + 14), (unsigned int *)&addr, 0);

  *((_DWORD *)v1 + 18) = size;

  //mmap分配空间

  if ( size && (v4 = addr, v5 = mmap(addr, size, 0, 34, -1, 0), v5 != (_BYTE *)-1) )

  {

    v6 = v5 - v4;   //计算 loadbias

    *((_DWORD *)v1 + 17) = v5;

    result = 1;

    *((_DWORD *)v1 + 19) = v6;

  }

  else

  {

    result = 0;

  }

  return result;

}

sub_4264的原理,因为篇幅原因就不贴代码了,就是循环段表,读取每个需要load的段,然后找到加载的最小虚拟地址,和最大的虚拟地址,页对齐后,取差值就是load size。

load segment

遍历 program header table,找到类型为 PT_LOAD 的 segment:

  1. 计算 segment 在内存空间中的起始地址 segstart 和结束地址 seg_end,seg_start 等于虚拟偏移加上基址load_bias,同时由于 mmap 的要求,都要对齐到页边界得到 seg_page_start 和 seg_page_end。
  2. 计算 segment 在文件中的页对齐后的起始地址 file_page_start 和长度 file_length。
  3. 使用mrpotect和memcpy将段映射到内存,指定映射地址为 seg_page_start,长度为 file_length,文件偏移为 file_page_start。

    signed int __fastcall sub_4448(MELFReader *a1, EncryptSoInfo *a2)
    
    {
    
    Byte *v2; // r6@1
    
    unsigned int v3; // r2@3
    
    int v4; // r4@4
    
    int v5; // r5@7
    
    int v6; // r3@7
    
    int v7; // r5@7
    
    int v8; // r12@8
    
    int v9; // r2@8
    
    int v10; // r10@8
    
    unsigned int v11; // r11@8
    
    unsigned int v12; // r12@8
    
    bool v13; // cf@8
    
    bool v14; // zf@8
    
    unsigned int v15; // r8@8
    
    void *v16; // r7@8
    
    int v17; // r12@8
    
    int v18; // r10@8
    
    void *v19; // r0@16
    
    signed int v20; // r2@19
    
    size_t n; // [sp+4h] [bp-34h]@10
    
    unsigned int v23; // [sp+8h] [bp-30h]@2
    
    Byte *v24; // [sp+Ch] [bp-2Ch]@1
    
    v24 = a2;
    
    v2 = a1;
    
    if ( a2 )
    
    {
    
     v23 = *((_DWORD *)a2 + 3);                  // 解压后的so的起始地址
    
     if ( *((_DWORD *)a2 + 3) )
    
     {
    
       v3 = *((_DWORD *)a1 + 14);                // segment 数量
    
       if ( !v3 )
    
         return 1;
    
       v4 = 0;
    
       while ( 1 )
    
       {
    
         while ( 1 )                             // 找到类型为LOAD的段
    
         {
    
           v5 = *((_DWORD *)v2 + 15);            // 段表起始地址
    
           v6 = *(_DWORD *)(v5 + 32 * v4);       // 迭代读取段表
    
           v7 = v5 + 32 * v4;
    
           if ( v6 == 1 )
    
             break;
    
           if ( v3 <= ++v4 )
    
             return 1;
    
         }
    
         v8 = *(_DWORD *)(v7 + 4);               // offset segment在文件的偏移
    
         v9 = *(_DWORD *)(v7 + 16);              // filesz segment在so中大小
    
         v10 = *((_DWORD *)v2 + 19) + *(_DWORD *)(v7 + 8);// segment 在虚拟内存的结束地址
    
         v11 = v8 & 0xFFFFF000;
    
         v12 = v8 + v9;
    
         v13 = v23 >= v12;
    
         v14 = v23 == v12;
    
         v15 = (*(_DWORD *)(v7 + 20) + 4095 + v10) & 0xFFFFF000;
    
         v16 = (void *)(v10 & 0xFFFFF000);
    
         v17 = v12 - v11;
    
         v18 = v10 + v9;
    
         if ( v14 || !v13 )
    
           break;
    
         n = v17;
    
         if ( mprotect(v16, v15 - (_DWORD)v16, 3) == -1 )
    
           break;
    
         if ( n )
    
           memcpy(v16, (const void *)(*((_DWORD *)v24 + 2) + v11), n);
    
         if ( *(_DWORD *)(v7 + 24) & 2 && v18 & 0xFFF )
    
           memset((void *)v18, 0, 4096 - (v18 & 0xFFF));
    
         v19 = (void *)((v18 + 4095) & 0xFFFFF000);
    
         if ( v15 > (unsigned int)v19 )
    
           memset(v19, 0, v15 - (_DWORD)v19);
    
         v20 = *(_DWORD *)(v7 + 24) & 1 ? 4 : 0;
    
         if ( mprotect(v16, v15 - (_DWORD)v16, *(_DWORD *)(v7 + 24) & 2 | (*(_DWORD *)(v7 + 24) << 29 >> 31) | v20) == -1 )
    
           break;
    
         v3 = *((_DWORD *)v2 + 14);
    
         if ( v3 <= ++v4 )
    
           return 1;
    
       }
    
     }
    
    }
    
    return 0;
    
    }
    
    

    至此第二个so已经完成了装载。

    创建soinfo

    完成装载后,就需要创建soinfo结构,并利用MELFReader设置一些soinfo参数。

char *__fastcall sub_3D60(EncryptSoInfo *a1)

{

  const char *v1; // r4@1

  char *result; // r0@2

  char *v3; // r5@2

  v1 = (const char *)(a1 + 20);

  if ( strlen((const char *)a1 + 20) > 0x7F )

  {

    result = 0;

  }

  else

  {

    result = (char *)operator new(0x128u); //创建soinfo

    v3 = result;

    if ( result )

    {

      memset(result, 0, 0x128u);

      strncpy(v3, v1, 0x7Fu);

      result = v3;

    }

  }

  return result;

}

sub_3DBC(EncryptSoInfo *a1)

{

  //节选

  soinfo = sub_3D60(*(Byte **)v1);

  v3 = (int)soinfo;

  if ( !soinfo )

    goto LABEL_16;

  v4 = nmemb_56;

  v5 = nmemb_68;

  v6 = nmemb_72;

  v7 = nmemb_76;

  *((_DWORD *)soinfo + 33) = nmemb_56;          // 设置 soinfo->phnum;

  *((_DWORD *)soinfo + 35) = v5;                // 设置 soinfo->loadstart

  *((_DWORD *)soinfo + 36) = v6;                // 设置 soinfo->size;  

  *((_DWORD *)soinfo + 62) = v7;                // 设置 soinfo->loadbias

  //

}

链接

自定义linker最重要的一步就是链接,链接主要步骤是:

  1. 定位 dynamic segment
  2. 解析 dynamic section
  3. 加载该so所依赖的so
  4. 重定位
    这一部分是由sub_33F8函数完成的,因为这个函数很长,所以节选主要步骤贴出来
signed int __fastcall sub_33F8(soinfo *a1)

{

    //遍历program header找到类型为Dynamic的program header,从而定位到Dynamic segment

    sub_45F4(*((_DWORD *)a1 + 32), *((_DWORD *)v2 + 33), *((_DWORD *)v2 + 62), (_DWORD *)v2 + 37, &v38);

    v3 = *((_DWORD *)v2 + 37); //dynamic segment

    //解析dynamic segment

    v4 = *(_DWORD *)v3; //符号类型

    if ( *(_DWORD *)v3 ) {

        v5 = (int *)(v3 + 8);

        while ( 1 ) {

            switch ( v4 ) {

                //根据符号类型做重定位

            }

        }

    }

    LABEL_7:

    if ( *(_DWORD *)v3 )

    {

      v11 = 0;

      do

      {

        if ( v10 == 1 )

        {

          v13 = (const char *)(*((_DWORD *)v2 + 39) + *(_DWORD *)(v3 + 4));

          if ( strlen(v13) > 0x80 )

            goto LABEL_21;

          if ( *((_DWORD *)v2 + 72) <= v11 )

            goto LABEL_21;

          v14 = 136 * v11++;

          strncpy((char *)(*((_DWORD *)v2 + 73) + v14 + 4), v13, 0x7Fu);

          v15 = dlopen(v13, 0);  //加载所需要的so

          if ( !v15 )

            goto LABEL_21;

          *(_DWORD *)(*((_DWORD *)v2 + 73) + v14) = v15;

          *(_DWORD *)(*((_DWORD *)v2 + 73) + v14 + 132) = 0;

        }

        v12 = *(_DWORD *)(v3 + 8);

        v3 += 8;

        v10 = v12;

      }

      while ( v12 );

    }

}

dump second so

清楚第二个so的加载流程后,就可以dump出第二个so,具体做法是在sub_3DBC处下断点,dump内存,并且解密ELF header 和 program header table。解密算法是libjiagu.so中的sub_6868.

#sub_6868

def decryptso(data, size):

    result = bytearray(data)

    for i in range(size):

        result[i] ^= 0x50

    return str(result)

def dump_so(start_address, size):

    fp = open("E:\\dump.so", "wb")

    #read elf header and decrypt header

    data = idaapi.dbg_read_memory(start_address, 52)

    header = decryptso(data, 52)

    fp.write(header)

    #read elf program header tables

    phnum = 9

    data = idaapi.dbg_read_memory(start_address + 52,  phnum * 32)

    pht = decryptso(data, phnum * 32)

    fp.write(pht)

    #read other part

    data = data = idaapi.dbg_read_memory(start_address + 52 + phnum * 32,  size - phnum * 32 - 52)

    fp.write(data)

    fp.close()

进入JNI_Onload

在加载完第二个so后,libjiagu.so通过sub_3F7C(soinfo info, char fucname),来找到第二个so的JNI_Onload入口,并在case 35中跳转到第二个so的JNI_Onload.

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功