Dex解密

加密后的Dex数据被放在壳classes.dex的末尾。格式如下:

struct info {
    byte unknow[4];
    int  file_len; // 不包含头8个字节
    int  unknow_data_len;
    byte *unknow_data;
    int  dex_number; // 下面数组的数量

    struct {
        int full_size; // data_size + 4
        int data_size;
        byte *data;
    } dex[1];    
};

解密dex需要两步,第一步是将数据解密,得到一个用lzma压缩的数据,解压后,得到一个加密后的dex数据。第二步就是将加密的dex数据解密。两步解密都各需要一个key。key的计算跟info.unknow_data有关,模拟函数sub_E153C828(该函数在libjiagu.so释放的另外一个so内,下面的函数凡是以sub_E15开头的,都是在释放的so内)执行得到两个key。

sub_E153C828(info.unknow_data, info.unknow_data_len, &key1, &key2);
第一步解密:

key1需要再次计算:

// sub_E159CE08(&key3, &key1, 16)
  int sub_E159CE08(_BYTE *a1, int a2, int a3)
{
  _BYTE *v3; // r4
  signed int v4; // r5
  int v5; // r6
  unsigned int v6; // r7
  int v7; // r0
  char **v8; // r1
  _BYTE *v9; // r0
  int v10; // r2
  int result; // r0
  int v12; // [sp+8h] [bp-124h]
  int a2a; // [sp+Ch] [bp-120h]
  int v14; // [sp+10h] [bp-11Ch]
  int *v15; // [sp+14h] [bp-118h]

  a2a = a3;
  v12 = a2;
  v3 = a1;
  v4 = 256;
  v5 = 0;
  memset_0();
  v6 = 0;
  do
  {
    v3[v6] = v6;
    v7 = mod(v6, a2a);
    v8 = (char **)&v15;
    *((_BYTE *)&v15 + v6++) = *(_BYTE *)(v12 + v7);
  }
  while ( v6 != 256 );
  v9 = v3;
  do
  {
    v10 = (unsigned __int8)*v9;
    v5 = (v10 + v5 + *(unsigned __int8 *)v8) % 256;
    *v9 = v3[v5];
    v3[v5] = v10;
    --v4;
    v8 = (char **)((char *)v8 + 1);
    ++v9;
  }
  while ( v4 );
  result = _stack_chk_guard - v14;
  if ( _stack_chk_guard != v14 )
  {
    ((void (*)(void))unk_E15BF3EC)();
    JUMPOUT(&unk_E15D7BA4);
  }
  return result;
}

key1计算得出key3,用key3进行解密

// sub_E159CEB4(&key3, info.dex[x].data, info.dex[x].data_size);
int __fastcall sub_E159CEB4(int result, _BYTE *a2, int a3)
{
  int v3; // r3
  int v4; // r4
  int v5; // r3
  int v6; // ST08_4
  _BYTE *v7; // r6
  int v8; // r3
  int v9; // r5
  _BYTE *v10; // r0

  v3 = 0;
  if ( a3 )
  {
    v4 = 0;
    do
    {
      v5 = (v3 + 1) % 256;
      v6 = v5;
      v7 = (_BYTE *)(result + v5);
      v8 = *(unsigned __int8 *)(result + v5);
      v9 = result;
      v4 = (v8 + v4) % 256;
      v10 = (_BYTE *)(result + v4);
      *v7 = *v10;
      *v10 = v8;
      result = v9;
      *a2 ^= *(_BYTE *)(v9 + (((unsigned __int8)*v7 + v8) & 0xFF));
      v3 = v6;
      --a3;
      ++a2;
    }
    while ( a3 );
  }
  return result;
}
第二步解密

上一步得到的结果用lzma解压后,将每个字节与key2进行异或就可以了。

指令还原

原指令被加密并且opcode被改为私有的指令,将加密的指令使用0x65异或得到除opcode的原指令。比如:

// 原指令
6E 20 E7 2B 32 00
// 加密后的指令
A9 45 82 4E 57 65
// xor 0x65
CC 20 E7 2B 32 00

壳通过table[私有指令(上例的0xCC) + 0x5C]得到一个操作数,用这个操作数跳转到对应的 switch case 执行相应的dalvik指令。

然后使用参考2的方法还原所有指令:

有一个好点的办法就是:自己在onCreate方法中将所有的dalvik指令,一共200多条全部写出来。然后用360加固,动态调试,总结出每条dalvik指令对应的360解释器的case处理指令的偏移,最后得到一张指令映射表。这样,后续在脱壳的时候,就可以根据解释执行代码的偏移,还原出原来的指令。当然,360解释器也是在不断变化的,所以,这个表也是要跟着变化的。

参考

[1] 某VMP加固onCreate方法分析

[2] 某数字公司VMP脱壳简记