ROOT与HOOK框架
提动态分析,HOOK/Instrument是最常见的手段,尽管存在多种不root也能hook的方案,但它不算主流,因此本文会首先聚焦当前主流的root和hook方案
远古

还记得俺在初中时拥有的第一台安卓手机,那是可以直接下载一个app获取到root权限的,料想当时系统权限限制应该极弱,存在普遍的提权方式,现在俺也不打算考古了,从安卓系统权限来猜猜吧!安卓基于Linux,存在两种权限模型:
- DAC:即UGO拥有的RWX权限,提供用户/组级别的粗粒度限制
- MAC:即SELinux,提供细粒度(进程/文件/端口等)的权限控制,DAC里root的权限也被限制
在最开始android是没使用MAC的,因此提权会更简单,搞个有SUID的应用就行了,而在支持MAC后还需要再配置/修改SELinux策略
Xposed开发
这一块不再常用就先忽略,但是"万恶之源"XPosed及其后继在现在还是主流,因此需要好好研究一番,先从用户视角体验,还是用Android studio,新建一个项目
1.添加依赖,新版的gradle在settings.gradle的dependencyResolutionManagement->repositories里添加jcenter()仓库
2.在build.gradle(app)的dependencies里添加compileOnly 'de.robv.android.xposed:api:82'
3.创建hook类,例如:
package com.example.lsposed_module_dd;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
public class DisableLonglink implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
XposedBridge.log("Loaded app: " + lpparam.packageName);
if(!lpparam.packageName.contains("xunmeng")){
XposedBridge.log("skip...");
return;
}
findAndHookMethod("com.xunmeng.basiccomponent.titan.api.helper.ApiNetChannelSelector", lpparam.classLoader, "canUseLongLink", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
boolean res = (boolean) param.getResult();
XposedBridge.log("call result:"+param.args[0]+res);
param.setResult(false);
}
});
}
}
4.在assets/xposed_init文件中添加hook入口类:
com.example.lsposed_module_dd.DisableLonglink
于是就能愉快的玩耍了...
注:它只提供Java层的hook哦~
xposed分析
现在咱从原理来看,翻开GitHub这个工具主要是由三部分组成的:
- XposedBridge:这是被注入到目标应用里的库文件
- Xposed:这是它能hook应用的核心部分,它修改了zygote来在应用启动前注入hook代码
- android_art: 不像openjdk等提供了丰富的插桩能力,ART虚拟机在这方面是严重受限的,它并没有提供运行时替换函数的能力,因此XPosed选择了修改源码的方式来提供最底层能力的支持,它对art的补丁就在这个项目下
现在从下往上分析吧~
android_art修补
它对art的修补分两部分,首先是dex2oat,这个工具是将dex预编译成了机器码,这个过程会有很多优化,例如方法内联,或者直接编码某些确定方法的入口,这会导致后续的hook失效,所以它先disable了这些特性;接着是最主要的,修改ArtMethod类,这是几乎所有安卓hook都会处理的类,该类表示Java的一个方法,每个Java方法在art里都有个对应的artmethod实例,它的结构一直在变化,这里随便贴个做说明:
class ArtMethod {
public:
GcRoot<mirror::Class> declaring_class_; // 指向该方法所属的 Class 对象
/**
* - kAccPublic, kAccPrivate, kAccProtected (可见性)
* - kAccStatic (是否为静态方法)
* - kAccFinal (是否为 final 方法)
* - kAccSynchronized (是否为同步方法)
* - kAccNative (是否为 Native 方法)
*/
uint32_t access_flags_; // 方法的访问标志,一个32位的 bitfield
uint32_t dex_code_item_offset_; // 指向DEX文件中 CodeItem 的偏移量
uint32_t dex_method_index_; // 方法在所属DEX文件的方法定义列表中的索引值,这是方法在 DEX 文件中的唯一ID,用于在运行时快速查找和链接
void* entry_point_from_jni_; // JNI (Java Native Interface) 调用的入口点
void* entry_point_from_quick_compiled_code_; // AOT (Ahead-Of-Time) 编译后机器码的入口点
//...
};
这里面在hook中最重要的是最后的entry_point_...,在无优化的情况下,做方法调用就是先找到方法的artmethod再根据属性跳到对应的入口点,而hook通常就是修改这些入口点的指向来实现的,xposed通过新增ArtMethod::EnableXposedHook来修改它的:
void ArtMethod::EnableXposedHook(ScopedObjectAccess& soa, jobject additional_info) {
if (UNLIKELY(IsXposedHookedMethod())) { // 如果方法已经被标记为“已Hook”,则直接返回,避免重复操作
return;
}
if (UNLIKELY(IsXposedOriginalMethod())) { // 如果方法是“原始方法备份”,这是一个异常情况,这在上一步就该返回了,到这说明有问题直接抛出异常
ThrowIllegalArgumentException("Cannot hook the method backup");
return;
}
auto* cl = Runtime::Current()->GetClassLinker(); // 获取 ClassLinker 和内存分配器,用于创建新的运行时方法对象
auto* linear_alloc = cl->GetAllocatorForClassLoader(GetClassLoader());
ArtMethod* backup_method = cl->CreateRuntimeMethod(linear_alloc); // 创建一个新的 ArtMethod 对象,作为原始方法的完整备份
backup_method->CopyFrom(this, cl->GetImagePointerSize()); // 将当前方法(this)的所有内容完整地复制到备份对象中
backup_method->SetAccessFlags(backup_method->GetAccessFlags() | kAccXposedOriginalMethod); // 给备份方法打上一个特殊的标记 kAccXposedOriginalMethod,以便将来识别
mirror::AbstractMethod* reflected_method; // 让Java层的XposedBridge能够通过标准反射API调用到原始方法
if (IsConstructor()) {
reflected_method = mirror::Constructor::CreateFromArtMethod(soa.Self(), backup_method);
} else {
reflected_method = mirror::Method::CreateFromArtMethod(soa.Self(), backup_method);
}
reflected_method->SetAccessible<false>(true); // 确保这个Java Method对象是可访问的
XposedHookInfo* hook_info = reinterpret_cast<XposedHookInfo*>(linear_alloc->Alloc(soa.Self(), sizeof(XposedHookInfo))); // 这个结构体将保存所有与本次Hook相关的信息
hook_info->reflected_method = soa.Vm()->AddGlobalRef(soa.Self(), reflected_method); // 将上一步创建的指向备份方法的Java Method对象存为JNI全局引用,防止被GC回收
hook_info->additional_info = soa.Env()->NewGlobalRef(additional_info); // 将Java层传来的 additional_info (包含回调)也存为JNI全局引用
hook_info->original_method = backup_method; // 保存指向C++层备份方法(ArtMethod)的指针
ScopedSuspendAll ssa(__FUNCTION__); // 暂停整个虚拟机,便于修改核心结构
// ...
jit::Jit* jit = art::Runtime::Current()->GetJit();
if (jit != nullptr) { // 通知JIT编译器,将原来与当前方法关联的已编译代码,转移给备份方法
jit->GetCodeCache()->MoveObsoleteMethod(this, backup_method);
}
SetEntryPointFromJniPtrSize(reinterpret_inpret_cast<uint8_t*>(hook_info), sizeof(void*)); // 将 entry_point_from_jni_指向hook_info结构体,下面会去掉native标志,所以它可以安全的用来存其它数据
SetEntryPointFromQuickCompiledCode(GetQuickProxyInvokeHandler()); // 将AOT编译代码的入口点,指向GetQuickProxyInvokeHandler
SetCodeItemOffset(0); // 清空DEX字节码偏移量,因为这个方法已经是一个代理,不再直接对应DEX中的代码
const uint32_t kRemoveFlags = kAccNative | kAccSynchronized; // 调整访问标志:移除 kAccNative, kAccSynchronized 等标志,因为这些将由Hook处理器来管理,添加 kAccXposedHookedMethod 标志,表明此方法已被Hook
SetAccessFlags((GetAccessFlags() & ~kRemoveFlags) | kAccXposedHookedMethod);
MutexLock mu(soa.Self(), *Locks::thread_list_lock_);
Runtime::Current()->GetThreadList()->ForEach(StackReplaceMethodAndInstallInstrumentation, this); // 遍历所有正在运行的线程,检查它们的调用栈。如果某个线程正停在此方法内部,需要对其栈进行修复,以确保它能正确地从新的Hook逻辑中返回
// Hook操作完成,函数结束时,scope退出会自动恢复虚拟机运行。
}
注意到这里,它把入口点改成了GetQuickProxyInvokeHandler,它是段汇编,最后会指向artQuickProxyInvokeHandler,它也被修改啦:
extern "C" uint64_t artQuickProxyInvokeHandler(
ArtMethod* proxy_method, mirror::Object* receiver, Thread* self, ArtMethod** sp)
SHARED_REQUIRES(Locks::mutator_lock_) {
const bool is_xposed = proxy_method->IsXposedHookedMethod(); // 之前设置过 kAccXposedHookedMethod
if (!is_xposed) {
DCHECK(proxy_method->IsRealProxyMethod()) << PrettyMethod(proxy_method);
}
const char* old_cause = self->StartAssertNoThreadSuspension("Proxy argument processing"); // 暂时禁止线程挂起,确保能安全地处理栈上的原始对象指针,防止GC移动它们
JNIEnvExt* env = self->GetJniEnv(); // 准备JNI环境,并创建一个新的局部引用作用域
ScopedObjectAccessUnchecked soa(env);
ScopedJniEnvLocalRefState env_state(env);
const bool is_static = proxy_method->IsStatic(); // 为 receiver(this) 创建一个JNI局部引用。从现在起,使用这个安全的JNI引用(rcvr_jobj)来代替原始的指针(receiver),以避免GC问题
jobject rcvr_jobj = is_static ? nullptr : soa.AddLocalReference<jobject>(receiver);
ArtMethod* non_proxy_method = proxy_method->GetInterfaceMethodIfProxy(sizeof(void*)); // 获取原始方法的Shorty签名,它描述了参数和返回值的类型
uint32_t shorty_len = 0;
const char* shorty = non_proxy_method->GetShorty(&shorty_len);
std::vector<jvalue> args;
BuildQuickArgumentVisitor local_ref_visitor(sp, is_static, shorty, shorty_len, &soa, &args); // 创建一个参数访问器(Visitor),它将根据Shorty签名遍历调用栈(sp),将原始的、类型不明的参数值,转换成一个包含jvalue的vector,对于对象类型的参数,它会自动创建JNI局部引用
local_ref_visitor.VisitArguments();
if (!is_static) { // 如果是实例方法,参数列表的第一个元素是'this',我们已经有rcvr_jobj了,所以从vector中移除
args.erase(args.begin());
}
self->EndAssertNoThreadSuspension(old_cause);// 恢复线程挂起,因为所有原始指针都已转换为安全的JNI引用
if (is_xposed) {
jmethodID proxy_methodid = soa.EncodeMethod(proxy_method); // 将C++的ArtMethod指针编码为Java层可识别的jmethodID
JValue result = InvokeXposedHandleHookedMethod(soa, shorty, rcvr_jobj, proxy_methodid, args);// 这个函数会从proxy_method中取出我们之前存入的hook_info,然后回调Java层的XposedBridge.handleHookedMethod,执行所有模块的hook逻辑。
local_ref_visitor.FixupReferences(); // 返回前,修复可能被GC移动过的对象引用
return result.GetJ(); // 将JValue格式的返回值转换为uint64_t并返回给调用者
} else {
//... 正常流程
}
}
上面可见,如果是代理方法就找到方法签名后调用InvokeXposedHandleHookedMethod,它的实现如下:
JValue InvokeXposedHandleHookedMethod(ScopedObjectAccessAlreadyRunnable& soa, const char* shorty,
jobject rcvr_jobj, jmethodID method,
std::vector<jvalue>& args) {
jobjectArray args_jobj = nullptr;// Java层的回调函数需要一个Object[]类型的参数数组
const JValue zero; // 用于在发生错误时返回一个空值。
if (args.size() > 0) {
args_jobj = soa.Env()->NewObjectArray(args.size(), WellKnownClasses::java_lang_Object, nullptr);// 创建一个Object类型的Java数组
for (size_t i = 0; i < args.size(); ++i) {// 遍历C++的参数向量,填充Java数组
if (shorty[i + 1] == 'L') {
jobject val = args.at(i).l;// 如果参数是对象类型('L'),直接放入数组
soa.Env()->SetObjectArrayElement(args_jobj, i, val);
} else {
JValue jv;
jv.SetJ(args.at(i).j);// 如果参数是基本类型(int, float等),则需要先“装箱”(Box)成对应的包装类(Integer, Float等)
mirror::Object* val = BoxPrimitive(Primitive::GetType(shorty[i + 1]), jv);
if (val == nullptr) {
CHECK(soa.Self()->IsExceptionPending());
return zero;
}
soa.Decode<mirror::ObjectArray<mirror::Object>* >(args_jobj)->Set<false>(i, val); // 将装箱后的对象放入数组
}
}
}
const XposedHookInfo* hook_info = soa.DecodeMethod(method)->GetXposedHookInfo(); // 从ArtMethod中取出我们之前在EnableXposedHook函数里存入的XposedHookInfo结构体指针 (GetEntryPointFromJniPtrSize)
// 准备调用Java层 XposedBridge.handleHookedMethod 的5个参数
// public static Object handleHookedMethod(Member method, int originalMethodId,
// Object additionalInfoObj, Object thisObject, Object[] args)
jvalue invocation_args[5];
invocation_args[0].l = hook_info->reflected_method; // 参数1: 指向原始方法的Java Method对象
invocation_args[1].i = 1; // 参数2: 原始方法ID (此处似乎为固定值或历史遗留)
invocation_args[2].l = hook_info->additional_info; // 参数3: 包含所有回调信息的对象
invocation_args[3].l = rcvr_jobj; // 参数4: this对象
invocation_args[4].l = args_jobj; // 参数5: 包含所有参数的Object[]数组
// 通过JNI调用Java层的静态方法 XposedBridge.handleHookedMethod。
// ArtMethod::xposed_callback_class 和 ArtMethod::xposed_callback_method 是预先缓存好的类和方法ID。
// 执行流从这里进入Java世界,执行所有模块的before/after回调逻辑
jobject result =
soa.Env()->CallStaticObjectMethodA(ArtMethod::xposed_callback_class,
ArtMethod::xposed_callback_method, // 下文会看到,叫handleHookedMethod
invocation_args);
if (UNLIKELY(soa.Self()->IsExceptionPending())) { // 处理Java层返回的结果,并返回给原始调用者
return zero; // 如果Java层抛出了异常,直接返回空值
} else {
if (shorty[0] == 'V' || (shorty[0] == 'L' && result == nullptr)) {// 如果原始方法返回类型是void('V'),或者返回的是null对象,直接返回空值
return zero;
}
mirror::Class* result_type = soa.DecodeMethod(method)->GetReturnType(true, pointer_size); // 如果Java层返回的是一个包装类对象(如Integer),而原始方法需要的是基本类型(int),则需要“拆箱”(Unbox)操作
mirror::Object* result_ref = soa.Decode<mirror::Object*>(result);
JValue result_unboxed;
if (!UnboxPrimitiveForResult(result_ref, result_type, &result_unboxed)) {
DCHECK(soa.Self()->IsExceptionPending()); // 拆箱失败
return zero;
}
return result_unboxed; // 返回最终处理好的、类型正确的值
}
}
至此,就分析完了art里java方法的hook安装和调用的全流程,从最后看到它是把原始方法id,装箱后的参数等一起打包成数组作为新的参数,去调用到了java层的ArtMethod::xposed_callback_class->ArtMethod::xposed_callback_method,接下来就是看它咯~
附:对art method的深入了解请参考ART的函数运行机制
Xposed
它会替换安卓自带的app_process,回到安卓启动过程,几乎所有Java应用都是从孵化器启动的,孵化器就是app_process出来的,它分为32和64两个版本,在xposed源码里根据sdk版本是否不大于21选择app_main.cpp/app_main2.cpp,咱直接从2开始看吧:
int main(int argc, char* const argv[])
{
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) { // 一个标准的Linux安全措施,禁止进程获取新的权限。
// ...
}
if (xposed::handleOptions(argc, argv)) { // 调用Xposed自己的选项处理器,比如读版本属性,是否安全模式直接退出,处理参数块等
return 0;
}
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv)); // 创建一个 AppRuntime 实例,这是 Android C++ 运行时库的核心,用于配置和启动ART虚拟机
// 解析参数,识别运行模式等...
if (zygote) {
isXposedLoaded = xposed::initialize(true, startSystemServer, NULL, argc, argv); // 初始化
runtimeStart(runtime, isXposedLoaded ? XPOSED_CLASS_DOTS_ZYGOTE : "com.android.internal.os.ZygoteInit", args, zygote); // 这里把主类替换了
} else if (className) {
isXposedLoaded = xposed::initialize(false, false, className, argc, argv); // 直接启动也可hook
runtimeStart(runtime, isXposedLoaded ? XPOSED_CLASS_DOTS_TOOLS : "com.android.internal.os.RuntimeInit", args, zygote);
} else {
// 错误处理:没有提供类名也没有指定--zygote...
return 10;
}
}
这里有两个点,先看xposed::initialize,它主要是启动一些服务(主要是过selinux的)和将xposedbridge.jar加入到库路径:
bool initialize(bool zygote, bool startSystemServer, const char* className, int argc, char* const argv[]) {
#if !defined(XPOSED_ENABLE_FOR_TOOLS)
if (!zygote) // 如果Xposed没有被编译为支持命令行工具,并且当前也不是Zygote模式,则直接禁用。
return false;
#endif
if (isMinimalFramework()) {// 如果设备处于一个“最小化框架”状态(通常发生在加密设备输入密码之前),则不加载Xposed,后续再进入到完整模式会再次初始化,那时才有必要hook
ALOGI("Not loading Xposed for minimal framework (encrypted device)");
return false;
}
xposed->zygote = zygote; // 将当前的运行模式信息存入一个全局的xposed结构体中,供其他模块使用。
xposed->startSystemServer = startSystemServer;
xposed->startClassName = className;
xposed->xposedVersionInt = xposedVersionInt;
#if XPOSED_WITH_SELINUX
xposed->isSELinuxEnabled = is_selinux_enabled() == 1; // 检查SELinux的状态。在现代Android上,Xposed需要与SELinux策略协同工作
xposed->isSELinuxEnforcing = xposed->isSELinuxEnabled && security_getenforce() == 1;
#else
xposed->isSELinuxEnabled = false;
xposed->isSELinuxEnforcing = false;
#endif
if (startSystemServer) {
xposed::logcat::printStartupMarker(); // 如果是即将启动System Server的Zygote进程,打印一个启动标记到logcat,便于调试
} else if (zygote) {
sleep(10);// 如果是次要的Zygote进程(例如64位系统上的32位Zygote),则等待10秒,这是为了让主要的Zygote进程先启动,同时也避免两个Zygote的日志混杂在一起
}
printRomInfo();// 打印当前ROM的一些信息,便于问题排查
if (startSystemServer) {
if (!determineXposedInstallerUidGid() || !xposed::service::startAll()) {// 如果是System Server,需要启动完整的Xposed服务,用于和Xposed Installer通信
return false;
}
xposed::logcat::start();// 启动logcat的监控,将日志转发给Xposed Installer
}
// ... 其他SELinux相关的服务启动逻辑 ...
if (zygote && !isSafemodeDisabled() && detectSafemodeTrigger(shouldSkipSafemodeDelay()))// 检查用户是否在开机时按下了特定按键组合来触发“安全模式”,这是一个非常重要的保险丝机制
disableXposed(); // 如果检测到安全模式触发,则临时禁用Xposed,防止因模块问题导致无法开机
if (isDisabled() || (!zygote && shouldIgnoreCommand(argc, argv))) // 如果Xposed被(临时或永久)禁用,或者当前是工具模式且该命令被设定为忽略,则返回false
return false;
return addJarToClasspath(); // 这个函数会找到 XposedBridge.jar 文件,并将其路径添加到 CLASSPATH 环境变量中
}
再来看它修改了AppRuntime的onVmCreated:
class AppRuntime : public AndroidRuntime
{
public:
virtual void onVmCreated(JNIEnv* env)
{
if (isXposedLoaded)
xposed::onVmCreated(env); // 上一步成功,在runtime启动时会到这
}}
/////////
void onVmCreated(JNIEnv* env) {
const char* xposedLibPath = NULL;
determineRuntime(&xposedLibPath) // 根据/proc/self/maps看当前加载的运行时是delvik还是art
void* xposedLibHandle = dlopen(xposedLibPath, RTLD_NOW); // 加载对应的运行时 libxposed_*.so
bool (*xposedInitLib)(XposedShared* shared) = NULL;
*(void **) (&xposedInitLib) = dlsym(xposedLibHandle, "xposedInitLib");
#if XPOSED_WITH_SELINUX
xposed->zygoteservice_accessFile = &service::membased::accessFile;
xposed->zygoteservice_statFile = &service::membased::statFile;
xposed->zygoteservice_readFile = &service::membased::readFile;
#endif // XPOSED_WITH_SELINUX
if (xposedInitLib(xposed)) { // 保存shared 设置onVmCreated = &onVmCreatedCommon
xposed->onVmCreated(env); // 进入对应的库,这里咱们只关注libxposed_art.cpp + libxposed_common.cpp
}
}
继续深入:
void onVmCreatedCommon(JNIEnv* env) {
!initXposedBridge(env) || !initZygoteService(env)) ;// 初始化这两个,它们做的事差不多,只是导出的方法有差别,因为权限不同角色不通
onVmCreated(env);
xposedLoadedSuccessfully = true;
return;
}
bool initXposedBridge(JNIEnv* env) {
classXposedBridge = env->FindClass(CLASS_XPOSED_BRIDGE); // 查找de/robv/android/xposed/XposedBridge类,就是xposedbridge.jar里的主要类
classXposedBridge = reinterpret_cast<jclass>(env->NewGlobalRef(classXposedBridge));
register_natives_XposedBridge(env, classXposedBridge) // 为该类注册大量工具方法
methodXposedBridgeHandleHookedMethod = env->GetStaticMethodID(classXposedBridge, "handleHookedMethod",
"(Ljava/lang/reflect/Member;ILjava/lang/Object;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"); // 获取 handleHookedMethod
}
bool onVmCreated(JNIEnv*) {
ArtMethod::xposed_callback_class = classXposedBridge; // 熟悉的,绑定类和方法咯!
ArtMethod::xposed_callback_method = methodXposedBridgeHandleHookedMethod;
return true;
}
总的来说,它就是劫持了启动过程,注入了xposedbridge.jar库,并对其进行初始化,注入native的代码,为了应对selinux它还启动了一些其它服务,例如文件读取它实现了共享内存和binder两种方式...到此,它已经提供了基本的hook能力了
XposedBridge
终于到你咯!但其实也没啥好说的了,它提供很多封装便于咱们使用,代码用脚都能想清楚,这里只看最核心的两个,首先是hook,无论用什么函数最后都会调用到hookMethod:
public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
if (!(hookMethod instanceof Method) && !(hookMethod instanceof Constructor<?>)) {// 确保传入的 hookMethod 是一个方法(Method)或构造函数(Constructor)
throw new IllegalArgumentException("Only methods and constructors can be hooked: " + hookMethod.toString());
} else if (hookMethod.getDeclaringClass().isInterface()) {// 确保不是一个接口方法
throw new IllegalArgumentException("Cannot hook interfaces: " + hookMethod.toString());
} else if (Modifier.isAbstract(hookMethod.getModifiers())) { // 确保不是一个抽象方法,因为抽象方法没有实现体可以Hook
throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod.toString());
}
boolean newMethod = false; // 是否是此方法第一次被Hook
CopyOnWriteSortedSet<XC_MethodHook> callbacks;
// 必须同步操作,因为多个模块可能同时Hook同一个方法。
synchronized (sHookedMethodCallbacks) { // `sHookedMethodCallbacks` 是一个全局的静态Map,存储了从“被Hook的方法”到“其所有回调函数集合”的映射
callbacks = sHookedMethodCallbacks.get(hookMethod); // 尝试获取该方法已有的回调集合
if (callbacks == null) { // 如果为null,说明这是此方法第一次被Hook
callbacks = new CopyOnWriteSortedSet<>();
sHookedMethodCallbacks.put(hookMethod, callbacks);
newMethod = true; // 设置标志,表示稍后需要执行Native Hook
}
}
callbacks.add(callback); // 将新的回调添加到集合中
if (newMethod) { // 只有当一个方法是第一次被Hook时,才需要调用到底层C++代码去修改ArtMethod结构 后续对同一个方法的Hook请求,只需要向上面的`callbacks`集合中添加新的回调即可
Class<?> declaringClass = hookMethod.getDeclaringClass();
int slot = 0;// 在ART上,Native层可以从Member对象中获取所有需要的信息,这位兼容旧的Dalvik,在它上面,需要通过反射获取一些额外信息(如slot ID)。
Class<?>[] parameterTypes = null;
Class<?> returnType = null;
// ... 兼容旧版Dalvik运行时的代码...
AdditionalHookInfo additionalInfo = new AdditionalHookInfo(callbacks, parameterTypes, returnType); // 创建一个附加信息对象,它将包含回调集合等信息,并被传递给Native层
hookMethodNative(hookMethod, declaringClass, slot, additionalInfo); // 这个JNI调用最终会执行我们之前分析过的 ArtMethod::EnableXposedHook 函数
}
return callback.new Unhook(hookMethod); // 创建并返回一个 Unhook 对象,开发者可以保存它,并在需要时调用其 unhook() 方法,以将本次添加的这个callback从回调集合中移除
}
而在被hook的方法被调用时,最终又会调用到Java里的handleHookedMethod:
private static Object handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj,
Object thisObject, Object[] args) throws Throwable {
AdditionalHookInfo additionalInfo = (AdditionalHookInfo) additionalInfoObj;
if (disableHooks) { // 如果Xposed被临时禁用,则直接调用原始方法,跳过所有Hook逻辑
try {
return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes,
additionalInfo.returnType, thisObject, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
Object[] callbacksSnapshot = additionalInfo.callbacks.getSnapshot(); // 获取当前所有回调的快照。使用快照可以保证线程安全,即使在遍历时有其他模块注册/注销Hook,本次执行的回调列表也不会改变
final int callbacksLength = callbacksSnapshot.length;
if (callbacksLength == 0) { // 如果没有任何回调(例如最后一个回调刚刚被注销),也直接调用原始方法
try {
return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes,
additionalInfo.returnType, thisObject, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
MethodHookParam param = new MethodHookParam(); // 创建一个MethodHookParam对象,它是一个“状态载体”,会在所有回调之间传递。模块通过修改这个对象来影响方法的执行
param.method = method;
param.thisObject = thisObject;
param.args = args;
int beforeIdx = 0;
do {
try {
((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param); // 按顺序执行所有 beforeHookedMethod 回调
} catch (Throwable t) {
XposedBridge.log(t); // 如果某个回调抛出异常,记录日志,但不会中断整个流程
param.setResult(null); // 重置结果,忽略异常回调所做的修改
param.returnEarly = false;
continue;
}
if (param.returnEarly) { // 模块可以在 before 回调中设置 param.returnEarly = true。这会使程序跳过原始方法和所有后续的 before/after 回调,直接返回一个指定的结果
beforeIdx++; // 移动索引,以便 after 回调能从正确的位置开始
break;
}
} while (++beforeIdx < callbacksLength);
if (!param.returnEarly) { // 如果没有被 "before" 回调提前返回,则执行原始方法
try {
Object result = invokeOriginalMethodNative(method, originalMethodId,
additionalInfo.parameterTypes, additionalInfo.returnType, param.thisObject, param.args); // 通过JNI调用,执行原始的、未被Hook的方法代码
param.setResult(result); // 将原始方法的返回值存入param对象
} catch (InvocationTargetException e) {
param.setThrowable(e.getCause()); // 如果原始方法抛出异常,捕获它并存入param对象
}
}
int afterIdx = beforeIdx - 1;
do { // 从最后一个被执行的 "before" 回调开始,以相反的顺序执行 "after" 回调。这种 LIFO (后进先出) 的顺序确保了回调的“洋葱模型”,即 a.before -> b.before -> original -> b.after -> a.after
Object lastResult = param.getResult();
Throwable lastThrowable = param.getThrowable();
try {
((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param);
} catch (Throwable t) {
XposedBridge.log(t);
if (lastThrowable == null) // 如果 after 回调异常,恢复到它执行前的结果,避免污染
param.setResult(lastResult);
else
param.setThrowable(lastThrowable);
}
} while (--afterIdx >= 0);
if (param.hasThrowable()) { // 在所有流程结束后,检查param对象的状态
throw param.getThrowable(); // 如果param中存有异常,则将其抛出
} else {
return param.getResult(); // 否则,返回最终的结果(可能已被after回调修改过)
}
}
结尾,xposed YYDS,但是直接改art并不优雅,也早早没更新了,它有很多后续,如:
EdXposed 支持Android 8~11
LSPosed 支持Android 8.1~13 (安装后默认从通知栏打开界面)
下面会继续介绍后者~
Magisk

magisk是流行的root框架,相比远古直接改/system分区,它更提前了,直接修改/init来实现控制流的劫持,于是在初始化阶段就能以特权身份启动自己的服务,对se策略做修补等;它还提供了一个包含zygisk的模块系统,它可以在系统启动早期以特权身份做很多事,包括执行脚本、替换文件等,利用zygisk还可以向app中注入额外逻辑,比如类似xposed的功能,前面也提到xposed停止维护了,取而代之的是lspoed,它可以利用zygisk将自身注入目标应用,实现Java级hook (兼容xposed模块),而lsposed底层是lsplant,所以接下来会分别介绍它们!
Magisk分析
它有多个部分:
1.magiskinit:它会被用于替换/init,在执行必要的分区挂载后,它就将magisk服务注入到init.rc中,并且对selinux 策略进行了修补,使其服务能以特权运行
2.magiskpolicy:它可以从不同来源加载SELinux策略,将其保存到文件或直接应用到内核,另外它还会被取别名为supolicy保持兼容
3.magisk:它是一个多功能的二进制文件,类似于busybox可由不同别名启动来实现相关功能,例如su启动就是做提权,resetprop就是直接读些属性(不经过系统原有机制,特权且过检查),它还能作为守护进程提供服务、管理模块等,有很多功能
4.magiskboot:它用于对启动镜像重打包,主要就是将magiskinit替换原始init,并将一些必要的组件(核心magisk.xz存根 stub.xz和接触se策略的 init-ld.xz)打包进boot镜像
模块系统
magisk可安装模块,模块是一个zip文件,会被安装在/data/adb/modules下,模块的典型结构如下:
/data/adb/modules
├── $MODID <--- 模块ID,每个模块的ID应该独一无二
│ ├── module.prop <--- 模块元信息:ID,名称,版本,作者,描述,升级URL等
│ ├── system <--- 该目录下的内容将会覆盖到 /system
│ ├── zygisk <--- Zygisk本地库,以架构命名,会根据当前架构注入对应的so
│ │ ├── arm64-v8a.so
│ │ ├── armeabi-v7a.so
│ │ ├── riscv64.so
│ │ ├── x86.so
│ │ ├── x86_64.so
│ │ └── unloaded <--- 没有兼容库
│ ├── skip_mount <--- 存在则不挂载system
│ ├── disable <--- 存在则模块被禁用
│ ├── remove <--- 存在则下次重启会删除当前模块
│ ├── post-fs-data.sh <--- post-fs-data阶段执行的脚本,这是只是/data被挂载,还很早起,zygote都没初始化,阻塞的
│ ├── service.sh <--- late_start service阶段执行的脚本,非阻塞,通常都是在这个阶段运行
│ ├── uninstall.sh <--- 模块被删除时执行的脚本
│ ├── action.sh <--- 在Magisk.app里模块action按钮被点击时执行
│ ├── system.prop <--- 会被合并到系统属性
│ ├── sepolicy.rule <--- 自定义的SE策略
│ ├── vendor <--- Symlink to $MODID/system/vendor
│ ├── product <--- Symlink to $MODID/system/product
│ ├── system_ext <--- Symlink to $MODID/system/system_ext
像改/system时有些额外规则,如默认覆盖/新增,而创建.replace则替换整个目录,而mknod <path> c 0 0可删除文件。
在系统启动过程中(post-fs-data阶段),magiskd会扫描模块目录,并根据每个目录里相应的标志决定是否加载模块还是干其它的。
zygisk
这里再看看zygisk,看名字就知道它是magisk for zygote的了,不像xposed直接改app_process,它利用了native bridge机制去注入so劫持zygote fork过程,从而实现任意库注入的,咱先看看它的模块长啥样,直接上样例:
class MyModule : public zygisk::ModuleBase {
public:
void onLoad(Api *api, JNIEnv *env) override { // 模块被加载时
this->api = api;
this->env = env;
}
virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs *args) {}// app 特化前执行,此时还没沙盒限制(和zygote一样权限),这里可以修改特化参数
virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs *args) {} // app特化后马上执行
virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs *args) {} // 对SystemServer的
virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs *args) {}
};
static int urandom = -1;
static void companion_handler(int i) {
if (urandom < 0) { urandom = open("/dev/urandom", O_RDONLY); }
read(urandom, &r, sizeof(r)); // 它会在特权进程(根据当前进程决定是zygiskd32/64)里运行
write(i, &r, sizeof(r));
}
REGISTER_ZYGISK_MODULE(MyModule) // 注册模块
REGISTER_ZYGISK_COMPANION(companion_handler) // 注册伙伴处理器
可以看出zygisk提供了接口让模块在app/system_server在特化前后执行相关操作,如注入指定so库等的能力,另外zygote本身不是root权限,所以它还提供了root权限运行的zygiskd服务,让so同时在该服务内运行,从而为模块提供高权限的能力。现在从源码开始分析具体的实现,前面提了它是劫持了native bridge,具体来说它在模块加载阶段,就看是否启用zygisk,启用则将ro.dalvik.vm.native.bridge属性备份并重新设置为zygisk的so,该so导出如下:
extern "C" [[maybe_unused]] NativeBridgeCallbacks NativeBridgeItf { // native bridge的标准导出结构
.version = 2,
.padding = {},
.isCompatibleWith = [](auto) { // 会被最先执行,看是否能处理,它永远返回false所以不会影响程序执行
zygisk_logging();
hook_entry(); // 执行hook操作
return false;
},
};
这个hook_entry被用于hook一些点,注意此时art还是在初始化阶段,很多东西还美完成初始化/装载所以它无法在此时直接hook关键点,退而求其次它很聪明的hook了一些其它点,当这些点满足条件时就是合适的时机点咯:
void hook_entry() {
default_new(g_hook); // 为HookContext分配内存,它里面保存了plt的备份,NativeBridgeRuntimeCallbacks等信息
g_hook->hook_plt();
}
void HookContext::hook_plt() {
ino_t android_runtime_inode = 0;
dev_t android_runtime_dev = 0;
ino_t native_bridge_inode = 0;
dev_t native_bridge_dev = 0;
for (auto &map : lsplt::MapInfo::Scan()) { // lsplt::MapInfo::Scan() 会扫描当前进程的内存映射(/proc/self/maps),找到所有已加载的so库
if (map.path.ends_with("/libandroid_runtime.so")) { // 定位 libandroid_runtime.so,这是Android Java框架与Native世界的桥梁
android_runtime_inode = map.inode;
android_runtime_dev = map.dev;
} else if (map.path.ends_with("/libnativebridge.so")) { // 定位 libnativebridge.so,用于支持跨CPU架构的应用(如在x86上运行ARM应用)
native_bridge_inode = map.inode;
native_bridge_dev = map.dev;
}
}
// 下面都是宏定义
PLT_HOOK_REGISTER(native_bridge_dev, native_bridge_inode, dlclose); // 系统在加载完Native Bridge(一种跨CPU架构的兼容层)后,会尝试通过dlclose卸载Zygisk的加载器(zygisk_loader),这是LoadNativeBridge流程的最后一步,它会在此时调用g_hook->post_native_bridge_load(handle)
PLT_HOOK_REGISTER(android_runtime_dev, android_runtime_inode, fork); // Zygisk预先fork一个进程,将新进程的PID保存在g_ctx->pid中,然后当系统请求fork时,直接返回这个已经创建好的PID,而不是调用真正的old_fork() ((g_ctx && g_ctx->pid >= 0) ? g_ctx->pid : old_fork())
PLT_HOOK_REGISTER(android_runtime_dev, android_runtime_inode, unshare); // 当一个新进程被创建后,系统会调用unshare(CLONE_NEWNS)来为这个进程创建一个独立的挂载命名空间(Mount Namespace),使它看不到系统或其他App的挂载点,当unshare被调用时,如果DO_REVERT_UNMOUNT被设置(进程在拒绝列表/zygisk请求设置拒绝),它会调用revert_unmount,这会在新的ns里写在magisk的特殊挂载点,可用于一定程度的痕迹隐藏 (只是卸载了挂载点,其它事还在生效)
PLT_HOOK_REGISTER(android_runtime_dev, android_runtime_inode, selinux_android_setcontext); // 在降权前保存日志fd,保证写日志正常
PLT_HOOK_REGISTER(android_runtime_dev, android_runtime_inode, strdup); // 当new_strdup的参数str为"com.android.internal.os.ZygoteInit"时触发,这发生在Android运行时准备调用Zygote的Java主类(ZygoteInit.main)之前,此时runtime已经完成初始化,它会在此调用g_hook->hook_zygote_jni()
PLT_HOOK_REGISTER_SYM(android_runtime_dev, android_runtime_inode, "__android_log_close", android_log_close); // 根据SKIP_CLOSE_LOG_PIPE决定是否要关闭日志
if (!lsplt::CommitHook()) // 在注册完所有Hook后,CommitHook() 会实际地去修改内存中目标库的GOT表,完成地址替换
ZLOGE("plt_hook failed\n");
plt_backup.erase(
std::remove_if(plt_backup.begin(), plt_backup.end(),
[](auto &t) { return *std::get<3>(t) == nullptr;}),
plt_backup.end()); // plt_backup 是一个保存了原始函数地址的备份列表,这段代码会遍历备份列表,移除那些由于某种原因未能成功Hook的条目,保持数据一致性
}
先看post_native_bridge_load,它在我们伪造的native bridge(zygisk loader)被卸载时调用,此时它就可以通过栈回溯找到ART内部的NativeBridgeRuntimeCallbacks:
void HookContext::post_native_bridge_load(void *handle) {
self_handle = handle; // 保存我们自己的句柄,以便将来可以卸载自己
// 定义一个函数指针类型,它与 android::LoadNativeBridge 的函数签名相匹配。
using method_sig = const bool (*)(const char *, const NativeBridgeRuntimeCallbacks *);
struct trace_arg { // 定义一个结构体,用于从栈回溯的lambda表达式中“返回”多个值
method_sig load_native_bridge; // 用于存储找到的LoadNativeBridge函数地址
const NativeBridgeRuntimeCallbacks *callbacks; // 用于存储找到的回调函数表指针
};
trace_arg arg{}; // 创建一个实例来接收结果
_Unwind_Backtrace(+[](_Unwind_Context *ctx, void *arg) -> _Unwind_Reason_Code { // _Unwind_Backtrace 会从当前位置开始,逐层向上遍历函数的调用栈,每遍历一层,它就会调用一次我们提供的lambda表达式
void *fp = unwind_get_region_start(ctx); // 获取当前栈帧正在执行的代码的地址
Dl_info info{};
dladdr(fp, &info); // dladdr可以根据代码地址,反查出它属于哪个so库
if (info.dli_fname && std::string_view(info.dli_fname).ends_with("/libnativebridge.so")) { // 不断向上回溯,直到找到一个属于 /libnativebridge.so 的栈帧
auto payload = reinterpret_cast<trace_arg *>(arg);
payload->load_native_bridge = reinterpret_cast<method_sig>(fp); // 假设当前栈帧的函数指针就是 android::LoadNativeBridge 的地址
payload->callbacks = find_runtime_callbacks(ctx); // 搜索当前栈帧的寄存器和栈内存,从中找出回调函数表的指针 (libart中的PROT_WRITE | PROT_READ且根据调用约定在栈/寄存器中找到指向该区域的))
return _URC_END_OF_STACK;
}
return _URC_NO_REASON; // 如果没找到,继续向上回溯
}, &arg);
if (!arg.load_native_bridge || !arg.callbacks)
return;
auto nb = get_prop(NBPROP); // 这里去加载真正的native bridge
auto len = sizeof(ZYGISKLDR) - 1;
if (nb.size() > len) {
arg.load_native_bridge(nb.data() + len, arg.callbacks); // 调用我们刚刚通过栈回溯找到的原始LoadNativeBridge函数,去加载真正的库
}
runtime_callbacks = arg.callbacks; // 保存到全局
}
接着是hook_zygote_jni,它含hook了nativeForkAndSpecialize/nativeSpecializeAppProcess/nativeForkSystemServer的所有重载,在其执行前与执行后封装了自己的逻辑(module.cpp里),在后者的内部就会调用模块对应的导出函数:
void HookContext::hook_zygote_jni() {
// ----------------------------------------------------------------
// 1. 寻找 JNI_GetCreatedJavaVMs 函数
// ----------------------------------------------------------------
// JNI_GetCreatedJavaVMs 是一个标准的JNI函数,可以获取当前进程中所有已创建的JavaVM实例。
// 这是我们与Java世界建立联系的起点。
// 定义一个函数指针类型,方便转换。
using method_sig = jint(*)(JavaVM **, jsize, jsize *); // 定义一个函数指针类型,方便转换
auto get_created_vms = reinterpret_cast<method_sig>( // 首先,尝试用dlsym(RTLD_DEFAULT, ...)在全局符号中直接查找
dlsym(RTLD_DEFAULT, "JNI_GetCreatedJavaVMs"));
if (!get_created_vms) { // 如果全局查找失败(在某些Android版本或设备上可能发生),则启动备用方案
for (auto &map: lsplt::MapInfo::Scan()) { // 扫描内存,找到 libnativehelper.so 这个库,因为 JNI_GetCreatedJavaVMs 通常由它导出
if (!map.path.ends_with("/libnativehelper.so")) continue;
void *h = dlopen(map.path.data(), RTLD_LAZY);
get_created_vms = reinterpret_cast<method_sig>(dlsym(h, "JNI_GetCreatedJavaVMs")); // 在这个库的句柄中,精确地查找 JNI_GetCreatedJavaVMs 函数
dlclose(h);
break;
}
if (!get_created_vms) {
ZLOGW("JNI_GetCreatedJavaVMs not found\n");
return;
}
}
JavaVM *vm = nullptr;
jsize num = 0;
jint res = get_created_vms(&vm, 1, &num); // 调用 JNI_GetCreatedJavaVMs,请求获取最多1个JavaVM实例(Android进程中只有一个)
if (res != JNI_OK || vm == nullptr) {
ZLOGW("JavaVM not found\n");
return;
}
JNIEnv *env = nullptr;
res = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6); // 通过获取到的JavaVM实例,得到属于当前线程的JNIEnv环境指针
if (res != JNI_OK || env == nullptr) {
ZLOGW("JNIEnv not found\n");
}
hook_jni_methods(env, kZygote, zygote_methods); // kZygote=com/android/internal/os/Zygote zygote_methods=<jni_hooks.cpp> define
}
void HookContext::hook_jni_methods(JNIEnv *env, const char *clz, JNIMethods methods) {
jclass clazz;
if (!runtime_callbacks || !env || !clz || !(clazz = env->FindClass(clz))) { // 如果任何一个关键组件(runtime_callbacks、JNI环境、类名)不存在,或者无法找到目标类,则无法进行Hook
for (auto &method : methods) {
method.fnPtr = nullptr;
}
return;
}
auto total = runtime_callbacks->getNativeMethodCount(env, clazz); // runtime_callbacks是之前通过栈回溯获取到的ART回调函数表,它提供了获取JNI方法表的能力
auto old_methods = std::make_unique_for_overwrite<JNINativeMethod[]>(total); // 创建一个智能指针数组,用于存放原始的方法表
runtime_callbacks->getNativeMethods(env, clazz, old_methods.get(), total); // 调用回调,将当前类所有已注册的JNI方法完整地备份到 old_methods 中
auto new_methods = std::make_unique_for_overwrite<JNINativeMethod[]>(total); // 再创建一个数组,用于在循环中存放更新后的方法表
for (auto &method : methods) {
// 允许传入空的函数指针,这可能用于恢复Hook。
if (!method.fnPtr) continue;
if (env->RegisterNatives(clazz, &method, 1) == JNI_ERR ||
env->ExceptionCheck() == JNI_TRUE) { // 调用JNI的RegisterNatives函数,这个函数有一个特性:如果注册的方法名和签名与一个已存在的方法相同,它会用新的函数指针覆盖掉旧的。Zygisk正是利用了这一点来实现Hook。
if (auto exception = env->ExceptionOccurred()) { // 如果注册失败,清理JNI异常,将fnPtr设为null表示此方法Hook失败,然后继续下一个
env->DeleteLocalRef(exception);
}
env->ExceptionClear();
method.fnPtr = nullptr;
continue;
}
runtime_callbacks->getNativeMethods(env, clazz, new_methods.get(), total); // Hook成功后,我们需要找到被替换掉的原始函数指针,以便在我们的新函数中调用它,再次获取JNI方法表,此时它已经被RegisterNatives修改过了
for (auto i = 0; i < total; ++i) {
auto &new_method = new_methods[i];
if (new_method.fnPtr == method.fnPtr && strcmp(new_method.name, method.name) == 0) { // 通过比较函数名和新的函数指针,在更新后的方法表中找到我们刚刚注册的那一项
// 获取在相同索引位置的、备份好的原始方法。
auto &old_method = old_methods[i];
ZLOGD("replace %s#%s%s %p -> %p\n", clz,
method.name, method.signature, old_method.fnPtr, method.fnPtr);
method.fnPtr = old_method.fnPtr; // [关键] 将我们传入的 method 结构体中的 fnPtr 修改为原始函数的指针, 这样,上层调用者就能通过这个被修改的结构体,拿到原始函数的地址
break;
}
}
old_methods.swap(new_methods); // 交换两个数组,下一次循环时,这次的“新”表就成了“旧”表
}
}
至此,magisk+zygisk结束咯~
LSPosed分析

LSPlant分析
lsplant是一个安卓ART hook库,也是LSPosed的底层库,所以先看它;不像xposed直接修改ART源码,它是通过hook实现对ART修改的,这里用的hook框架(native hook)是初始化时传入的,可以由用户指定,测试用例中是dobby:
struct InitInfo {
// 需要在外部实现下面4个函数(前三个是必选,第四个可选),下面是函数定义和域
using InlineHookFunType = std::function<void *(void *target, void *hooker)>;
using InlineUnhookFunType = std::function<bool(void *func)>;
using ArtSymbolResolver = std::function<void *(std::string_view symbol_name)>;
using ArtSymbolPrefixResolver = std::function<void *(std::string_view symbol_prefix)>;
InlineHookFunType inline_hooker; // Inline Hook函数
InlineUnhookFunType inline_unhooker; // 取消Inline Hook函数
ArtSymbolResolver art_symbol_resolver; // 用于在libart.so中查找内部的函数或变量的地址(包括导出的和未导出的)
ArtSymbolPrefixResolver art_symbol_prefix_resolver; // 一个更灵活的符号查找器,用于查找具有特定前缀的第一个符号,这在符号名可能因编译器或版本而略有变化时非常有用
// 下面几个是在生成动态代理类时设置的名称,主要是调试用 (改下也可以一定程度躲避检查)
std::string_view generated_class_name = "LSPHooker_"; // 动态生成的代理类的类名,必选
std::string_view generated_source_name = "LSP"; // 动态生成的DEX文件的源文件名,可选
std::string_view generated_field_name = "hooker"; // 在代理类中生成的、用于存放回调对象的静态字段的名称,必选
std::string_view generated_method_name = "{target}"; // 在代理类中生成的Hook方法的名称,如果设置为"{target}",则生成的方法名将与被Hook的目标方法名相同
};
现在,咱们得从上往下看它的hook是咋实现的了(说实话下面的不太适合讲解,但是懒得自己写了):
test(){
var staticMethod = LSPTest.class.getDeclaredMethod("staticMethod");
var staticMethodReplacement = Replacement.class.getDeclaredMethod("staticMethodReplacement", Hooker.MethodCallback.class);
Hooker hooker = Hooker.hook(staticMethod, staticMethodReplacement, null); // 测试类里,去用Replacement.staticMethodReplacement替换LSPTest的staticMethod
}
public class Replacement {
static boolean staticMethodReplacement(Hooker.MethodCallback callback) {
return true; // hook方法直接返回true
}}
public class Hooker {
public Object callback(Object[] args) throws InvocationTargetException, IllegalAccessException {
var methodCallback = new MethodCallback(backup, args);
return replacement.invoke(owner, methodCallback); // 调用替换的函数
}
public static Hooker hook(Member target, Method replacement, Object owner) {
Hooker hooker = new Hooker();
try {
var callbackMethod = Hooker.class.getDeclaredMethod("callback", Object[].class);
var result = hooker.doHook(target, callbackMethod); // 核心,做hook,实际调用的lsplant::Hook,这里没有直接替换,而是自己的callback作为hook方法,主要是一个封装,如果为了演示直接替换最简单
if (result == null) return null;
hooker.backup = result; // 保存原始方法和一些额外信息
// ...
} catch (NoSuchMethodException ignored) {
}
return hooker;
}
}
再往下,看Hook是怎么实现的,这里简单先说下,它利用了ArtMethod的两个函数BackupTo(保存原函数点)和SetEntryPoint修改入口为新地址,先说新地址它并不是直接跳到新的函数处,而是用了段硬编码的蹦床,蹦床是架构相关的,而且也没做啥,就是一个绝对跳转(说实话俺没懂为啥要这样做...),而新的位置,是一段动态生成的Dex的新方法的位置:
[[maybe_unused]] jobject Hook(JNIEnv *env, jobject target_method, jobject hooker_object,
jobject callback_method) {
// 输入参数校验 ...
jmethodID hook_method = nullptr; // 预先声明将要使用的变量
jmethodID backup_method = nullptr;
jfieldID hooker_field = nullptr;
auto *target = ArtMethod::FromReflectedMethod(env, target_method); // 获取目标方法(被hook的方法)在ART虚拟机内部的C++对象 ArtMethod*
bool is_static = target->IsStatic();
if (IsHooked(target, true)) { // 检查是否已经Hook过,防止重复Hook
LOGW("Skip duplicate hook");
return nullptr;
}
ScopedLocalRef<jclass> built_class{env}; //使用一个独立的作用域来管理JNI局部引用和字符串的生命周期
{
auto callback_name =
JNI_Cast<jstring>(JNI_CallObjectMethod(env, callback_method, method_get_name)); // 通过JNI获取进行DEX构建所需的所有信息:回调方法名、目标方法名、回调类的类加载器和类名等
JUTFString callback_method_name(callback_name);
// ... (获取其他所需信息) ...
std::tie(built_class, hooker_field, hook_method, backup_method) = WrapScope( // 调用BuildDex,在内存中动态生成一个包含代理类的DEX文件,这个代理类里包含了与目标方法签名一致的“Hook方法”和“Backup方法”
env,
BuildDex(env, callback_class_loader.get(), // 获取目标方法的shorty签名
__builtin_expect(is_proxy, 0) ? GetProxyMethodShorty(env, target_method)
: ArtMethod::GetMethodShorty(env, target_method),
is_static, target->IsConstructor() ? "constructor" : target_method_name.get(),
class_name.get(), callback_method_name.get()));
}
auto reflected_hook = JNI_ToReflectedMethod(env, built_class, hook_method, is_static); // 将动态生成的Hook方法和Backup方法的jmethodID,转换回Java层的Method对象(jobject)
auto reflected_backup = JNI_ToReflectedMethod(env, built_class, backup_method, is_static);
JNI_CallVoidMethod(env, reflected_backup, set_accessible, JNI_TRUE); // 确保Backup方法是可访问的,这样用户才能通过它调用原始方法
auto *hook = ArtMethod::FromReflectedMethod(env, reflected_hook.get()); // 获取动态生成的Hook方法和Backup方法在ART内部的C++ ArtMethod* 指针
auto *backup = ArtMethod::FromReflectedMethod(env, reflected_backup.get());
JNI_SetStaticObjectField(env, built_class, hooker_field, hooker_object); // 将用户传入的hooker_object实例,设置到我们动态生成的代理类的静态字段中,这样,当代理方法被执行时,它就能找到并调用正确的用户回调逻辑
if (DoHook(target, hook, backup)) { // 调用DoHook,执行“心脏手术”——暂停虚拟机,修改目标ArtMethod的入口点
std::apply(
[backup_method, target_method_id = env->FromReflectedMethod(target_method)](auto... v) { // 遍历框架内部缓存的一些JNI方法ID,如果发现其中某个ID正好是我们刚刚Hook的目标,就把它更新为指向Backup方法。这可以防止在回调逻辑中因调用反射而导致的无限递归
((*v == target_method_id &&
(LOGD("Propagate internal used method because of hook"), *v = backup_method)) ||
...);
},
kInternalMethods);
jobject global_backup = JNI_NewGlobalRef(env, reflected_backup); // 创建一个全局JNI引用指向Backup方法,这是将要返回给用户的句柄
RecordHooked(target, target->GetDeclaringClass()->GetClassDef(), global_backup, backup);// 在框架内部记录这次Hook
if (!is_proxy) [[likely]] { // 记录JIT相关的状态,防止JIT编译干扰Hook
RecordJitMovement(target, backup);
}
RecordDeoptimized(hook->GetDeclaringClass()->GetClassDef(), backup); // 将Backup方法标记为“已去优化”,防止它被意外修改
return global_backup;
}
return nullptr;
}
上面有两个重点,DoHook说了,而BuildDex也很简单,它创建了一个新的类,类里面有两个和目标方法签名一样的方法hook和backup,后者是dummy没做啥,主要是创建artmethod保存原始方法指针,而前者完成了一个参数装箱和打包为数组的操作,之后会调用传入的hook方法,之后又对hook方法的结果做处理(如拆箱,类似xposed里InvokeXposedHandleHookedMethod做的事),还有一个域存储hook类,其实它为了防止Jit优化,匹配多版本等还做了很多事,可以自己看代码,咱就不继续了!
轮到LSPosed
这个项目依然庞大,先简单列下目录吧:
(base) ➜ LSPosed git:(master) tree -L 1
.
├── app # Manager应用的UI界面,用于管理LSPosed框架和模块
├── core # LSPosed框架核心实现模块,会被注入到目标应用 (lspd.dex)
├── daemon # 后台服务,处理框架的核心逻辑和模块管理
├── dex2oat # DEX编译优化工具模块
├── external # 外部依赖和第三方库目录
├── hiddenapi # 提供对Android隐藏API的访问能力
├── magisk-loader # Magisk模块加载器,支持Zygisk和Riru两种方式
└── services # AIDL服务接口定义模块
接下来咱只关注它的注入和模块加载过程了,即magisk-loader,而且只关注zygisk,它分为对system_server和普通应用的hook,这里只看后者:
class ZygiskModule : public zygisk::ModuleBase {
void preAppSpecialize(zygisk::AppSpecializeArgs *args) override {
MagiskLoader::GetInstance()->OnNativeForkAndSpecializePre(
env_, args->uid, args->gids, args->nice_name,
args->is_child_zygote ? *args->is_child_zygote : false, args->app_data_dir);
}
void postAppSpecialize(const zygisk::AppSpecializeArgs *args) override {
MagiskLoader::GetInstance()->OnNativeForkAndSpecializePost(env_, args->nice_name, args->app_data_dir);
if (*allowUnload) api_->setOption(zygisk::DLCLOSE_MODULE_LIBRARY);
}
}
可见主要逻辑还是在MagiskLoader里,它的pre主要是初始化服务连接、对magisk自己授网络权限和判断是否要注入,这里看post部分:
void MagiskLoader::OnNativeForkAndSpecializePost(JNIEnv *env, jstring nice_name, jstring app_dir) {
const JUTFString process_name(env, nice_name);
auto *instance = Service::instance();
auto binder = skip_ ? ScopedLocalRef<jobject>{env, nullptr}
: instance->RequestBinder(env, nice_name); // `skip_` 是在 Pre 函数中设置的布尔值,如果为 false,说明这是一个合法的注入目标,我们需要通过IPC向Magisk服务请求一个Binder通信句柄, 如果为 true,则不请求Binder,后续注入流程将被跳过
if (binder) {
lsplant::InitInfo initInfo{//... 我们熟悉的,lsplant初始化结构
};
auto [dex_fd, size] = instance->RequestLSPDex(env, binder); // 通过Binder向Magisk服务请求LSPosed框架的DEX文件描述符和大小 (core:lspd.dex)
auto obfs_map = instance->RequestObfuscationMap(env, binder);
ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); // 请求混淆映射表(用于反混淆)
LoadDex(env, PreloadedDex(dex_fd, size)); // 将获取到的DEX文件加载到当前进程的内存中
close(dex_fd); // 加载完即可关闭fd
InitArtHooker(env, initInfo); // 调用lsplant的初始化函数,传入我们准备好的InitInfo
InitHooks(env); // 执行LSPosed框架自身需要的一些初始Hook
SetupEntryClass(env); // 设置LSPosed框架在Java层的入口类 找到:org.lsposed.lspd.core.Main
FindAndCall(env, "forkCommon",
"(ZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V",
JNI_FALSE, nice_name, app_dir, binder); // 调用org.lsposed.lspd.core.Main的`forkCommon`,把进程信息(nice_name, app_dir, binder)传递过去,Java层从这里开始接管,进行加载用户模块等操作
setAllowUnload(false); // 明确设置不允许卸载Zygisk的so库,因为它正在承载LSPosed框架
GetArt(true);
} else {
auto context = Context::ReleaseInstance(); // 释放所有已初始化的资源
auto service = Service::ReleaseInstance();
GetArt(true);
setAllowUnload(true); // 明确设置允许卸载Zygisk的so库,以节省内存
}
}
这里面的InitHooks注册了很多native函数用来做hook等操作(回顾lsplant),不多说,直接去forkCommon:
public class Main {
public static void forkCommon(boolean isSystem, String niceName, String appDir, IBinder binder) {
Startup.initXposed(isSystem, niceName, appDir, ILSPApplicationService.Stub.asInterface(binder));// 首先,调用Startup.initXposed进行最基础的初始化,比如设置日志、保存进程上下文等
if ((niceName.equals(BuildConfig.MANAGER_INJECTED_PKG_NAME) || niceName.equals(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME))
&& ParasiticManagerHooker.start()) { // 如果当前启动的进程是LSPosed管理器本身,则执行特殊的寄生逻辑,这会在管理器内部设置一些Hook,让管理器能和框架通信,但不会加载其他模块,然后直接返回。
return;
}
Startup.bootstrapXposed(); // 开始执行Xposed的引导程序
}
}
public class Startup {
private static void startBootstrapHook(boolean isSystem) {
LSPosedHelper.hookMethod(CrashDumpHooker.class, Thread.class, "dispatchUncaughtException", Throwable.class); // Hook线程的未捕获异常处理器,这样当App崩溃时,LSPosed可以捕获崩溃信息,并在日志中加入LSPosed相关上下文(如激活的模块),便于排查问题
if (isSystem) { // 如果是System Server进程,Hook ZygoteInit.handleSystemServerProcess,以便在System Server的生命周期中执行特定操作。
LSPosedHelper.hookAllMethods(HandleSystemServerProcessHooker.class, ZygoteInit.class, "handleSystemServerProcess");
} else { // 如果是普通App进程,Hook DexFile的几个open方法,这使得LSPosed可以感知到App加载的所有DEX文件。
LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openDexFile");
LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openInMemoryDexFile");
LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openInMemoryDexFiles");
}
LSPosedHelper.hookConstructor(LoadedApkCtorHooker.class, LoadedApk.class,
ActivityThread.class, ApplicationInfo.class, CompatibilityInfo.class,
ClassLoader.class, boolean.class, boolean.class, boolean.class); // Hook LoadedApk的构造函数。LoadedApk是Android框架中代表一个已加载APK的类,通过Hook它的构造函数,LSPosed可以在一个App被加载的最早期捕获到它的信息。
LSPosedHelper.hookMethod(LoadedApkCreateCLHooker.class, LoadedApk.class, "createOrUpdateClassLoaderLocked", List.class); // Hook LoadedApk的创建ClassLoader的方法,这是LSPosed能够获取到App的ClassLoader,并将模块代码注入进去的核心Hook点之一
LSPosedHelper.hookAllMethods(AttachHooker.class, ActivityThread.class, "attach"); // Hook ActivityThread.attach,这是App生命周期中一个非常早的阶段,它在这里开始加载模块
}
public static void bootstrapXposed() {
try {
startBootstrapHook(XposedInit.startsSystemServer);
XposedInit.loadLegacyModules(); // 在框架自身准备就绪后,开始扫描、加载并初始化所有用户安装的传统Xposed模块
} catch (Throwable t) {
Utils.logE("error during Xposed initialization", t);
}
}
public static void initXposed(boolean isSystem, String processName, String appDir, ILSPApplicationService service) {
ApplicationServiceClient.Init(service, processName); // 初始化客户端,连接服务端
XposedBridge.initXResources(); // 初始化Xposed的资源Hook功能
XposedInit.startsSystemServer = isSystem; // 将进程上下文信息保存到全局静态变量中,供框架各部分使用
LSPosedContext.isSystemServer = isSystem;
LSPosedContext.appDir = appDir;
LSPosedContext.processName = processName;
PrebuiltMethodsDeopter.deoptBootMethods(); // 对一些系统启动方法进行“去优化”,确保它们可以被稳定Hook
}
}
这里最重要的就是对ActivityThread.attach的hook,它在应用初始化的最后阶段执行,在AttachHooker.class里做的事就是调用XposedInit.loadModules:
public final class XposedInit {
public static void hookResources() throws Throwable { // 初始化资源Hook,资源hook是xposed很少被提及的功能,lsposed也实现了,忽略~
}
private static XResources cloneToXResources(XC_MethodHook.MethodHookParam<?> param, String resDir) { // 将一个标准的Resources对象,转换成我们自定义的XResources对象
}
private static final Map<String, Optional<String>> loadedModules = new ConcurrentHashMap<>(); // 用于存放已加载模块信息的Map
public static void loadLegacyModules() { // 加载所有已安装的Xposed模块。
var moduleList = serviceClient.getLegacyModulesList(); // 通过Binder向Magisk服务请求已安装的模块列表
moduleList.forEach(module -> { // 遍历列表,逐一加载
if (!loadModule(module.packageName, module.apkPath, module.file)) {
loadedModules.remove(module.packageName);// 如果加载失败,则从已加载列表中移除
}
});
}
public static void loadModules(ActivityThread at) {
var packages = (ArrayMap<?, ?>) XposedHelpers.getObjectField(at, "mPackages");
serviceClient.getModulesList().forEach(module -> {
loadedModules.put(module.packageName, Optional.empty());
if (!LSPosedContext.loadModule(at, module)) {
loadedModules.remove(module.packageName);
} else {
packages.remove(module.packageName);
}
});
}
private static boolean loadModule(String name, String apk, PreLoadedApk file) { // 加载单个传统模块的APK文件
var mcl = LspModuleClassLoader.loadApk(apk, file.preLoadedDexes, librarySearchPath, initLoader); // 创建一个特制的LspModuleClassLoader,用于加载模块的APK,这个ClassLoader被设计用来隔离模块,同时能访问到框架的类
if (mcl.loadClass(XposedBridge.class.getName()).getClassLoader() != initLoader) {}// 检查模块是否将Xposed API打包进了自己的APK,这是一个常见的错误,会导致类冲突和各种奇怪的问题,如果检测到,则拒绝加载该模块
initNativeModule(file.moduleLibraryNames); // 初始化模块中的Native库
return initModule(mcl, apk, file.moduleClassNames); // 初始化模块中的Java类
}
private static boolean initModule(ClassLoader mcl, String apk, List<String> moduleClassNames) { // 初始化模块中在xposed_init文件中定义的Java类
var count = 0;
for (var moduleClassName : moduleClassNames) { // 遍历模块声明的所有入口类
try {
Class<?> moduleClass = mcl.loadClass(moduleClassName); // 使用模块自己的ClassLoader加载入口类
if (!IXposedMod.class.isAssignableFrom(moduleClass)) {continue;} // 检查这个类是否实现了Xposed的接口
final Object moduleInstance = moduleClass.newInstance(); // 创建模块入口类的实例
if (moduleInstance instanceof IXposedHookZygoteInit) { // 根据它实现的接口类型,注册相应的回调 for Zygote初始化
((IXposedHookZygoteInit) moduleInstance).initZygote(param);
}
if (moduleInstance instanceof IXposedHookLoadPackage) { // for app包加载
XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));
}
if (moduleInstance instanceof IXposedHookInitPackageResources) { // for 资源初始化
hookResources(); // 确保资源Hook已初始化
XposedBridge.hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance));
}
} catch (Throwable t) {
Log.e(TAG, " Failed to load class " + moduleClassName, t);
}
}
return count > 0; // 如果至少有一个回调被成功加载,则认为模块加载成功。
}
}
LSPosed支持两种api,传统的兼容xposed的api和新的基于注解的api,两种模块的加载方式存在差异,上面是传统模块的加载,而loadModules调用的是LSPosedContext.loadModule:
public static boolean loadModule(ActivityThread at, Module module) {
try {
var sb = new StringBuilder(); // 创建模块专用的ClassLoader,这部分逻辑与加载旧式模块类似:为模块的APK创建一个独立的ClassLoader,以隔离模块的类,同时设置好so库的搜索路径
var abis = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS;
for (String abi : abis) {
sb.append(module.apkPath).append("!/lib/").append(abi).append(File.pathSeparator);
}
var librarySearchPath = sb.toString();
var initLoader = XposedModule.class.getClassLoader();
var mcl = LspModuleClassLoader.loadApk(module.apkPath, module.file.preLoadedDexes, librarySearchPath, initLoader);
if (mcl.loadClass(XposedModule.class.getName()).getClassLoader() != initLoader) {return false;} // 检查模块是否违规打包了API,同上
var ctx = new LSPosedContext(module.packageName, module.applicationInfo, module.service); // 实例化并初始化模块 (新版API的核心),为这个模块创建一个专用的LSPosedContext,这个上下文对象将作为模块与框架交互的桥梁
for (var entry : module.file.moduleClassNames) { // 遍历模块在清单中声明的所有入口类
var moduleClass = mcl.loadClass(entry);
if (!XposedModule.class.isAssignableFrom(moduleClass)) {continue;} // 检查入口类是否实现了新的 XposedModule 接口
try {
var moduleEntry = moduleClass.getConstructor(XposedInterface.class, XposedModuleInterface.ModuleLoadedParam.class); // 寻找构造函数 ctr(XposedInterface context, XposedModuleInterface.ModuleLoadedParam param)
var moduleContext = (XposedModule) moduleEntry.newInstance(ctx, new XposedModuleInterface.ModuleLoadedParam() { // 通过调用这个特定的构造函数来实例化模块,用依赖注入主动将自己(ctx)和环境信息(param)注入到模块中,通过一个匿名内部类,向模块的构造函数提供当前的进程信息。
@Override
public boolean isSystemServer() {
return isSystemServer;
}
@Override
public String getProcessName() {
return processName;
}
});
modules.add(moduleContext); // 将成功实例化的模块添加到全局列表中
} catch (Throwable e) {
Log.e(TAG, " Failed to load class " + moduleClass, e);
}
}
module.file.moduleLibraryNames.forEach(NativeAPI::recordNativeEntrypoint); // 记录模块需要加载的Native库
Log.d(TAG, "Loaded module " + module.packageName + ": " + ctx);
} catch (Throwable e) {
Log.d(TAG, "Loading module " + module.packageName, e);
return false;
}
return true;
}
OK,LSPosed主要部分就结束啦~
使用笔记
安装magisk模块卡在启动界面后:
1.直接adb wait-for-device shell magisk --remove-modules删除所有模块
2.直接去adb shell -c 'rm -rf /data/adb/modules'
模块推荐
Hide-My-Applist:隐藏app列表
LSPosed:用于支持xposed插件
AlwaysTrustUserCerts/Magisk-MoveCACerts/MoveCertificate:自动将证书安装为系统证书,去证书绑定
magisk-frida: 开机后自动运行frida-server
lsposed开发
他们都是兼容xposed api的,开发用的sdk也是用的xposed
KernelSU

或许是第一个开始直接改内核的?说改内核也不准确,它开发了一个内核模块,由于模块直接在内核运行权限当然杠杠的,要安装它,需要是GKI内核,因为GKI是通用的同版本就能用,所以Kernel SU官方可以预先编译含改模块的内核直接替换就好了,除了直接替换内核,还可以用LKM,Kernel SU利用boot_patch.rs去重打包boot.img/init_boot.img,替换init来劫持启动过程,去注入kernelsu.ko;如果不是GKI,那么....有源码也能自己编译内核,把它编进去,如果啥都没有,那看后面的apatch吧!
Kernel SU还有很多可以分析的,比如它的UI,它的内核模块实现,它的守护进程,有空再聊吧~
附一加root笔记
-
打开开发者模式,在开发者选项中打开“OEM解锁”和“USB调试”选项。
-
对于android13以下和以上要分别用GKI和LKM,但是都需要下全量包去提取启动镜像(git上的GKI直接使用会黑屏原因未分析),可以
-
直接去大侠木木处下载全量包,用mt管理器提取
boot.img/init_boot.img(也可使用payload-dumper-go直接在电脑上操作),推送到手机里,用kernelsu管理器去打补丁,然后:
adb reboot bootloader
fastboot flashing unlock
fastboot boot boot.img # 之前有版本支持,后来又不支持了
fastboot flash boot boot.img # android12及之下
fastboot flash init_boot init_boot.img # android13及以上
fastboot reboot
安装ZygiskNext去获取magisk提供zygisk功能。
注:KernelSU不会自动弹出授权提示,需手动开启应用的超级用户权限,如要开启
adb shell的root权限,需为com.android.shell启用超级用户,打开"超级用户"标签页,找到目标应用(Shell)后启用开关 。
Apatch

Apatch 又是后出来的ROOT框架,它依赖KernelPatch直接在内核里实现相关功能,相比Kernel SU修改了更多底层部分,它本身支持KPM和APM两类模块,前者类似于LKM,而后者和Magisk/KernelSu的用户态模块一致,下面从它的底层开始分析
KernelPatch
它通过对内核打补丁来实现代码注入和内核hook能力,它不需要有内核源码或内核含调试信息,只要编译时开启CONFIG_KALLSYMS=y即可,下面先简单讲下它的组成部分
组成部分
1.符号解析:要做hook得先找到hook点,它通过kernel里的banner信息确认内核版本,再依赖内核文件里的kallsyms节获取函数的地址信息,细节上elf里存储的是压缩后的数据,它自己实现解析与符号查找能力;到目前为止它只找到了函数地址,而它在工作时还需要识别一些关键结构体成员偏移,对此它采用了设置与测试的方式,即备份值后调用相关函数修改值,看哪个偏移被修改了从而识别相关成员
2.HOOK机制:它自己实现了一个hook系统,支持指针hook(主要用在系统调用表上)和内联hook,这个系统支持在原始函数前和后hook,支持多重hook,它使用蹦床代码按优先级(默认是挂钩顺序)执行hook代码,执行过程中可修改参数/返回值,不调用原始函数等
3.内存管理:它的几乎所有功能都在内核完成,它对内核进行了较多修改,并且支持配置与模块加载等,因此它实现了自己的内存管理系统,而不依赖内核本身的内存子系统
4.超级调用:不像KernelSu直接扩展prctl的功能,它直接增加了一个叫做SuperCall的系统调用(调用号45),有权限的用户可以使用该调用完成多种功能,例如ROOT权限管理,KernelPatch管理,KPM模块管理等,同时它提供了用户态的库封装了其调用便于使用
static void before(hook_fargs6_t *args, void *udata)
{
// ...
if (!auth_superkey(key)) { // 认证superkey,先直接比较,失败会尝试hash(key)==root superkey
is_key_auth = 1;
} else if (!strcmp("su", key)) { // 如果是ru级,判断是否有权限
uid_t uid = current_uid();
if (!is_su_allow_uid(uid)) return;
} else {
return; // 否则返回,执行原始流程
}
args->ret = supercall(is_key_auth, cmd, a1, a2, a3, a4);
}
static long supercall(int is_key_auth, long cmd, long arg1, long arg2, long arg3, long arg4)
{
// 信息相关
case SUPERCALL_HELLO: // 直接返回魔数
case SUPERCALL_KLOG: // 打日志
case SUPERCALL_KERNELPATCH_VER:
case SUPERCALL_KERNEL_VER:
case SUPERCALL_BUILD_TIME:
// SU管理
case SUPERCALL_SU: // 为当前任务配置特权
case SUPERCALL_SU_TASK: // 为指定任务配置
case SUPERCALL_SU_GRANT_UID: // 为指定用户授权
case SUPERCALL_SU_REVOKE_UID: // 撤销su
case SUPERCALL_SU_NUMS: // 显示数量
case SUPERCALL_SU_LIST: // 列出
case SUPERCALL_SU_PROFILE: // 获取指定uid的特权配置 (e-uid和se context)
case SUPERCALL_SU_RESET_PATH: // 重置su路径
case SUPERCALL_SU_GET_PATH: // 获取su路径
case SUPERCALL_SU_GET_ALLOW_SCTX: // 获取全局的sctx
case SUPERCALL_SU_SET_ALLOW_SCTX: // 设置
// k storage
case SUPERCALL_KSTORAGE_READ:
case SUPERCALL_KSTORAGE_WRITE:
case SUPERCALL_KSTORAGE_LIST_IDS:
case SUPERCALL_KSTORAGE_REMOVE:
case SUPERCALL_SU_GET_SAFEMODE: // 是否是安全模式
// log
case SUPERCALL_BOOTLOG:
case SUPERCALL_PANIC:
case SUPERCALL_TEST:
if (!is_key_auth) return -EPERM; // 下面的东西就只有sk有权访问了
// SuperKey相关
case SUPERCALL_SKEY_GET:
case SUPERCALL_SKEY_SET:
case SUPERCALL_SKEY_ROOT_ENABLE:
// 模块相关
case SUPERCALL_KPM_LOAD:
case SUPERCALL_KPM_UNLOAD:
case SUPERCALL_KPM_CONTROL: // 模块控制,当前只实现了ctrl0
case SUPERCALL_KPM_NUMS:
case SUPERCALL_KPM_LIST:
case SUPERCALL_KPM_INFO:
}
5.认证系统:在ROOT权限外,它还自己在内核中实现了所谓SuperKey的一个认证系统,在使用超级调用时,需要传入一个key,它可以是su,此时内核会检查当前用户是否有root权限,有则执行一般特权操作,而超级调用还支持的如KernelPatch/KPM管理等功能则需要key为预设的SuperKey才允许调用
6.ROOT能力:在Linux下限制用户权限的方式有很多,如DAC、SELinux、Seccomp、Capabilities等,调用su是直接改DAC,将euid和egid等改为0,而前面提到的其它工具都是通过新增一个特权服务/修改SELinux 策略的方式来绕过第二个,KP换了种方式,它直接在内核中hook相关校验接口来绕过,Seccomp也是直接disable掉了
7.模块系统:上面提到了它支持开发自己的模块,它将这种模块叫做KernelPatch Module(KPM),它类似于内核模块,可在打补丁时直接嵌入到内核镜像中,也可以在运行时动态加载,它自己实现了一个模块解析与加载机制,利用这个模块就可以方便的在内核中为所欲为
8.存储系统:说实话,俺没看懂,好像就是一个内存中的键值对存储实现,用来保存配置信息的~
对它有个大概了解后就来看看它的工作流程吧
工作流程
在对内核打补丁后,它会直接劫持内核启动过程,在内核启动时跳转到KP的镜像处执行,具体来说它是静态替换了paging_init,将其重定向到镜像的起始位置:
// modify kernel entry
int paging_init_offset = get_symbol_offset_exit(&kallsym, kallsym_kimg, "paging_init");
setup->paging_init_offset = relo_branch_func(kallsym_kimg, paging_init_offset); // kallsym_kimg里
那是一段汇编代码,最终会跑到start里:
int __attribute__((section(".start.text"))) __noinline start(uint64_t kimage_voff, uint64_t linear_voff)
{
start_init(kimage_voff, linear_voff); // 初始化 物理地址与线性地址等
prot_myself(); // 设置自身的内存权限
restore_map(); // 恢复原始内核代码
log_regs(); // 输出寄存器信息
predata_init(); // 准备自身的一些数据
symbol_init(); // 符号解析,后续hook用
rc = patch(); // 打补丁
return rc;
}
它最后是去安装运行时的钩子,其实依然是跳板:
int patch()
{
linux_libs_symbol_init(); // 初始化 Linux 库符号表
linux_misc_symbol_init(); // 初始化 Linux 杂项符号表
module_init(); // 初始化模块子系统
syscall_init(); // 初始化系统调用接口
rc = hook_wrap12((void *)patch_config->panic, before_panic, 0, 0); // 在panic执行前执行kp的信息,调试用
// rest_init or cgroup_init
unsigned long init_addr = patch_config->rest_init || patch_config->cgroup_init
hook_wrap4((void *)init_addr, before_rest_init, 0, (void *)init_addr); // rest_init执行前会初始化核心功能
// kernel_init
unsigned long kernel_init_addr = patch_config->kernel_init;
hook_wrap4((void *)kernel_init_addr, before_kernel_init, after_kernel_init, 0); // kernel_init处主要是加载KPM
}
再接着,这里重点看:
static void before_rest_init(hook_fargs4_t *args, void *udata)
{
if ((rc = bypass_kcfi())) goto out; // 绕过cfi,不然指针hook会直接崩
if ((rc = resolve_struct())) goto out; // 解析关键结构,如task cred等结构体里关键信息的偏移
if ((rc = bypass_selinux())) goto out; // 绕过selinux,它直接hook avc_denied和slow_avc_audit,若用户有权限则直接使其失效
if ((rc = task_observer())) goto out; // hook任务创建时,它会在task的内核栈结束位置放一个task_ext保存额外的信息,如权限/profile等信息,便于认证
rc = supercall_install(); // 安装supercall
rc = kstorage_init(); // storage初始化
rc = su_compat_init(); // su兼容初始化,它拦截fstatat faccessat execve,当有权限的用户请求su时重定向到正确的位置 (安卓还会额外添加shell和root用户)
rc = resolve_pt_regs(); // 解析pt结构个域的偏移
rc = android_sepolicy_flags_fix(); // 这两个仅安卓的 拦截sepolicy写入,确保 android_netlink_route 和 android_netlink_getneigh 标志被正确设置
rc = android_user_init(); // hook execve/execveat/openat来监听用户空间初始化状态,注入rc脚本等,hook input_handle_event来探测音量键开安全模式
}
ok,KP大体就结束了,现在可以回到AP咯!
注:
- 符号信息分多种,
CONFIG_KALLSYMS=y只包含函数名,CONFIG_KALLSYMSALL=y额外包含全局变量等的信息,而-g是包含完整的调试信息,包括本地变量等,而在发行版的内核中通常只有第一个被激活- 其实个人感觉SuperKey是个君子协议,都有不受限的root权限了干啥不行,要么直接去内存偷Key或者直接Hook绕过...另外除了SuperKey它还有个Root SuperKey,这个东西是hash后保存的,如果忘了SuperKey还可以用它去认证并重置SuperKey
一笔带过的Native Hook
上面和hook相关的都是ART HOOK,而Native的HOOK有出现dobby,现在简单说下吧,安卓基于Linux用的ELF格式,所以常见两种native hook技术:
1.PLT HOOK:就是改函数表的方式,ELF对于外部符号默认使用懒加载(即使不启用也会有plt/got),pwn手对此很熟悉,plt hook就是直接改got表(所以为啥不叫got hook呢?🤔),它的优点是实现简单(也就很稳定安全咯),缺点就是能hook的函数很有限(只能是外部函数),且可能被绕过(dlsym直接调用)
2.Inline HOOK:这是所有二进制文件通用的hook方式,直接改指定位置的代码(通常是函数入口处),令其跳到hook代码处执行,同时备份原始指令便于调用原始函数或unhook,优点是通用啥都能钩,缺点是稳定性和安全性缺点儿
在安卓上常见的有前面出现过的dobby,字节的android-inline-hook (for inline)和bhook (for plt),And64InlineHook,whale等~
动态分析
JNI追踪
拦截原理
先说下jni基础,android可使用Linux标准的dlopen加载so,新版更常用功能更强大的android_dlopen_ext加载,加载后会使用dlsym去查找jni的函数。jni函数注册方式分两种,静态注册就是按特定规则命名Java_<package name>_<class name>_<method name>,而动态注册是在JNI_OnLoad中调用JNIEnv的RegisterNatives注册的,后者的jni函数可任意命名。在逆向一开始就会出现两个关键结构:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
if (vm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
jclass clazz = env->FindClass("com/example/dynamicregister/Test");
if (env->RegisterNatives(clazz, nativeMethods, sizeof(nativeMethods) / sizeof(nativeMethods[0])) != JNI_OK) {
return -1;
}
return JNI_VERSION_1_6;
}
JavaVM管理当前的Java虚拟机,而JNIEnv是线程相关的,用来具体提供JNI使用的各种功能,所以JNI追踪就是要先定位到JNI函数,并拦截。
注:有些骚操作可以使用更底层的接口去注册JNI,甚至直接操作ART注册,但同样的可直接获取artmethod去找到真实jni的地址
静态处理
通常都会先静态分析下,第一步导头文件不必多说,现在新的ida已经不需要这不了。第二步就是找出所有的JNI函数并且将参数类型改好,这部分工作可使用jni_helper,它使用androguard解析dex文件,获取所有jni函数(即native修饰的函数)的签名,使用pyelftools来定位对应的jni函数(当前只通过命名规则Java_xxx,还不支持动态注册的),将其匹配下来生成json数据库,再使用特定的逆向分析工具脚本将其应用,如ida pro的脚本加载json后自动为函数定义类型,算是给jni来个预处理!
Jnitrace分析
真正动态追踪害得看jnitrace,它能追踪对native方法的调用以及所有标准JNI方法的调用,不过它只是一个命令行封装,主要用于trace,而它的核心jnitrace-engine提供更强大的能力,我们可以直接调用API编写自己的拦截方法,下面说明后者的拦截原理:
1.它通过dlopen监控库加载,dlsym监控对Java_xxx和JNI_OnLoad的查找,监控库加载意义不大,因为现代android很少用dlopen了,作者没想勾android_dlopen_ext而是用dlsym去搂它,监控到对这两个的查找时看看处理过没以及要不要处理(黑名单优先级更高),若要则处理
2.处理方式是替换掉JavaVM和JNIEnv,前者通常是唯一的,而后者每线程一个,由前者管理。我们编写的JNI函数里对JVM的调用都要经过这两个对象,所以作者就分别创建了一个影子JavaVM和JNIEnv作为代理对象,影子对象的方法表变成了新的函数,该函数可在调用原函数之前和之后做一些用户定义的行为,通过替换第一/二个参数就替换了和JVM通信的通道。
这个库还有很多细节,例如内存管理,配置管理,参数输出(特别是变参处理),很值得学习!
# 安装
pip install jnitrace
# trace
jnitrace -l libnative.so -i "Find" -i "Get" -o trace.json com.example.app
jnitrace -l "*" -e "GetEnv" -e "^Find" com.example.app
系统调用追踪
stackplz
stackplz是个基于eBPF的追踪工具,它提供用户态函数调用追踪、系统调用追踪功能。
先看用户态函数追踪,该功能依赖于uprobe机制,虽然uprobe没用ptrace这个系统调用而是直接在内核里就把目标内存的代码改了,相对隐蔽,但修改的代码依然可以被扫内存的方式发现,另外uprobe会将指令存储在用户态指定内存区域,因此依然有多种被发现的方法,stackplz更进一步提供了硬件断点处理,通过perf子系统为指定进程设置硬件断点,eBPF去捕获断点事件并传给用户。
再看系统调用,它依赖于raw tracepint机制,它会捕获raw_tracepoint/sys_enter和raw_tracepoint/sys_exit的事件,而且它内置了常见系统调用的参数格式,便于格式化输出对应参数。
它的另一个亮点是完整的调用栈回溯,这是在另一个项目unwinddaemon里实现的,具体来说,在Linux下栈回溯常见有两种方式:基于栈帧指针和基于DWARF调试信息,像ARM/X86都有栈帧约定,如果遵守约定是能直接根据栈回溯出完整的调用栈的,再根据/proc/<pid>/maps和对应elf的符号信息即可完成符号解析,但如果使用了-fomit-frame-pointer标志编译则不会有帧,还好有另一种方式,就是dwarf,它存储了在每个位置应该怎么回溯栈的方法,因此解析elf的.eh_frame|.eh_frame_hdr也可实现,该库基于安卓的libunwindstack库做的扩展,支持离线(提前给上下文信息)或在线(远程去取上下文数据)去回溯栈,除了支持native的回溯也支持java的回溯,不过后者每次回溯需要暂停jvm。
指令级追踪
之前的文章有讲过frida,它的stalker可做到指令级追踪,但实际用起来效果并不好,相对来说QBDI是个更专注该目的的框架,它通过llvm重新指令(JIT)来实现每指令/块级的插桩,以及内存访问的插桩,这部分以后再详细介绍吧!