整合非常规hook框架

安卓逆向 fakelocation

Posted by BC on May 7, 2019

前言

2019.7.16 重置最终版
前后断断续续2个月摸鱼时间,终于有点拿得出手的产品了。

笔者最近玩一个山寨手游 使用了Fake Location作为模拟定位软件,对其中反检测功能的实现原理颇为感兴趣
反检测在java层和native层都做了常见API的hook,该软件也没有使用xposed的迹象,想必是注入dex和so的了。
但是该游戏有ptrace保护。年少的笔者非常想知道他是怎么绕过ptrace保护注入进程实现hook功能的。
这项技术叫做zygote注入。其优势在于不借助常规hook框架(Xposed),就可以实现java层的hook并load so。
并且会对逆向分析造成更多的麻烦,因为没有使用标准Xposed API。

分析SuperSU日志

第一步 先康康他用root权限干了些什么  
inject -P zygote -l initzygote.so
向zygote进程中注入initzygote.so文件,解决了目标进程被ptrace保护的问题。

linux的zygote和fork机制:
zygote 孵化器是 Android 应用进程的模板,通过其 fork 出来。
在fork()时,子进程和父进程暂时共享父进程的内存空间,通过COW机制管理。
而我们在fork前注入zygote的so和dex,会存在于之后的每一个新进程中实现hook功能。

SuperSU日志

分析initzygote.so

int init(void)
{
  int v0; // r0
  JNIEnv *v1; // r4
  int v2; // r0
  bool v3; // zf
  int v4; // ST24_4
  int v5; // ST10_4
  int v6; // r5
  int v7; // r9
  int v8; // ST18_4
  int v9; // r8
  int v10; // r0
  int v11; // ST1C_4
  int v12; // r6
  int v13; // r6
  int v14; // ST14_4
  int v15; // r8
  int v16; // r5
  int v17; // ST0C_4
  int result; // r0
  signed int v19; // r0
  const char *v20; // r2
  char s; // [sp+28h] [bp-220h]
  int v22; // [sp+228h] [bp-20h]

  v0 = _android_log_print(4, "LINJECT.native", "InitApp is Executing!!");
  v1 = (JNIEnv *)j_getJniEnv(v0);
  if ( !v1 )
  {
    result = _stack_chk_guard - v22;
    if ( _stack_chk_guard != v22 )
      return result;
    v19 = 4;
    v20 = "jni_env is NULL!!";
    goto LABEL_11;
  }
  v2 = j_isSignQualified();
  v3 = v2 == -2;
  if ( v2 != -2 )
    v3 = v2 == 0;
  if ( v3 )
  {
    __android_log_print(4, "LINJECT.native", "jni_env is %p", v1);
    v4 = ((int (__fastcall *)(JNIEnv *, const char *))(*v1)->NewStringUTF)(v1, "/data/fakeloc/zygote_dex");//此处在实现时的小坑:optimized directory 必须属于当前用户,所以 chown 0 /data/shithack/zygote_dex
    v5 = ((int (__fastcall *)(JNIEnv *, char *))(*v1)->NewStringUTF)(v1, apkpath[0]);
    v6 = ((int (__fastcall *)(JNIEnv *, const char *))(*v1)->FindClass)(v1, "dalvik/system/DexClassLoader");
    snprintf(
      &s,
      0x200u,
      "(%s%s%s%s)V",
      "Ljava/lang/String;",
      "Ljava/lang/String;",
      "Ljava/lang/String;",
      "Ljava/lang/ClassLoader;");
    v7 = ((int (__fastcall *)(JNIEnv *, int, const char *, char *))(*v1)->GetMethodID)(v1, v6, "<init>", &s);
    snprintf(&s, 0x200u, "(%s)%s", "Ljava/lang/String;", "Ljava/lang/Class;");
    v8 = v6;
    v9 = ((int (__fastcall *)(JNIEnv *, int, const char *, char *))(*v1)->GetMethodID)(v1, v6, "loadClass", &s);
    v10 = j_getSystemClassLoader(v1);
    v11 = v10;
    v12 = v10;
    __android_log_print(4, "LINJECT.native", "Oat2dex...");
    v13 = _JNIEnv::NewObject(v1, v6, v7, v5, v4, 0, v12);
    __android_log_print(4, "LINJECT.native", "Oat2dex OK.");
    v14 = ((int (__fastcall *)(JNIEnv *, const char *))(*v1)->NewStringUTF)(
            v1,
            "com.lerist.inject.fakelocation.InjectDex");
    v15 = _JNIEnv::CallObjectMethod(v1, v13, v9, v14);
    __android_log_print(4, "LINJECT.native", "entry_class:%p", v15);
    v16 = ((int (__fastcall *)(JNIEnv *, int, const char *, const char *))(*v1)->GetStaticMethodID)(
            v1,
            v15,
            "initZygote",
            "(Ljava/lang/Object;)[Ljava/lang/Object;");
    __android_log_print(4, "LINJECT.native", "Invoke method...");
    v17 = _JNIEnv::CallStaticObjectMethod(v1, v15, v16, 0);
    __android_log_print(4, "LINJECT.native", "InitApp is finished");
    return _stack_chk_guard - v22;
  }
  result = _stack_chk_guard - v22;
  if ( _stack_chk_guard == v22 )
  {
    v19 = 6;
    v20 = "Illegal application!";
LABEL_11:
    result = j___android_log_print(v19, "LINJECT.native", v20);
  }
  return result;
}

main函数里 dexclasslaoder动态加载插件 反射执行了com.lerist.inject.fakelocation.InjectDex.initZygote方法

  public static Object[] initZygote(Object obj) {
        StringBuilder sb = new StringBuilder();
        sb.append("initZygote.");
        sb.append(obj);
        String sb2 = sb.toString();
        String str = TAG;
        Log.d(str, sb2);
        StringBuilder sb3 = new StringBuilder();
        sb3.append("");
        sb3.append(Build.CPU_ABI);
        LHooker.m251a(sb3.toString().contains("64") ? "/data/fakeloc/liblhooker64.so" : "/data/fakeloc/liblhooker.so");
        C0021a.hook(null);//有坑 传入ClassLoader为null 因为此处还无法getSystemClassLoader(),猜测是还没有走到ZygoteInit 所以没有Classloader
        StringBuilder sb4 = new StringBuilder();
        sb4.append("initZygote finish.");
        sb4.append(LHooker.f65a);
        Log.d(str, sb4.toString());
        return null;
    }

在这里System.load加载了liblhooker.so 为hook库
com.lerist.lib.lhooker.LHooker类中有多个hook相关JNI方法

    public static native java.lang.Object findMethodNative(java.lang.Class r1, java.lang.String r2, java.lang.String r3);

    public static native java.lang.Object[] getKeys(byte[] r1, java.lang.String r2);

    private static native boolean hookMethodNative(java.lang.Object r1, java.lang.reflect.Method r2, java.lang.reflect.Method r3, java.lang.reflect.Method r4);

    public static native int init(int r1);

    public static native void resumeAll(long r1);

    public static native long suspendAll();

第一步小结

1. Fake软件使用了非xposed的hook库
2. 通过zygote注入loader So 反射加载dex,在插件dex里加载Hook用so和java hook代码
3. 这样做的好处是可以绕过常见xposed检测,又可以对APP进行自定义加固(xposed真的不行)
4. 根据关键词发现,fake使用的hook框架为YAHFA+ELFHOOKER,笔者将继续学习模仿,整合一个自己的APP框架。

动手模仿-Java层Hook

中间过程艰辛。Whale和Epic都用不来,只能老老实实模仿YAHFA。 期间走了不少弯路,还是要摸着前人的石头过河。

问题1 dlopen和dlsym 获取不到libart.so

这个是7.0以上so文件安全性 关于namespace的问题
使用了Nougat_dlfunctions 的fake_dlfcn也没有用
只能从initzygote.so里抠代码出来 居然可以 奇怪
分析代码 关键点 ```c    while ( !handle && i <= 1 )
{
    dlerror();
    handle = dlopen(0, 1);
    error = dlerror();
    if ( error )
        LOGD( "BCINJECT.native failed to load %s: %s", paths[i], error);
    if ( handle )
        LOGD( "BCINJECT.native Android runtime loaded from %s", paths[i]);
    else
        ++i;
} ```
乍一看非常奇怪 怎么第一个参数可以是NULL呢 随便google一下没有找到答案
于是开始dlopen分析源码 [参考dlopen分析文章](https://blog.csdn.net/SweeNeil/article/details/83744843)   [参考函数文档](https://www.xuebuyuan.com/1912632.html)

If file is a null pointer, dlopen() shall return a global symbol table handle for the currently running process image. This symbol table handle shall provide access to the symbols from an ordered set of executable object files consisting of the original program image file, any executable object files loaded at program start-up as specified by that process file (for example, shared libraries), and the set of executable object files loaded using dlopen() operations with the RTLD_GLOBAL flag. As the latter set of executable object files can change during execution, the set of symbols made available by this symbol table handle can also change dynamically.

第一个参数fileName为空时,handle指向当前进程镜像的全局符号表。
handle类似迭代器指针,每调用一次指向下一个模块。
在Android中,符号表首部的应该就是jvm相关 libart或libdvm。
所以我们可以获取到JavaVM 从而获取到JNIEnv*

问题2 插件目录归属用户问题

插件加载的目录归属用户 需要与目标进程的用户相同
如果是zygote注入 归属root用户 system_server 归属system用户 以此类推

YAHFA代码编写

可惜YAHFA对于XPosed的没有什么兼容(有其他魔改框架 BudHook等)
示例如下 ```java public class HookMain {
public static void hookAll(final Object context) {
    Log.e(TAG, "enter hookAll");
    init();
    doHookItemDefault(HookMain.class.getClassLoader(), Hook_ActivityThread_handleBindApplication.class.getName(), null);
} }

public class Hook_ActivityThread_handleBindApplication { public static String className = “android.app.ActivityThread”; public static String methodName = “handleBindApplication”; public static String methodSig = “(Landroid/app/ActivityThread$AppBindData;)V”;

public static void hook(Object thiz, Object data) {
    Log.e(TAG, "in handleBindApplication " + String.valueOf(data));
    try {

        Class AppBindData = Class.forName("android.app.ActivityThread$AppBindData", false,
                thiz.getClass().getClassLoader());
        ApplicationInfo applicationInfo = (ApplicationInfo) ReflectUtil.getField(data, AppBindData, "appInfo");
        String packageName = applicationInfo.packageName.equals("android") ? "system" : applicationInfo.packageName;
        String processName = (String) ReflectUtil.getField(data, AppBindData, "processName");
        Log.e(TAG,packageName +" ,"+processName);

        backup(thiz, data);
    } catch (Exception e) {
        Log.e(TAG, Log.getStackTraceString(e));
    }
    return;
}

public static void backup(Object thiz, Object data) {
    Log.w(TAG, "handleBindApplication should not be here");
    return;
} } } ``` 这样就可以成功注入每一个进程,hook到handleBindApplication,拿到第一个Classloader,其他问题随之迎刃而解,YAHFA方便的hook配置甚至可以插件化,暂且不表。

Native层Hook-集成ELFHooker

为什么不用InlineHook呢,因为他不支持ARM64,有一些落伍了。ELFHooker顺利接班。
fake的加载插件so方法,同Xposed插件加载so的方法(https://www.jianshu.com/p/614721f7a2c8)

总结

至此我们已经成功模仿了fake location实现的java层和native层hook
可以做出一个不依赖Xposed框架的hook工具,可以绕开常见检测(也可以修改epic特征 美团就会检测epic)
如果要整合进自己的产品 需要更多安全保护
之后的计划
1.ollvm环境搭建
2.空白混淆加强修改(方法名加长)
3.检测自身是否被hook 反调试

参考

1.Lody’s elfHook - 主要解决ARM64 HOOK
2.YAHFA -Yet Another Hook Framework for ART 3.Linux中的fork机制
4.Android中的so注入(inject)和挂钩(hook) - For both x86 and arm
5.dlopen分析文章
6.dlopen函数文档