xposed 05 - so函数hook

本文讨论如何hook目标apk中的 so 中的函数。

实现 so hook 的一个主要思想,就是将目标apk当成是我们自己编写的就行,就像开发中hook系统调用一样。有了这个思维,思路会宽广许多。

so 打开流程

在 Android 中加载一个so会使用到 System.loadLibrary 方法。

在 native 中,一般加载 so 是使用的 dlopen 方法。

由于 System.loadLibrary 最终也会调用到 dlopen 方法,所以我们先看 dlopen 方法流程。

dlopen 会调用到 do_dlopen 里面去:

yaml 复制代码
1937  void* do_dlopen(const char* name, int flags,
1938                  const android_dlextinfo* extinfo,
1939                  const void* caller_addr) {
1945    ...
2011  
2012    ProtectedDataGuard guard;
2013    soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
2014    loading_trace.End();
2015  
2016    if (si != nullptr) {
2017      void* handle = si->to_handle();
2018      ...
2021      si->call_constructors();
2022      ...
2026      return handle;
2027    }
2028  
2029    return nullptr;
2030  }

核心逻辑就是使用 find_library 方法获取到 so 相关信息,然后调用其 call_constructors 方法。

call_constructors 里面会调用 init 与 init_array 相关代码:

javascript 复制代码
388  void soinfo::call_constructors() {
389    ...
417  
418    // DT_INIT should be called before DT_INIT_ARRAY if both are present.
419    call_function("DT_INIT", init_func_, get_realpath());
420    call_array("DT_INIT_ARRAY", init_array_, init_array_count_, false, get_realpath());
421  
422    ...
425  }

这就是为啥 init 与 init_array 会在so加载后就执行的原因了。

我们再分析 System.loadLibrary 流程,它会调用到 LoadNativeLibrary 方法:

ini 复制代码
796  bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
797                                    const std::string& path,
798                                    jobject class_loader,
799                                    jstring library_path,
800                                    std::string* error_msg) {
801    ...
866    void* handle = android::OpenNativeLibrary(env,
867                                              runtime_->GetTargetSdkVersion(),
868                                              path_str,
869                                              class_loader,
870                                              library_path,
871                                              &needs_native_bridge,
872                                              error_msg);
873  
874    ...
915    bool was_successful = false;
916    void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
917    if (sym == nullptr) {
918      VLOG(jni) << "[No JNI_OnLoad found in "" << path << ""]";
919      was_successful = true;
920    } else {
921      // Call JNI_OnLoad.  We have to override the current class
922      // loader, which will always be "null" since the stuff at the
923      // top of the stack is around Runtime.loadLibrary().  (See
924      // the comments in the JNI FindClass function.)
925      ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
926      self->SetClassLoaderOverride(class_loader);
927  
928      VLOG(jni) << "[Calling JNI_OnLoad in "" << path << ""]";
929      typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
930      JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
931      int version = (*jni_on_load)(this, nullptr);
932  
933      ...
959    return was_successful;
960  }

函数分为两个片段,第一个是 OpenNativeLibrary,它会调用到 dlopen 方法,也就是我们上面分析的过程。

第二个片段,会先找到 JNI_OnLoad 符号,然后调用这个函数,这就是为啥 JNI_OnLoad 会紧跟在 init 与 init_array 后面执行的原因。

hook 函数

有了上面的基础,我们再来介绍一下如何做 so 的 hook。arm 的hook会比较麻烦,但是好在有 github,我们可以使用开源的一些库做到开箱即用。

hook 分两种,一种是 plt hook,一种是 inline hook。这两种先简单介绍,后面我们开篇 elf 的时候在细说。简单来说,plt hook 只能 hook plt 表中存在的函数。inline hook 可以 hook 几乎所有的函数。

看一个例子来理解为啥要有这两种 hook:

arduino 复制代码
extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativehooktarget_MainActivity_hookMe(JNIEnv
                                                      *env,
                                                      jobject thiz
) {
    if (test_hook("hookMe")) {
        __android_log_print(4, "hook_so", "hookMe success");
    } else {
        __android_log_print(4, "hook_so", "hookMe failed");
    }

}

在同一个文件里面写这两个方法:

Java_com_example_nativehooktarget_MainActivity_hookMe
test_hook

编译后,看其汇编:

csharp 复制代码
95c:   91181800        add     x0, x0, #0x606
    if (test_hook("hookMe")) {
 960:   97ffffa8        bl      800 <_Z9test_hookPKc>
 964:   36000100        tbz     w0, #0, 984 <Java_com_example_nativehooktarget_MainActivity_hookMe+0x40>

看到对 test_hook 函数的调用直接使用了绝对地址 800,这样的函数,我们是没有办法使用 plt hook 的,因为 plt 表里面根本就没有这个函数的符号。

本来是想使用 inline hook 框架,字节开源的,虽然肯定比内部的版本号低了些,但是够用了:

github.com/bytedance/a...

但是发现它不是纯 native 的,还需要在 java 里面初始化,由于 classLoader 的问题,用起来会非常的麻烦。虽然说 xposed 将模块的代码注入了app进程,但是它们还是使用了不同的 classLoader,so的加载也是与 classLoader有关,就会遇见各种奇葩问题。

所以还是使用老版的 sandhook,将代码 copy 进来,在 cmakelists.txt 里面配置,然后编写hook代码:

arduino 复制代码
//
// Created by root on 12/9/23.
//

#include "jni.h"
#include <cstring>
#include <android/log.h>
#include "sandhook_native.h"

void *orig = nullptr;

typedef char *(*type_t)(char *, char *);

char* proxy(char *str1, char *str2) {
    // invoke origin method
    char * result = ((type_t) orig)(str1, str2);
    if (strcmp(str2, "test_hook") == 0) {
        return str1;
    }
    __android_log_print(4, "hook_so", "proxy origin result %s", result);
    return result;
}

void do_hook_test_hook() {
    const char *libc_path = "/system/lib64/libc.so";
    orig = SandInlineHookSym(libc_path, "strstr", reinterpret_cast<void *>(&proxy));

    __android_log_print(4, "hook_so", "hook result %p", orig);
}

extern "C" jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    do_hook_test_hook();
    return JNI_VERSION_1_6;
}

这里我们选择 hook libc 中的 strstr 函数,由于 sandhook 的限制,我们只能将hook时机放在JNI_OnLoad 处。

编译好模块的 so 后,将其 push 到目标 app 的 lib下:

bash 复制代码
/data/app/com.example.nativehooktarget-prsFV1IVkibTNbuAUhlI-w==/lib/arm64/libsohook.so

这样,我们就可以在模块里面加载这个 so,然后让其加载后自动 hook:

typescript 复制代码
XposedHelpers.findAndHookMethod(
                "java.lang.Runtime",
                loadPackageParam.classLoader,
                "loadLibrary0",
                ClassLoader.class,
                String.class,
                new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                        super.beforeHookedMethod(param);
                        XposedBridge.invokeOriginalMethod(param.method, param.thisObject, new Object[] {
                                param.args[0], "sohook"
                        });
                        Log.e("hook_so", "beforeHookedMethod");
                    }
                });

注意,这里需要使用app的 classLoader,param.thisObject 就是 app 的classLoader,且为了避免循环调用,所以需要使用 invokeOriginalMethod 来调用 loadLibrary0 方法。

这里我们在加载目标 so 之前,先加载我们的 so,然后 hook 目标so中的方法。

最后hook结果如下:

css 复制代码
I  hook result 0x7473fc2000
E  beforeHookedMethod
I  proxy origin result (null)
I  proxy origin result (null)
I  junk code
I  junk code
I  _init success
I  junk code
I  junk code
I  my_init_array1 success
I  proxy origin result (null)
I  proxy origin result (null)
I  proxy origin result (null)
I  proxy origin result (null)
I  proxy origin result (null)
I  proxy origin result (null)
I  junk code
I  junk code
I  hookMe success

可以看到,对 so 中的hook都成功了。

so 中函数主动调用

也是使用 sandhook 中的 api:

ini 复制代码
void *libnativebase = SandGetModuleBase("libnative-lib.so");

在加上函数的偏移地址即可:

ini 复制代码
unsigned long tmpaddr = (unsigned long) libnativebase + 0xf67c;
void *testhookaddr = reinterpret_cast<void *>(tmpaddr);
testhookfunction = reinterpret_cast<testhook>(testhookaddr);
LOGD("libnative-lib.so base:%p,testfuncaddr:%p",libnativebase,(void*)tmpaddr);

不过,用起来还是感觉很蛋疼,而且这些代码都还没考虑 32与64 的区别。

总的来说,这个可以自己玩玩,总感觉差点什么东西,比如,我想使用地址来hook,但是就必须要等到so加载之后才行,这样就hook不到初始化时间,就有点烦。或许将 shadowhook 改一下,用起来会更舒服。

相关推荐
haodanzj5 分钟前
在uniapp中封装请求接口 (带刷新token)
前端·javascript·uni-app
Suckerbin19 分钟前
黑客基础之HTML
前端·html
空&白25 分钟前
uniapp h5地址前端重定向跳转
前端·uni-app
NiNg_1_2341 小时前
前端CSS3 渐变详解
前端·css·html
不法1 小时前
uniapp 跨域前端代理
前端·uni-app
黄景圣2 小时前
CURD低代码程序设计
前端·vue.js·后端
lin-lins2 小时前
Vue 模板编译原理
前端·javascript·vue.js
API小知识2 小时前
Python中哪个框架最适合做API?
前端·后端·api
堕落年代2 小时前
在uniapp当中隐藏掉默认tabbar并且使用自己的tabbar
前端·javascript·uni-app
不惑_2 小时前
Redis:发布(pub)与订阅(sub)实战
前端·redis·bootstrap