原作者:Kai Lu

译者:BlackTrace

原文链接

最近,我们发现了一个新的android rootnik 恶意app,它使用了开源的Android root漏洞利用工具和来自dashi root工具里的MTK root方案在Android设备上获取root权限。这个恶意软件把自己伪装成文件管理app(file helper app),其次还使用了很高级的反调试和反hook技来防止被逆向工程。它还使用了多个dex的方案去加载第二个dex文件。在成功获取了设备的root权限后,rootnik恶意app能执行多个恶意行为,例如,app和广告的推广,色情的推广,在主屏幕上创建快捷方式,静默安装app,推送通知等。在这个博客,我将深度分析这个恶意app。

快速浏览这个恶意app

这个恶意app 看起来像你android手机里管理你的文件或者其他存储的资源的合法文件管理器(file helper app)。

恶意app的安装后的图标

图1. 恶意app的安装后的图标

恶意app的界面

图2. 恶意app的界面

我们来反编译这个apk文件,结果如图3

反编译结果

图3. 反编译后的文件

它的包名是com.web.sdfile。首先让我们来看一下它的AndroidManifest.xml文件。

图4. 这个app里面的AndroidManifest.xml文件

我们并没有在main activity中找到图4中的com.sd.clip.activity.FileManagerActivity, service class, broadcast class。很明显,这个文件管理器(file helper app)的主要逻辑代码并没有在这个classes.dex。经过分析后返现,这个恶意app使用了multidex方案去动态加载和运行第二个dex。

Rootnik是如何工作的?

I. Rootnik工作流程

下图是这个Android rootnik malware的工作流程

Android rootnik malware的工作流程

图5. Android rootnik malware的工作流程

II. 深度分析第一个dex文件

下图是SecAppWrapper类的代码片段

SecAppWrapper类的代码片段

图6. SecAppWrapper类的代码片段

这个执行流程如下

静态代码块 -> attachBaseContext -> onCreate

静态代码快加载动态链接库 libSecShell.so到assets文件夹,并进入native层执行多个反调试操作,解密第二个dex文件,然后使用multidex方案去加载解密后的第二个dex文件,解密后的第二个dex文件,才是真正的程序主逻辑。

类Dexinstall其实是类MultiDex,关于Multidex的资料https://android.googlesource.com/platform/frameworks/multidex/+/d79604bd38c101b54e41745f85ddc2e04d978af2/library/src/android/support/multidex/MultiDex.java

这个程序调用Dexinstall里面的install方法去加载第二个dex文件。这个install方法是在native层调用的。

安装第二个dex 图7 安装第二个dex

这个程序在attachBaseContext方法里加载了第二个dex的运行入口class com.sd.clip.base.MyApplication。这个Helper类的attach是native方法。 接着程序运行了类com.sd.clip.base.MyApplication里的onCreate方法。好了就这些了,第一个dex文件相当简单的。接下来,我们将深度分析native层的代码,这是非常复杂和棘手的。

III.native层的代码

上面说过,native层使用了一些高级的反调试和反hook技术,并且还使用了几种解密算法对一些字节数组进行解密以得到纯文本字符串。

下图8是libSecShell.so的导出函数。它混淆了函数名使分析变得更加困难,更加费劲。

图8 libSecShell.so的导出函数

图8.libSecShell.so的导出函数

所有的反调试代码都在JNI_Onload函数里面

在上一节说过,java层中的Helper类的attach方法是native方法。这个程序是在native层动态注册这个方法。下图是程序在native层动态注册这个native方法的ARM汇编代码片段。 图9 动态注册native方法

图9 动态注册native方法

RegisterNatives方法是用来注册native方法的,这个方法的原型如下

jint RegisterNatives(JNIEnv *env,jclass clazz, const JNINativeMethod* methods,jint nMethods)

JNINativeMethod结构定义如下

typedef struct { const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod; 第一个变量name是指在java层调用的name,这里是"attach",第三个变量是fnPtr空指针是一个函数指针,指向要动态注册的C代码的函数

我们需要找到这个反调试代码的位置并且绕过它。还要分析第二个dex文件是如何解密的,并且在内存中dump出解密后的第二个dex文件。

让我们在IDA中看一下接下来的代码 反调试代码附近

图10. 反调试代码附近

经过我们的深度分析,在地址0xF832这里的这里的指令是跳转到地址loc_F924,跟过去后,我们找到了反调试代码。

图11 反调试代码的位置

图11.反调试代码的位置

在p7E7056598F77DFCC42AE68DF7F0151CA()里执行了反调试的操作。

在IDA的graphic中看一下执行流程,如下图,可以看出,这个流程是相当复杂的。

图12 反调试的执行流程

图12.反调试的执行流程

下面是这个恶意app里使用的一些反调试和反hook的方法。

1. 检测一些流行的hook框架,如 Xposed, substrate, adbi, ddi, dexposed。一旦发现,流行hook框架hook了它,它将结束相关的进程。

图14. 查找hook框架

2. 使用一种多进程ptrace来实现反调试,这还是有点棘手的。在这里我们不提供这种反调试的实现机制的详细分析,但是我们会给一些简单的解释。

我们在这里看见了名字为com.web.sdfile的两个进程。

图15. 进程名为com.web.sdfile的两个进程

下图是多进程反调试代码片段

图16. 反调试代码片段

3. 这个程序还使用了inotify来监控内存和主进程的pagemag,当内存dump不完整,这两个进程将使用pipe互相通知对方。

总之,这些反调试和反hook给逆向工程创造了很大的障碍,所以我们绕过这些反调试和反hook方法是我们的首要任务。

让我们尝试绕过它们。

在图10中,在偏移0x0000F832的指令是跳转到loc_F924,然后程序执行反调试代码。当我们动态调试的时候,我们可以修改一些寄存器的值或者是ARM指令来改变执行流程。当程序运行到偏移0xF828的SUBS R1, R0, #0时,我们可以把R0寄存器的值修改为非0值,使得指令BNE loc_F834的条件成立。这将允许这个程序跳转到loc_F834。

图17. 如何绕过反调试代码

接下来,我们动态调试它,绕过反调试并且dump解密后的第二个dex文件。动态调试过程如下

图18 修改R0寄存器的值为非0值

图19 跳转到local_75178834

接着跳转到local_751788D8,如下

图20. 解密第二个dex的函数

函数p34D946B85C4E13BE6E95110517F61C41就是解密函数,R0寄存器指向内存中存储的加密的dex文件,R1寄存器里存着这个文件的大小,文件大小等于0x94A3E(608830)。解密dex文件是在apk包里的assets文件夹下的secData0.jar文件。下图是secData0.jar

图21.这个文件secData0.jar是在assets文件夹下。

图22. 内存中解密后的第二个apk的内容

我们现在把解密后的文件dump到decrypt.dump中。

这个解密后的文件是zip的格式,它包含了第二个dex文件。解密后,这个程序解压这个解密后的apk文件到dex文件。方法p3CBBD6F30D91F38FCD0A378BE7E54877是用来解压这个文件的。 接下来,unk_75176334方法调用com.secshell.shellwrapper.DexInstall类的java方法install加载第二个dex。

图23. 解压解密后的apk文件并且加载第二个dex文件

图24. 通过JNI调用方法安装

这里,我们完成了native层的分析并且得到解密后的第二个apk文件,然后我们将分析这个apk文件在这个博客的part II 。

Native层的secData0.jar的解密方法

int __fastcall sub_7518394C(int result, _BYTE *a2, int a3)  
{
  int v3; // r1@1
  int v4; // r3@5
  unsigned int v5; // r3@7
  int v6; // r6@7
  int v7; // r5@7
  char v8; // r2@8
  int v9; // r4@9
  int v10; // r3@9
  int v11; // r7@11
  _BYTE *v12; // r6@12
  int v13; // r4@13
  _BYTE *v14; // r1@15
  int v15; // [sp+0h] [bp-138h]@1
  int v16; // [sp+4h] [bp-134h]@1
  _BYTE *v17; // [sp+8h] [bp-130h]@1
  int v18; // [sp+10h] [bp-128h]@5
  char v19[256]; // [sp+1Ch] [bp-11Ch]@6
  int v20; // [sp+11Ch] [bp-1Ch]@1

  v17 = a2;
  v16 = result;
  v20 = _stack_chk_guard;
  v15 = a3;
  v3 = 0;
  if ( result <= 0x1FFFF )
  {
    v3 = 0x20000 - result;
    if ( 0x20000 - result > a3 )
      v3 = a3;
    v15 = a3 - v3;
    if ( v3 > 0 )
    {
      v18 = dword_751AF650;
      v4 = 0;
      do
      {
        v19[v4] = v4;
        ++v4;
      }
      while ( v4 != 256 );
      v5 = 0;
      v6 = 0;
      v7 = 0;
      do
      {
        v6 = (*(_BYTE *)(v18 + v5) + (unsigned __int8)v19[v7] + v6) & 0xFF;
        v8 = v19[v7];
        v5 = (v5 + 1) & -((v5 + 1 <= 0xF) + ((v5 + 1) >> 31));
        v19[v7] = v19[v6];
        v19[v6] = v8;
        ++v7;
      }
      while ( v7 != 256 );
      v9 = 0;
      result = 0;
      v10 = 0;
      while ( v9 != v16 )
      {
        v10 = (v10 + 1) & 0xFF;
        v11 = (unsigned __int8)v19[v10];
        ++v9;
        result = (v11 + result) & 0xFF;
        v19[v10] = v19[result];
        v19[result] = v11;
      }
      v12 = v17;
      do
      {
        v10 = (v10 + 1) & 0xFF;
        v13 = (unsigned __int8)v19[v10];
        result = (result + v13) & 0xFF;
        v19[v10] = v19[result];
        v19[result] = v13;
        *v12++ ^= v19[(v13 + (unsigned __int8)v19[v10]) & 0xFF];
      }
      while ( v12 != &v17[v3] );
    }
  }
  if ( v15 > 0 )
  {
    v14 = &v17[v3];
    result = (int)v14;
    do
      *v14++ ^= 0xACu;
    while ( (signed int)&v14[-result] < v15 );
  }
  if ( v20 != _stack_chk_guard )
    result = ((int (*)(void))unk_75173E48)();
  return result;
}

第一次翻译,难免会有错误,还请指正。万分感谢 --BlackTrace