从样本中学习反调试


前言

  最近对某银行app进行了重新逆向,发现他的安全防护措施几个月来并没有更新,还是很容易就搞定了反调试,它的表现是在一开头就把自己ptrace上导致你无法使用Frida进行hook,但是用-f参数进行spawn模式启动就能成功hook上了。现在的app如果在so层做加壳反调之类的一般都会加上ollvm,对逆向人员极不友好,我遇到的这个样本是难得的没有用ollvm的样本,所以虽然成功绕过了它的反调,那就顺便拿它来学习一下它是如何做防护的。

0x01 SO Header破坏与修复

  首先这个so是没有对头部进行破坏的,直接拖到IDA中就能看,所以顺便说下破坏so是啥情况正好也遇到了一个case。这个case叫的壳叫vdog,但是lib目录没有这个so,然后在它的生命周期函数里找到这个初始化函数,发现它的so藏在asset的main000目录下,还有dex文件也是。直接把so拖到ida发现无法识别,肯定是被处理过了。看看下面的函数

public static void a(Context context) {
        String soPath;
        try {
            Defines.context = context;
            StringBuilder v16 = new StringBuilder().append(context.getApplicationInfo().dataDir);
            a.a = v16.append("/").toString();
            StringBuilder v16_1 = new StringBuilder().append(a.a);
            a.a(v16_1.append(".cache/").toString());
            StringBuilder v16_2 = new StringBuilder().append(a.a);
            a.a(v16_2.append(".local/").toString());
            StringBuilder v16_3 = new StringBuilder().append(a.a);
            a.a(v16_3.append(".meta-inf/").toString());
            String soName = a.b("libvdog.so");
            StringBuilder v16_4 = new StringBuilder().append(a.a);
            StringBuilder v16_5 = v16_4.append(".cache/");
            soPath = v16_5.append("libvdog.so").toString();
            String soSqliteName = a.b("libsqlite_encrypt.so");
            StringBuilder v16_6 = new StringBuilder().append(a.a);
            StringBuilder v16_7 = v16_6.append(".cache/");
            String soSqilitePath = v16_7.append("libsqlite_encrypt.so").toString();
            String soNmgName = a.b("libnMg.so");
            StringBuilder v16_8 = new StringBuilder().append(a.a);
            StringBuilder v16_9 = v16_8.append(".cache/");
            String soNmgPath = v16_9.append("libnMg.so").toString();
            StringBuilder v16_10 = new StringBuilder().append(a.a);
            StringBuilder v16_11 = v16_10.append(".cache/");
            String _main_data = v16_11.append("main.data").toString();
            StringBuilder v16_12 = new StringBuilder().append(a.a);
            StringBuilder v16_13 = v16_12.append(".cache/");
            String _data_rc = v16_13.append("datarc").toString();
            StringBuilder v16_14 = new StringBuilder().append(a.a);
            StringBuilder v16_15 = v16_14.append(".cache/");
            String _res_data = v16_15.append("res.data").toString();
            StringBuilder v16_16 = new StringBuilder().append(a.a);
            StringBuilder v16_17 = v16_16.append(".cache/");
            String _cert0 = v16_17.append("cert0").toString();
            StringBuilder v16_18 = new StringBuilder().append(a.a);
            StringBuilder v16_19 = v16_18.append(".meta-inf/");
            String _enc_mf = v16_19.append("enc.mf").toString();
            StringBuilder v16_20 = new StringBuilder().append(a.a);
            StringBuilder v16_21 = v16_20.append(".cache/");
            String tablePath = v16_21.append("table.dat").toString();
            StringBuilder v16_22 = new StringBuilder().append(a.a);
            StringBuilder v16_23 = v16_22.append(".cache/");
            String screenTable = v16_23.append("scrtable.dat").toString();
            if(!a.b(context) && (new File(soPath).exists())) {
                StringBuilder v17 = new StringBuilder().append(a.a);
                String v17_1 = v17.append(".cache/").toString();
                if(new File(v17_1, "classes.dex").exists()) {
                    goto label_261;
                }

                goto label_152;
            }
            else {
            label_152:
                StringBuilder v16_24 = new StringBuilder().append(a.a);
                a.c(v16_24.append(".cache/").toString());
                StringBuilder v16_25 = new StringBuilder().append(a.a);
                a.c(v16_25.append(".local/").toString());
                StringBuilder v16_26 = new StringBuilder().append(a.a);
                a.c(v16_26.append(".meta-inf/").toString());
                a.a(_main_data, "main000/main.data", context);
                a.a(_data_rc, "main000/datarc", context);
                a.a(_res_data, "main000/res.data", context);
                a.a(_cert0, "main000/cert0", context);
                a.a(_enc_mf, "meta-inf/enc.mf", context);
                a.a(tablePath, context);
                a.a(screenTable, "main000/scrtable.dat", context);
                StringBuilder v16_27 = new StringBuilder();
                a.a(soPath, v16_27.append("main000/").append(soName.substring(0, soName.length() - 3)).toString(), context);
                StringBuilder v16_28 = new StringBuilder();
                a.a(soSqilitePath, v16_28.append("main000/").append(soSqliteName.substring(0, soSqliteName.length() - 3)).toString(), context);
                StringBuilder v16_29 = new StringBuilder();
                a.a(soNmgPath, v16_29.append("main000/").append(soNmgName.substring(0, soNmgName.length() - 3)).toString(), context);
                StringBuilder v16_30 = new StringBuilder().append(a.a);
                a.a(context, v16_30.append(".cache/").toString());
                a.c(context);
            }
        }
        catch(Exception e) {
            goto label_270;
        }

        try {
        label_261:
            System.loadLibrary("libvdog.so".substring(3, "libvdog.so".length() - 3));
            return;
        }
        catch(Throwable e) {
        }

        try {
            System.load(soPath);
            return;
        }
        catch(Exception e) {
        }

    label_270:
        e.printStackTrace();
}

最后so文件的读取是这个函数

private static void a(String output, String input, Context context) {
    try {
        InputStream inputStream = context.getAssets().open(input);
        File localFile = new File(output);
        byte[] bytes = new byte[0x10000];
        BufferedInputStream bufferedInput = new BufferedInputStream(inputStream);
        BufferedOutputStream bufferedOutput = new BufferedOutputStream(new FileOutputStream(localFile));
        int first = 1;
        byte[] v7 = new byte[]{0x7F, 69, 76, 70};
        while(true) {
            int i = bufferedInput.read(bytes);
            if(first != 0) {
                first = 0;
                if(output.endsWith(".so")) {
                    System.arraycopy(((Object)v7), 0, ((Object)bytes), 0, v7.length);
                }
            }

            if(i <= 0) {
                bufferedOutput.flush();
                bufferedOutput.close();
                bufferedInput.close();
                return;
            }

            bufferedOutput.write(bytes, 0, i);
        }
    }
    catch(Exception v8) {
        return;
    }
}

原来它把elf的magic number改了,拖到010editor里发现被改成了GODV,根据这个代码new byte[]{0x7F, 69, 76, 70}改回去就行了,然后拖到ida里还是会报错,因为它把header改的面目全非了,参考这篇文章,虽然一直点确认也勉强能看但是还原一下更方便我们看。如果对ELF格式很熟悉的话可以手动还原,不过懒狗找到了这个工具,很方便能直接修复SO,修复完成后拖到IDA里就能不报错了。但是很可惜这个SO基本上所有函数都有ollvm的控制流平坦化,放弃了。

0x02 入口函数

  SO文件被加载以后首先会执行init函数(如果有的话),然后执行JNI_Onload,这个顺序是由安卓的linker决定的,在安卓源码中可以看到,相关的文章有很多,随便列一篇。看了下init.array里只有一个叫setup_opt_hookv的函数,和反调试无关,所以直接快进到JNI_Onload函数

  JNI_Onload函数开头一堆初始化相关代码具体干啥的没注意,不过在最底下有个很显眼的protect_self函数,直接快进康康它是怎么实现的。
JNI_Onload

0x03 protect_self

完整代码如下:

__pid_t __fastcall protect_self(char a1, int a2, int a3)
{
  //省略无关代码

  prctl(4, 1, 0, 0, 0);
  result = getpid();
  if ( result == javapid )
  {
    v4 = getpid();
    javapid = v4;
    if ( (a1 & 2) != 0 )
    {
      v4 = sub_3902C(a2);
      if ( !v4 )
      {
        v5 = getpid();
        v4 = sub_3A5F8(v5, a3, a2);
      }
    }
    if ( (a1 & 4) != 0 )
      sub_38D90(v4);
    if ( (a1 & 8) != 0 )
      sub_3B54C(a2);
    if ( (a1 & 1) != 0 )
    {
      result = protect_polling(javapid);
    }
    else
    {
      pipe(&v21);
      pipe(v23);
      pipe(v24);
      v14 = sub_38F8C(a2);
      v15 = fork();
      if ( v15 <= 0 )
      {
        if ( v15 )
        {
          result = protect_polling(javapid);
        }
        else
        {
          close(v21);
          close(v24[0]);
          close(v23[1]);
          anti_thread_of_process_debug(javapid);
          monitor_env(javapid);
          if ( !v14 )
          {
            v19 = dlopen("/system/lib/libc.so", 0);
            hookFun(v19, "ptrace", (int)ptrace_stub, (int)&libc_ptrace);
          }
          v12 = 0;
          if ( libc_ptrace(16, javapid, 0, 0) < 0 )
            v12 = -1;
          if ( !v12 )
          {
            wait(&v10);
            libc_ptrace(7, javapid, 0, 0);
          }
          v25[0] = v12;
          write(fd, v25, 1u);
          arg = malloc(8u);
          memset(arg, 0, 8u);
          *(_DWORD *)arg = v23[0];
          *((_DWORD *)arg + 1) = getpid();
          if ( v12 )
          {
            result = keep_pipe_connect(arg);
          }
          else
          {
            for ( i = 30; pthread_create(&v10, 0, (void *(*)(void *))keep_pipe_connect, arg) && i > 0; --i )
              sleep(1u);
            handle_catched_signal(javapid);
            libc_ptrace(17, javapid, 0, 0);
            v6 = getpid();
            mykill(v6, 9);
            result = mykill(javapid, 9);
          }
        }
      }
      else
      {
        close(fd);
        close(v24[1]);
        close(v23[0]);
        v16 = dlopen("/system/lib/libc.so", 0);
        hookFun(v16, "ptrace", (int)ptrace_stub, (int)&libc_ptrace);
        anti_thread_of_process_debug(v15);
        monitor_env(v15);
        result = read(v21, v25, 1u);
        v17 = result;
        if ( result != -1 && !v25[0] )
        {
          s = malloc(8u);
          memset(s, 0, 8u);
          *(_DWORD *)s = v24[0];
          *((_DWORD *)s + 1) = javapid;
          for ( j = 30; ; --j )
          {
            result = pthread_create(&v10, 0, (void *(*)(void *))keep_pipe_connect, s);
            if ( !result || j <= 0 )
              break;
            sleep(1u);
          }
        }
      }
    }
  }
  return result;
}

先来看下第一个函数sub_3902C

bool __fastcall sub_3902C(int a1)
{
  char v3[64]; // [sp+Ch] [bp+Ch] BYREF

  if ( a1 != 23 )
    return 0;
  sub_38F44("ro.product.model", v3, &unk_671F8);
  return strcmp(v3, "HUAWEI eH880") == 0;
}

只是查看机型是否为HUAWEI eH880意义不大,下一个

sub_3A5F8

int __fastcall sub_3A5F8(__pid_t a1, int a2, int a3)
{
  int result; // r0
  pthread_t v6; // [sp+10h] [bp+10h] BYREF
  pthread_t v7; // [sp+14h] [bp+14h] BYREF
  int i; // [sp+18h] [bp+18h]
  void *arg; // [sp+1Ch] [bp+1Ch]
  void *v10; // [sp+20h] [bp+20h]

  if ( check_xposed(a2) )
    mykill(a1, 9);
  arg = malloc(4u);
  *(_DWORD *)arg = a1;
  for ( i = 30; pthread_create(&v6, 0, (void *(*)(void *))scan_proc_jars, arg) && i > 0; --i )
    sleep(1u);
  result = sub_3A55C(a3);
  if ( !result )
  {
    v10 = malloc(4u);
    *(_DWORD *)v10 = a1;
    for ( i = 30; ; --i )
    {
      result = pthread_create(&v7, 0, (void *(*)(void *))scan_proc_libs, v10);
      if ( !result || i <= 0 )
        break;
      sleep(1u);
    }
  }
  return result;
}

可以看到这边对xposed进行了检测,康康具体是怎么做的

check_xposed里有sub_3CBE8、sub_3CD48和sub_3CE04三个函数,一个一个看

sub_3CBE8

{
  _BOOL4 v1; // r3
  int v4; // [sp+8h] [bp+8h]
  int v5; // [sp+Ch] [bp+Ch]
  int v6; // [sp+10h] [bp+10h]
  int v7; // [sp+14h] [bp+14h]
  int v8; // [sp+18h] [bp+18h]
  int v9; // [sp+1Ch] [bp+1Ch]
  int v10; // [sp+20h] [bp+20h]
  int v11; // [sp+24h] [bp+24h]
  int v12; // [sp+28h] [bp+28h]
  int v13; // [sp+2Ch] [bp+2Ch]
  char *haystack; // [sp+30h] [bp+30h]
  int v15; // [sp+34h] [bp+34h]
  int v16; // [sp+38h] [bp+38h]
  char *v17; // [sp+3Ch] [bp+3Ch]

  v4 = _JNIEnv::FindClass(a1, "java/lang/Thread");
  v5 = _JNIEnv::GetStaticMethodID(a1, v4, "currentThread", "()Ljava/lang/Thread;");
  v6 = _JNIEnv::CallStaticObjectMethod(a1, v4, v5);
  v7 = _JNIEnv::GetMethodID(a1, v4, "getStackTrace", "()[Ljava/lang/StackTraceElement;");
  v8 = _JNIEnv::CallObjectMethod(a1, v6, v7);
  v9 = _JNIEnv::GetArrayLength(a1, v8);
  v10 = _JNIEnv::GetObjectArrayElement(a1, v8, v9 - 1);
  v11 = _JNIEnv::FindClass(a1, "java/lang/StackTraceElement");
  v12 = _JNIEnv::GetMethodID(a1, v11, "getClassName", "()Ljava/lang/String;");
  v13 = _JNIEnv::CallObjectMethod(a1, v10, v12);
  haystack = (char *)_JNIEnv::GetStringUTFChars(a1, v13, 0);
  v15 = _JNIEnv::GetObjectArrayElement(a1, v8, v9 - 2);
  v16 = _JNIEnv::CallObjectMethod(a1, v15, v12);
  v17 = (char *)_JNIEnv::GetStringUTFChars(a1, v16, 0);
  if ( strcasestr(haystack, "xposed") )
    v1 = 1;
  else
    v1 = strcasestr(v17, "xposed") != 0;
  return v1;
}

通过打印函数调用栈检测是否有xposed特征

sub_3CD48

int __fastcall sub_3CD48(_JNIEnv *a1)
{
  int v5; // [sp+8h] [bp+8h]
  int v6; // [sp+Ch] [bp+Ch]
  int v7; // [sp+10h] [bp+10h]
  int v8; // [sp+14h] [bp+14h]

  v5 = _JNIEnv::FindClass(a1, "java/lang/ClassLoader");
  v6 = _JNIEnv::GetStaticMethodID(a1, v5, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
  v7 = _JNIEnv::CallStaticObjectMethod(a1, v5, v6);
  v8 = _JNIEnv::GetMethodID(a1, v5, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
  _JNIEnv::NewStringUTF(a1, "de.robv.android.xposed.XposedHelpers");
  if ( _JNIEnv::CallObjectMethod(a1, v7, v8) && !_JNIEnv::ExceptionCheck(a1) )
    return 1;
  _JNIEnv::ExceptionClear(a1);
  return 0;
}

通过类加载器主动加载Xposed根据加载结果来判断是否存在Xposed框架

sub_3CE04

int __fastcall sub_3CE04(_JNIEnv *a1)
{
  int v5; // [sp+8h] [bp+8h]
  int v6; // [sp+Ch] [bp+Ch]

  v5 = _JNIEnv::FindClass(a1, "android/os/ServiceManager");
  v6 = _JNIEnv::GetStaticMethodID(a1, v5, "getService", "(Ljava/lang/String;)Landroid/os/IBinder;");
  _JNIEnv::NewStringUTF(a1, "user.xposed.system");
  if ( _JNIEnv::CallStaticObjectMethod(a1, v5, v6) && !_JNIEnv::ExceptionCheck(a1) )
    return 1;
  _JNIEnv::ExceptionDescribe(a1);
  _JNIEnv::ExceptionClear(a1);
  return 0;
}

通过判断ServiceManager里是否有Xposed的Service来检测

回到sub_3A5F8,如果check_xposed返回true则调用mykill函数来关闭自身进程,来康康mykill函数

unsigned int __fastcall mykill(__pid_t a1, int a2)
{
  unsigned int result; // r0

  result = linux_eabi_syscall(__NR_kill, a1, a2);
  if ( result > 0xFFFFF000 )
    result = j_my_set_errno(-result);
  return result;
}

可以看到是用系统调用来关闭自己的,可以防止exit函数被hook的情况。看下它的汇编代码

.text:0003895C                 EXPORT mykill
.text:0003895C mykill                                  ; CODE XREF: monitor_pid+2E↑p
.text:0003895C                                         ; monitor_pid+3C↑p ...
.text:0003895C ; __unwind {
.text:0003895C                 PUSH            {R4-R7,R12,LR}
.text:00038960                 MOV             R7, #0x25 ; '%'
.text:00038964                 SVC             0
.text:00038968                 POP             {R4-R7,R12,LR}
.text:0003896C                 CMN             R0, #0x1000
.text:00038970                 BXLS            LR
.text:00038974                 RSB             R0, R0, #0
.text:00038978                 B               j_my_set_errno
.text:00038978 ; } // starts at 3895C
.text:00038978 ; End of function mykill

里边有个SVC指令,就是用来执行系统调用的,具体的调用号通过R7寄存器传参,这边0x25就是指__NR_kill,有个系统调用的表来映射调用号与其具体的功能。可以参考这篇文章

接下来看sub_3A5F8中的scan_proc_jars函数

scan_proc_jars

int __fastcall scan_proc_jars(__pid_t *a1)
{
  int v2; // [sp+Ch] [bp+Ch]
  FILE *v3; // [sp+10h] [bp+10h]
  char v4[256]; // [sp+14h] [bp+14h] BYREF
  char v5[1024]; // [sp+114h] [bp+114h] BYREF
  char v6[1024]; // [sp+514h] [bp+514h] BYREF

  v2 = *a1;
  free(a1);
  sub_5B390((int)v4, 256, "/proc/%d/maps", v2);
  v3 = fopen(v4, "r");
  if ( v3 )
  {
    memset(v5, 0, sizeof(v5));
    memset(v6, 0, sizeof(v6));
    sub_39E0C(v2, (int)v6);
    while ( fscanf(v3, "%*p-%*p %*c%*c%*c%*c %*s %*s %*d%1023[^\n]", v5) == 1 )
    {
      readLine((int)v5);
      if ( v5[0] && jar_filter(v5, v6) && is_xposed_att(v5) )
        mykill(v2, 9);
    }
    fclose(v3);
  }
  return 0;
}

可以知道就是从/proc/pid/maps中读取内容然后进行jar_filter和is_xposed_att两个函数的检测。jar_filter没懂是干啥的,来看看is_xposed_att

is_xposed_att

bool __fastcall is_xposed_att(const char *a1)
{
  _BOOL4 v1; // r3

  if ( a1 )
    v1 = strcasestr(a1, "xposedbridge") || strcasestr(a1, ".xposed.") || strcasestr(a1, "xposed_art");
  else
    v1 = 0;
  return v1;
}

也是xposed的一些特征(为啥一直针对xposed)

接下来看sub_3A5F8中的scan_proc_libs函数

int __fastcall scan_proc_libs(int *a1)
{
  size_t v1; // r0
  char v3; // [sp+Fh] [bp+Fh] BYREF
  int v4; // [sp+10h] [bp+10h]
  FILE *v5; // [sp+14h] [bp+14h]
  const char *v6; // [sp+18h] [bp+18h]
  char v7[256]; // [sp+1Ch] [bp+1Ch] BYREF
  char v8[1024]; // [sp+11Ch] [bp+11Ch] BYREF
  _BYTE v9[1024]; // [sp+51Ch] [bp+51Ch] BYREF

  v4 = *a1;
  free(a1);
  sub_5B390((int)v7, 256, "/proc/%d/maps", v4);
  v5 = fopen(v7, "r");
  if ( v5 )
  {
    memset(v8, 0, sizeof(v8));
    memset(v9, 0, sizeof(v9));
    sub_39E0C(v4, (int)v9);
    v3 = 0;
    while ( fscanf(v5, "%*p-%*p %*c%*c%c%*c %*s %*s %*d%1023[^\n]", &v3, v8) == 2 )
    {
      readLine((int)v8);
      if ( v3 == 120 )
      {
        v6 = "/system/bin/app_process";
        v1 = strlen("/system/bin/app_process");
        if ( !strncmp(v8, v6, v1) )
        {
          if ( sub_3A30C(v8) == 1 )
            mykill(v4, 9);
        }
        else if ( so_filter(v8, v9) && find_hook_feature(v8) == 1 )
        {
          mykill(v4, 9);
        }
      }
    }
    fclose(v5);
  }
  return 0;
}

和之前的差不多,也是读maps,看看find_hook_feature有啥宝贝

find_hook_feature

int __fastcall find_hook_feature(const char *a1)
{
   // 此处省略无关代码

  v6 = 0;
  if ( strcasestr(a1, "substrate") || strcasestr(a1, "frida") )
  {
    v6 = 1;
    v1 = 1;
  }
  else
  {
    v10 = "substrate";
    v11 = "adbi_hook";
    v12 = "ALLINONEs_arthook";
    v13 = "ddi_hook";
    v14 = "dexposed";
    v15 = "frida";
    v16 = "MSHookFunction";
    v17 = "MSFindSymbol";
    v18 = "MSCloseFunction";
    v19 = "hook_postcall";
    v20 = "hook_precall";
    v21 = "dalvik_java_method_hook";
    v22 = "art_java_method_hook";
    v23 = "art_quick_call_entrypoint";
    v24 = "artQuickToDispatcher";
    v25 = "dexstuff_defineclass";
    v26 = "dexstuff_loaddex";
    v27 = "dexstuff_resolv_dvm";
    v28 = "DexposedBridge";
    v29 = "dexposedIsHooked";
    v30 = "dexposedCallHandler";
    v31 = "frida_agent_main";
    v32 = "MSHookFunction";
    v33 = "substrate";
    v34 = "MSFindSymbol";
    v35 = "substrate";
    v36 = "MSCloseFunction";
    v37 = "substrate";
    v38 = "hook_postcall";
    v39 = "adbi_hook";
    v40 = "hook_precall";
    v41 = "adbi_hook";
    v42 = "dalvik_java_method_hook";
    v43 = "ALLINONEs_arthook";
    v44 = "art_java_method_hook";
    v45 = "ALLINONEs_arthook";
    v46 = "art_quick_call_entrypoint";
    v47 = "ALLINONEs_arthook";
    v48 = "artQuickToDispatcher";
    v49 = "ALLINONEs_arthook";
    v50 = "dexstuff_defineclass";
    v51 = "ddi_hook";
    v52 = "dexstuff_loaddex";
    v53 = "ddi_hook";
    v54 = "dexstuff_resolv_dvm";
    v55 = "ddi_hook";
    v56 = "DexposedBridge";
    v57 = "dexposed";
    v58 = "dexposedIsHooked";
    v59 = "dexposed";
    v60 = "dexposedCallHandler";
    v61 = "dexposed";
    v62 = "frida_agent_main";
    v63 = "frida";
    ptr = 0;
    v5 = 0;
    if ( !sub_39930(a1, &ptr, &v5) )
    {
      v7 = 0;
      i = 0;
      while ( !v6 && v7 <= 0xF )
      {
        for ( i = 0; i < v5; ++i )
        {
          if ( !strcmp((const char *)*(&v64 + 2 * v7 - 33), *((const char **)ptr + 3 * i)) )
          {
            v6 = 1;
            break;
          }
        }
        ++v7;
      }
      if ( ptr )
      {
        for ( j = 0; j < v5; ++j )
          free(*((void **)ptr + 3 * j));
        free(ptr);
      }
    }
    v1 = v6;
  }
  return v1;
}

好家伙,还是列了不少特征的,但是不顶用啊,谁用frida-server不改个名字?

还有个so_filter暂时不知道干啥的

int __fastcall so_filter(const char *a1, const char *a2)
{
  int v2; // r3
  size_t v3; // r0
  size_t v4; // r0
  size_t v5; // r0
  size_t v6; // r0
  size_t v7; // r0
  size_t v8; // r0
  size_t v9; // r0
  size_t v10; // r0
  size_t v11; // r0
  size_t v12; // r0

  if ( !*a1 || !a1 || !a2 )
    return 0;
  v3 = strlen("/system/");
  if ( !strncmp(a1, "/system/", v3) )
    goto LABEL_16;
  v4 = strlen("/dev/");
  if ( !strncmp(a1, "/dev/", v4) )
    goto LABEL_16;
  v5 = strlen("/data/dalvik-cache/");
  if ( !strncmp(a1, "/data/dalvik-cache/", v5)
    || (v6 = strlen("/cache/dalvik-cache/"), !strncmp(a1, "/cache/dalvik-cache/", v6))
    || (v7 = strlen("/vendor/"), !strncmp(a1, "/vendor/", v7))
    || (v8 = strlen("[vector]"), !strncmp(a1, "[vector]", v8))
    || (v9 = strlen("[vectors]"), !strncmp(a1, "[vectors]", v9))
    || (v10 = strlen("[sigpage]"), !strncmp(a1, "[sigpage]", v10))
    || (v11 = strlen("[vdso]"), !strncmp(a1, "[vdso]", v11))
    || (v12 = strlen("[vsyscall]"), !strncmp(a1, "[vsyscall]", v12))
    || strstr(a1, a2) )
  {
LABEL_16:
    v2 = 0;
  }
  else
  {
    v2 = 1;
  }
  return v2;
}

看完了,回到protect_self看下一个函数:sub_38D90

sub_38D90

int sub_38D90()
{
  int result; // r0
  pthread_t v1; // [sp+4h] [bp+4h] BYREF
  int i; // [sp+8h] [bp+8h]

  for ( i = 30; ; --i )
  {
    result = pthread_create(&v1, 0, (void *(*)(void *))scan_gettimeofday, 0);
    if ( !result || i <= 0 )
      break;
    sleep(1u);
  }
  return result;
}

看到scan_gettimeofday应该是和时间相关的反调了,网上也能找到相关的内容

scan_gettimeofday

void __noreturn scan_gettimeofday()
{
  __pid_t v0; // r0
  double v1; // [sp+10h] [bp+8h]
  double v2; // [sp+18h] [bp+10h]
  int v3; // [sp+24h] [bp+1Ch] BYREF
  struct timeval v4; // [sp+2Ch] [bp+24h] BYREF
  int v5; // [sp+34h] [bp+2Ch] BYREF
  struct timezone v6; // [sp+3Ch] [bp+34h] BYREF
  int v7[2]; // [sp+44h] [bp+3Ch] BYREF
  int v8; // [sp+4Ch] [bp+44h] BYREF
  int v9; // [sp+50h] [bp+48h]

  v7[0] = 0;
  v7[1] = 0;
  v8 = 0;
  v9 = 0;
  while ( 1 )
  {
    sub_38BB0(78, &v3, &v5, 0, 0);
    gettimeofday(&v4, &v6);
    if ( v4.tv_sec == v3 || sub_38B94(v4.tv_sec - v3) <= 1 )
      v1 = (double)v4.tv_sec / (double)v3;
    else
      v1 = 0.0;
    sub_38BB0(263, 1, v7, 0, 0);
    clock_gettime(1, (struct timespec *)&v8);
    if ( v8 == v7[0] || sub_38B94(v8 - v7[0]) <= 1 )
      v2 = (double)v8 / (double)v7[0];
    else
      v2 = 0.0;
    if ( v1 > 1.001 || v1 < 0.99 || v2 > 1.001 || v2 < 0.99 )
    {
      v0 = getpid();
      mykill(v0, 9);
    }
    sleep(2u);
  }
}

这边可以看到它不仅同时使用了gettimeofday和clock_gettime这两个函数,他还调用了两次sub_38BB0这个函数分别传参78和263,即系统调用表中的__NR_gettimeofday__NR_clock_gettime,以防止有人hook libc

再往下看sub_3B54C,这个暂时不知道要干啥了,貌似是hook了dlopen,原因还没仔细看

int __fastcall sub_3B54C(int a1)
{
  void *(**v3)(const char *, int); // [sp+Ch] [bp+Ch]

  dword_84A14 = a1;
  v3 = &dlopen;
  sub_3B190();
  if ( dword_84A04 && (dword_84A04 > (unsigned int)&dlopen || dword_84A08 < (unsigned int)&dlopen) && dword_849E0 )
    v3 = (void *(**)(const char *, int))dword_849E0;
  if ( a1 > 23 && dword_849E8 )
    v3 = (void *(**)(const char *, int))dword_849E8;
  return hookFunEx(v3, sub_3B4C4, &dword_84A18);
}

接下来看这个protect_polling函数,往里跟发现两个主要函数check_process_stopped和scan_process_threads

check_process_stopped

int __fastcall check_process_stopped(int a1)
{
    //省略
  v7 = "/proc/%ld/status";
  sub_5B390((int)v15, 256, "/proc/%ld/status", a1);
  v6 = 0;
  v8 = fopen(v15, "r");
  if ( !v8 )
    return v6;
  v5 = 0;
  v9 = "TracerPid:";
  v10 = "State:";
  v11 = "T (stopped)";
  v12 = "(zombie)";
  v13 = "t (tracing stop)";
  while ( fgets(v16, 1024, v8) )
  {
    v2 = strlen(v10);
    if ( !strncmp(v16, v10, v2) && (strcasestr(v16, v11) || strcasestr(v16, v12) || strcasestr(v16, v13)) )
    {
      while ( fgets(v17, 1024, v8) )
      {
        v3 = strlen(v9);
        if ( !strncmp(v17, v9, v3) )
        {
          sscanf(v17, "%s %d", &v14, &v5);
          break;
        }
      }
      if ( v5 && getpid() != v5 )
        v6 = 1;
      break;
    }
  }
  fclose(v8);
  return v6;
}

经典反调操作,读status看自己是否被ptrace

看看scan_process_threads

int __fastcall scan_process_threads(int a1)
{
  int v3; // [sp+4h] [bp+4h]
  int v4; // [sp+8h] [bp+8h]
  DIR *v5; // [sp+Ch] [bp+Ch]
  struct dirent *v6; // [sp+10h] [bp+10h]
  const char *v7; // [sp+14h] [bp+14h]
  int v8; // [sp+18h] [bp+18h]
  char v9[256]; // [sp+1Ch] [bp+1Ch] BYREF

  v3 = a1;
  sub_5B3D8(v9, "/proc/%ld/task/", a1);
  v5 = opendir(v9);
  if ( !v5 )
    return 0;
  v4 = 0;
  while ( 1 )
  {
    v6 = readdir(v5);
    if ( !v6 )
      break;
    v7 = &v6->d_name[8];
    if ( v6 != (struct dirent *)-19 )
    {
      if ( strcmp(v7, ".") )
      {
        if ( strcmp(v7, "..") )
        {
          v8 = atol(&v6->d_name[8]);
          if ( v3 != v8 )
          {
            if ( sub_3C41C(v3, v8) )
            {
              v4 = 1;
              break;
            }
          }
        }
      }
    }
  }
  closedir(v5);
  return v4;
}

这个不清楚读task有啥用,下一个

sub_38F8C又是检查手机型号是三星或者华为的,原因不明再下一个,anti_thread_of_process_debug和protect_polling是同一个,再下一个。。。好像很多都是重复了

再往下看大概就是hook了ptrace函数替换成自己的,然后调用ptrace将自己进行ptrace就结束了。

结语

  总体来说这个样本还算比较适合入门的,很多反调方法网上都有提及,可以用作学习材料。


文章作者: 大A
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 大A !
评论
  目录