安卓逆向实践7——乐固脱壳实战

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hbhgyu/article/details/81437656

待分析应用zxxy拖到Android killer中反编译:

可以看到是加了乐固壳的。查看lib目录下的so文件为libshella-2.8.so,后缀为a表示基于ARM平台,x代表x86平台,这里的版本是2.8.0版本的乐固壳。要分析如何脱壳,那么这个libshella-2.8.so就是分析的重点了。拖到IDA中静态分析,一开始就无法解析并且出现警告提示,这里老师上课讲过了,是对elf文件中的section header table做了混淆处理,因为我们在实际装载过程中并不需要链接视图中的section信息,所以只需要找到section header table将其全部置为0,就可正常打开了。这里查看section header table的文件偏移以及大小可以在虚拟机Linux上使用readelf -a libshella-2.8.so来查看,然后使用010Editor来编辑。另外一种方法就是使用xAnSo-master 这个工具进行修复,这是Github上面提供的,搜一下就能找到。==
处理之后IDA静态分析如下:

这里就能查看到一些导入函数了,在exports导出函数中可以看到JNI_Onload函数,点击去看发现数据都是经过加密处理的,直接make code(快捷键c)并不能将数据转化为代码。那么猜想JNI_Onload函数肯定是在init_array或者init中进行了处理。接下来进行动态调试so文件进行分析,调试的步骤这里就不多说了,请参考上一篇文章。
这里写图片描述
点击运行,在上图中加载libshella-2.8.so时停下来,接下来我们先定位到init_array的位置:
这里写图片描述
可以看到init_array的偏移地址为0x3e84,在动态分析IDA中ctrl+s查看libshella-2.8.so加载到内存中的基地址:
这里写图片描述
那么其地址为基地址+偏移地址,0x7631FE84,
这里写图片描述
这里对应函数地址0x7631C944, 跳转到该地址查看。

// write access to const memory has been detected, the output may be wrong!
int sub_944()
{
  unsigned int v0; // ST30_4@7
  unsigned int v1; // ST2C_4@10
  unsigned int v2; // ST28_4@10
  char v3; // ST1B_1@14
  int v4; // r0@16
  int j; // [sp+34h] [bp-18h]@13
  char v7; // [sp+38h] [bp-14h]@13
  char v8; // [sp+39h] [bp-13h]@13
  char v9; // [sp+3Ah] [bp-12h]@13
  char v10; // [sp+3Bh] [bp-11h]@13
  unsigned int v11; // [sp+3Ch] [bp-10h]@4
  int v12; // [sp+40h] [bp-Ch]@4
  int i; // [sp+44h] [bp-8h]@1

  for ( i = (unsigned int)sub_944 & 0xFFFFF000; *(_DWORD *)i != 1179403647; i -= 4096 )
    ;
  v12 = *(_DWORD *)(i + 28) + i;
  v11 = 0;
  while ( *(_WORD *)(i + 44) > v11 )
  {
    if ( *(_DWORD *)v12 != 1 || *(_DWORD *)(v12 + 24) != 5 )
    {
      if ( *(_DWORD *)v12 == 1 && *(_DWORD *)(v12 + 24) == 6 )
      {
        v1 = *(_DWORD *)(v12 + 8) & 0xFFFFF000;
        v2 = (*(_DWORD *)(v12 + 8) + *(_DWORD *)(v12 + 16) + 4095) & 0xFFFFF000;
        break;
      }
    }
    else
    {
      v0 = (*(_DWORD *)(v12 + 8) + *(_DWORD *)(v12 + 16) + 4095) & 0xFFFFF000;
    }
    ++v11;
    v12 += 32;
  }
  v10 = 43;
  v9 = -103;
  v8 = 32;
  v7 = 21;
  mprotect(i, 0, 3);
  for ( j = 0; j <= 0; ++j )
  {
    v3 = *(_BYTE *)(i + j);
    *(_BYTE *)(i + j) ^= (unsigned __int8)(((v9 - v8) ^ j) + v7) ^ v10;
    *(_BYTE *)(i + j) += v8 & v7 ^ v9;
    v10 += (v9 + v8 - v7) & v3 & j;
    v9 += (j + v10) ^ v3;
    v8 ^= (v3 - v10) ^ j;
    v7 += j - (v3 + v10);
  }
  mprotect(i, 0, 5);
  v4 = sub_7E8(i, 0);
  dword_4008 = i;
  return sub_898(v4);
}

这里就是init_array对应的函数,通过查看相应的函数可以看到前面实际上是进行了一些字节变换,然后调用了mprotect,sub_B6F083DC和sub_B6F0F630三个函数。sub_B6F083DC函数就是做了 cacheflush和syscall操作。猜想这里就是对JNI_Onload代码区的数据进行解密还原,这里不是关注的重点。我们直接运行单步进入到sub_898函数内部,当前版本中并没有反调试代码。
这里写图片描述
更改名称之后的两个函数向日志中打印tag为txtag的日志信息,然后留意一下第一个函数,点进去看到有“LD_PRELOAD”关键字,猜想这里应该是跟环境变量共享库的设置有关,无需过分关注,直接单步跳过去。查看最后一个函数调用,unk_7631E74C:
这里写图片描述
其实这个地址7631E74C刚好就是静态分析对应的JNI_Onload地址,也就是说此时加密数据已经被还原了,能够被我们IDA识别出来。
具体的流程就不用管了,我们知道肯定会去执行JNI_Onloadh函数,就在上图这个函数中下断点,然后运行到这个地方:
这里写图片描述
这里想说一个很有效的调试技巧,在调试时可以结合反汇编出来的函数形式,通过查看参数以及返回值来判断函数的大致功能(查看寄存器值,arm中r0-r4传递参数)。
通过查看参数我们看到这段代码调用了dlopen和dlsym两个函数。dlopen打开一个动态链接库,dlsym根据动态链接库操作句柄(handle)与符号(symbol),返回符号对应的地址(这里的符号为JNI_Onload)。使用这个函数不但可以获取函数地址,也可以获取变量地址。
那么也就是说最终v7才是真正希望执行的JNI_Onload函数地址,也就是对应如下arm代码中的BLX R3;中的R3寄存器中的地址:
这里写图片描述
查看对应的C代码:

signed int __fastcall sub_7632DADC(int a1)
{
  signed int v1; // r4@1
  int v2; // r6@1
  int v4; // [sp+4h] [bp-14h]@1

  v1 = 0;
  v2 = a1;
  v4 = 0;
  if ( ((int (__cdecl *)(int, int *))unk_7632DA50)(a1, &v4) )
  {
    if ( ((int (__fastcall *)(_DWORD, _DWORD, signed int))unk_7632DA50)(v2, &v4, 65540) )
    {
      if ( ((int (__fastcall *)(int, int *, signed int))unk_7632DA50)(v2, &v4, 65538) )
      {
        if ( ((int (__fastcall *)(int, int *, signed int))unk_7632DA50)(v2, &v4, 65537) )
          return v1;
        v1 = 65537;
      }
      else
      {
        v1 = 65538;
      }
    }
    else
    {
      v1 = 65540;
    }
  }
  else
  {
    v1 = 65542;
  }
  if ( v4 )
  {
    ((void (*)(void))unk_7632E270)();
    ((void (*)(void))unk_7632DAA0)();
  }
  return v1;
}

依次点击其中的函数查看代码,最后一个函数代码如下所示:
这里写图片描述
看到了registerNatives,那么这里肯定就是手动注册native方法了,跟到上面的函数中:
这里写图片描述
那么这里我们重点关注RegisterNatives函数的参数,其函数原型为:
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)
第二个参数是一个结构体:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;

也就是说,我们跟踪到这个参数的时候就可以知道其注册的native方法加载到内存后的函数地址,即对应结构体的第三个参数。从而可以获取到在Java代码中调用了哪些原生函数进行脱壳操作。这里结构体指针对应着第三个参数v6,因为第一个参数还传入了env。
单步运行到这里:
这里写图片描述
这里我们看到第一个注册方法的地址为7628CB55,那么实际的地址应该为7628CB56,跳转到该地址,查看反汇编代码:
这里写图片描述
通过一些打印信息提示可以猜测这里肯定是与脱壳加载相关的函数代码。
这里写图片描述
这里根据不同的虚拟机指令版本执行不同的Load操作,那么这里的标出来的函数就应该是脱壳并加载的关键代码了。
接下来如何dump dex直接参考后面给出来的链接。
脱壳结果:
这里写图片描述
这里还是比较坑的,调试过程中即使是在真机上也会报出各种异常,不过还好最后一次调试成功了。

参考链接:
乐固壳分析
乐固加固软件脱壳分析

猜你喜欢

转载自blog.csdn.net/hbhgyu/article/details/81437656