第九章 Android Hook框架
9.1 Xposed
Xposed
是一个Android Hook
框架,它可以实现在不修改APK
文件的情况下更改系统行为和应用程序的行为,通过开发模块,就能对目标进程的Java
函数调用进行Hook
拦截,但是需要安装在Root
的Android
设备上,才能使用该框架中的模块生效。根据该框架的原理衍生出了很多类似的框架,例如Edxposed
、Lsposed
、VirtualXposed
等等。
在Xposed
的架构中,主要包含了三个部分:Xposed Installer
、Xposed Bridge
和Xposed Module
。其中,Xposed Installer
是用户安装和管理Xposed
模块的应用程序;Xposed Bridge
是实现系统和模块之间相互通信的核心组件;Xposed Module
则是开发者使用Xposed API
编写的模块,用于实现对目标进程的函数调用的拦截和修改。
在运行时,Xposed Installer
会通过Android
的PackageManager
查询已安装的应用程序,并将相关信息传递给Xposed Bridge
。Xposed Bridge
会在运行过程中监听应用程序的启动事件,当目标应用程序启动时,Xposed Bridge
会将Xposed Module
加载到目标进程中,并且与Xposed Module
建立通信管道,以便进行后续的函数调用拦截和修改操作。
Xposed Module
通过实现IXposedHookLoadPackage
接口,来完成对应用程序的启动事件的监听和模块的加载。一旦模块加载成功,在IXposedHookLoadPackage
回调函数中,我们就可以使用Xposed API
提供的函数来实现对目标进程的函数调用的拦截和修改。这些函数包括XposedHelpers.findAndHookMethod()
和XposedHelpers.callMethod()
等,它们能够帮助我们定位到目标进程中的函数,并对其进行拦截和修改。
需要注意的是,Xposed
框架只能在Root
的Android
设备上使用,因为它需要对系统进行修改才能实现函数调用的拦截和修改。在使用Xposed
框架时,需要特别小心,不要随意地修改系统行为和应用程序行为,以免引起意外的后果。
这一章将详细解析Xposed
的原理,学习Xposed
是如何利用Android
的运行机制来实现对函数的Hook
机制。
9.2 Xposed实现原理
在开始分析Xposed
源码前,首先回顾一下第三章中,讲解Android
启动流程时,最后根据AOSP
的源码得到的以下结论。
1. `zygote`进程启动是通过`app_process`执行程序启动的
2. 由`init`进程解析`init.rc`时启动的第一个`zygote`
3. 在第一个`zygote`进程中创建的`ZygoteServer`,并开始监听消息。
4. `zygote`是在`ZygoteServer`这个服务中收到消息后,再去`fork`出新进程的
5. 所有进程均来自于`zygote`进程的`fork`而来,所以`zygote`是进程的始祖
从上面的结论中可以看到,app_process
执行程序在其中占据着非常重要的位置,而Xposed
的核心原理,就是将app_process
替换为Xposed
修改过的app_process
,这样就会让所有进程都会通过它的业务逻辑处理。首先找到项目https://github.com/rovo89/Xposed
。查看文件Android.mk
。
ifeq (1,$(strip $(shell expr $(PLATFORM_SDK_VERSION) \>= 21)))
LOCAL_SRC_FILES := app_main2.cpp
LOCAL_MULTILIB := both
LOCAL_MODULE_STEM_32 := app_process32_xposed
LOCAL_MODULE_STEM_64 := app_process64_xposed
else
LOCAL_SRC_FILES := app_main.cpp
LOCAL_MODULE_STEM := app_process_xposed
endif
可以看到这里是用来编译一个Xposed
专用的app_process
。当Android
版本大于21(Android 5)时,使用app_main2.cpp
来编译。接下来查看入口函数的实现。
#define XPOSED_CLASS_DOTS_TOOLS "de.robv.android.xposed.XposedBridge$ToolEntryPoint"
int main(int argc, char* const argv[])
{
...
// 检测Xposed的参数
if (xposed::handleOptions(argc, argv)) {
return 0;
}
...
if (zygote) {
// Xposed 框架的初始化,为后续的 Hook 操作和代码注入操作提供支持。
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);
runtimeStart(runtime, isXposedLoaded ? XPOSED_CLASS_DOTS_TOOLS : "com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.\n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
return 10;
}
}
在这个特殊的app_process
中,首先是对启动进程的参数进行检查,然后初始化Xposed
框架,如果初始化成功了,则使用Xposed
的入口de.robv.android.xposed.XposedBridge$ToolEntryPoint
来替换系统原本的com.android.internal.os.ZygoteInit
入口。
xposed::initialize
是一个非常关键的函数,它完成了 Xposed
框架的初始化工作。查看实现代码如下。
bool initialize(bool zygote, bool startSystemServer, const char* className, int argc, char* const argv[]) {
#if !defined(XPOSED_ENABLE_FOR_TOOLS)
...
// 将参数保存
xposed->zygote = zygote;
xposed->startSystemServer = startSystemServer;
xposed->startClassName = className;
xposed->xposedVersionInt = xposedVersionInt;
#if XPOSED_WITH_SELINUX
xposed->isSELinuxEnabled = is_selinux_enabled() == 1;
xposed->isSELinuxEnforcing = xposed->isSELinuxEnabled && security_getenforce() == 1;
#else
xposed->isSELinuxEnabled = false;
xposed->isSELinuxEnforcing = false;
#endif // XPOSED_WITH_SELINUX
...
if (startSystemServer) {
if (!determineXposedInstallerUidGid() || !xposed::service::startAll()) {
return false;
}
// 启动Xposed框架的日志记录,将xposed框架日志写入logcat中。
xposed::logcat::start();
// SELinux启用的情况
#if XPOSED_WITH_SELINUX
} else if (xposed->isSELinuxEnabled) {
// 用于启动Xposed框架的 membased 服务,该服务实现hooking功能
if (!xposed::service::startMembased()) {
return false;
}
#endif // XPOSED_WITH_SELINUX
}
// SELinux启用的情况
#if XPOSED_WITH_SELINUX
// 限制内存继承,以确保Xposed服务只能被当前进程和其子进程使用,而不能被其他进程使用
if (xposed->isSELinuxEnabled) {
xposed::service::membased::restrictMemoryInheritance();
}
#endif // XPOSED_WITH_SELINUX
// 是否禁用xposed
if (zygote && !isSafemodeDisabled() && detectSafemodeTrigger(shouldSkipSafemodeDelay()))
disableXposed();
if (isDisabled() || (!zygote && shouldIgnoreCommand(argc, argv)))
return false;
// 将Xposed JAR文件添加到应用程序或服务的类路径中
return addJarToClasspath();
}
在启用SELinux
的情况下,Xposed
需要使用 membased
服务来实现hooking
功能。但是,为了确保安全性,Xposed
需要限制将Xposed
服务复制到其他进程中的能力。通过调用 restrictMemoryInheritance
函数,Xposed
会防止任何进程继承Zygote
进程的内存,这将确保Xposed
服务只能被当前进程和其子进程使用。
初始化完成时,将一个JAR
文件添加到了CLASSPATH
环境变量中,查看addJarToClasspath
的实现。
#define XPOSED_JAR "/system/framework/XposedBridge.jar"
bool addJarToClasspath() {
ALOGI("-----------------");
if (access(XPOSED_JAR, R_OK) == 0) {
if (!addPathToEnv("CLASSPATH", XPOSED_JAR))
return false;
ALOGI("Added Xposed (%s) to CLASSPATH", XPOSED_JAR);
return true;
} else {
ALOGE("ERROR: Could not access Xposed jar '%s'", XPOSED_JAR);
return false;
}
}
初始化成功后,接着继续追踪替换后的入口点de.robv.android.xposed.XposedBridge$ToolEntryPoint
,该入口点的实现是在XposedBridge.jar
中。查看项目https://github.com/rovo89/XposedBridge
,文件XposedBridge.java
的实现代码如下。
package de.robv.android.xposed;
public final class XposedBridge {
protected static final class ToolEntryPoint {
protected static void main(String[] args) {
isZygote = false;
XposedBridge.main(args);
}
}
protected static void main(String[] args) {
// 初始化Xposed框架和模块
try {
if (!hadInitErrors()) {
initXResources();
SELinuxHelper.initOnce();
SELinuxHelper.initForProcess(null);
runtime = getRuntime();
XPOSED_BRIDGE_VERSION = getXposedVersion();
if (isZygote) {
// hook Android 资源系统
XposedInit.hookResources();
// 初始化 Xposed 框架的 zygote 进程,创建用于跨进程通信的 Binder 对象,并注册相关的 Service。这样就能够实现跨进程的 Hook 功能
XposedInit.initForZygote();
}
// 加载Xposed模块
XposedInit.loadModules();
} else {
Log.e(TAG, "Not initializing Xposed because of previous errors");
}
} catch (Throwable t) {
Log.e(TAG, "Errors during Xposed initialization", t);
disableHooks = true;
}
// 调用原始应用的入口
if (isZygote) {
ZygoteInit.main(args);
} else {
RuntimeInit.main(args);
}
}
}
到这里,Xposed
的启动流程基本完成了,Xposed
首先替换原始的app_process
,让每个进程启动时使用自己的app_process_xposed
,在执行zygote
入口函数前,先初始化了自身的环境,然后每个进程后是先进入的XposedBridge
,在完成自身的逻辑后,才调用zygote
的入口函数,进入应用正常启动流程。这也意味着,对于系统定制者来说,所谓的Root
权限才能使用Xposed
并不是必须的。最后看看loadModules
的实现,是如何加载Xposed
模块的。
private static final String INSTANT_RUN_CLASS = "com.android.tools.fd.runtime.BootstrapApplication";
// 加载模块列表
static void loadModules() throws IOException {
final String filename = BASE_DIR + "conf/modules.list";
BaseService service = SELinuxHelper.getAppDataFileService();
if (!service.checkFileExists(filename)) {
Log.e(TAG, "Cannot load any modules because " + filename + " was not found");
return;
}
// 拿到顶端的ClassLoader
ClassLoader topClassLoader = XposedBridge.BOOTCLASSLOADER;
ClassLoader parent;
while ((parent = topClassLoader.getParent()) != null) {
topClassLoader = parent;
}
// 读取模块列表
InputStream stream = service.getFileInputStream(filename);
BufferedReader apks = new BufferedReader(new InputStreamReader(stream));
String apk;
// 使用顶端ClassLoader加载每个模块
while ((apk = apks.readLine()) != null) {
loadModule(apk, topClassLoader);
}
apks.close();
}
private static void loadModule(String apk, ClassLoader topClassLoader) {
Log.i(TAG, "Loading modules from " + apk);
if (!new File(apk).exists()) {
Log.e(TAG, " File does not exist");
return;
}
DexFile dexFile;
try {
dexFile = new DexFile(apk);
} catch (IOException e) {
Log.e(TAG, " Cannot load module", e);
return;
}
// 如果加载成功,说明该应用启用了 Instant Run
if (dexFile.loadClass(INSTANT_RUN_CLASS, topClassLoader) != null) {
Log.e(TAG, " Cannot load module, please disable \"Instant Run\" in Android Studio.");
closeSilently(dexFile);
return;
}
// 尝试在目标模块中加载XposedBridge类,可以获取到说明已经成功注入XposedBridge
if (dexFile.loadClass(XposedBridge.class.getName(), topClassLoader) != null) {
Log.e(TAG, " Cannot load module:");
Log.e(TAG, " The Xposed API classes are compiled into the module's APK.");
Log.e(TAG, " This may cause strange issues and must be fixed by the module developer.");
Log.e(TAG, " For details, see: http://api.xposed.info/using.html");
closeSilently(dexFile);
return;
}
closeSilently(dexFile);
// 由于模块实际都是apk,而apk本质是压缩包,所以使用Zip来处理文件
ZipFile zipFile = null;
InputStream is;
try {
zipFile = new ZipFile(apk);
// 解压出xposed_init文件,这里存放着模块启动的入口
ZipEntry zipEntry = zipFile.getEntry("assets/xposed_init");
if (zipEntry == null) {
Log.e(TAG, " assets/xposed_init not found in the APK");
closeSilently(zipFile);
return;
}
is = zipFile.getInputStream(zipEntry);
} catch (IOException e) {
Log.e(TAG, " Cannot read assets/xposed_init in the APK", e);
closeSilently(zipFile);
return;
}
// 动态加载模块
ClassLoader mcl = new PathClassLoader(apk, XposedBridge.BOOTCLASSLOADER);
BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is));
try {
String moduleClassName;
while ((moduleClassName = moduleClassesReader.readLine()) != null) {
moduleClassName = moduleClassName.trim();
if (moduleClassName.isEmpty() || moduleClassName.startsWith("#"))
continue;
try {
// 加载模块的入口类
Log.i(TAG, " Loading class " + moduleClassName);
Class<?> moduleClass = mcl.loadClass(moduleClassName);
// 检查该类是否有实现接口
if (!IXposedMod.class.isAssignableFrom(moduleClass)) {
Log.e(TAG, " This class doesn't implement any sub-interface of IXposedMod, skipping it");
continue;
} else if (disableResources && IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) {
Log.e(TAG, " This class requires resource-related hooks (which are disabled), skipping it.");
continue;
}
// 使用该类初始化一个对象
final Object moduleInstance = moduleClass.newInstance();
if (XposedBridge.isZygote) {
// 不同的实现接口有各自对应的处理,这里是Zygote模块初始化时使用的模块
if (moduleInstance instanceof IXposedHookZygoteInit) {
IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam();
param.modulePath = apk;
param.startsSystemServer = startsSystemServer;
((IXposedHookZygoteInit) moduleInstance).initZygote(param);
}
// 普通应用的模块接口
if (moduleInstance instanceof IXposedHookLoadPackage)
// 调用了模块中的实现。
XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));
if (moduleInstance instanceof IXposedHookInitPackageResources)
XposedBridge.hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance));
} else {
if (moduleInstance instanceof IXposedHookCmdInit) {
IXposedHookCmdInit.StartupParam param = new IXposedHookCmdInit.StartupParam();
param.modulePath = apk;
param.startClassName = startClassName;
((IXposedHookCmdInit) moduleInstance).initCmdApp(param);
}
}
} catch (Throwable t) {
Log.e(TAG, " Failed to load class " + moduleClassName, t);
}
}
} catch (IOException e) {
Log.e(TAG, " Failed to load module from " + apk, e);
} finally {
closeSilently(is);
closeSilently(zipFile);
}
}
分析完加载模块的实现后,这时就明白模块开发时定义的入口是如何被调用的,以及被调用的时机在哪里。理解其中的原理后,同样可以自己进行修改,在其他的时机来选择注入。用自己的方式来定义模块。
9.3 常见的hook框架
根据Xposed
的源码分析不难看出其关键在于XposedBridge.jar
的注入,然后由XposedBridge.jar
实现对函数Hook
的关键逻辑,因为Xposed
框架提供了非常方便和灵活的API
,使得开发者可以快速地编写自己的Hook
模块并且可以兼容大多数Android
系统版本和设备。所以很多Hook
框架都会兼容支持Xposed
框架。
SandHook
是作用在Android ART
虚拟机上的Java
层 Hook
框架,作用于进程内是不需要Root
的,支持Android 4.4 - Android 10
,该框架兼容Xposed Api
调用。
除了支持常规的Java
层Hook
外,Sandhook
还支持对Native
层的函数进行Hook
。它通过使用系统提供的符号表来获取函数地址,并将函数地址转换为可执行代码,从而实现Native Hook
。
Sandhook
本身是没有注入功能的,开发完模块功能后,需要自行重打包,或者使用其他工具将模块注入。从开发AOSP
的角度,可以参考前文内置JAR
包的做法,直接将Sandhook
内置到AOSP
系统中,并实现对任意进程自动注入。
pine
是一个在虚拟机层面、以Java
方法为粒度的运行时动态Hook
框架,它可以拦截本进程内几乎所有的java
方法调用。支持Android 4.4 - Android 12
。同样该框架也兼容Xposed Api
调用。
Pine
支持两种方案,一种是替换入口,即修改ArtMethod
的entrypoint
;另一种类似于native
的inline hook
,即覆盖掉目标方法的代码开始处的一段代码,用于弥补Android 8.0
以下版本入口替换很有可能不生效的问题。
Dobby
是一个基于Android NDK
开发的Native Hook
框架。它可以在Android
应用程序中注入自定义代码段,从而实现函数替换、跳转、插桩等操作。Dobby
主要使用了动态链接库和指令重写技术,通过Hook目标进程中的函数来达到修改目的。
相比Java
层的Hook
框架,Native Hook
有一些优势。首先,Native Hook
可以直接操作目标进程的内存空间,更加灵活;其次,Native Hook
可以通过指令重写技术来控制执行流程,效果更加精准;最后,Native Hook
避免了Java
层Hook
可能引起的兼容性问题,适用范围更广。
9.4 集成pine
其实集成各种Hook
框架的方式基本大同小异,主要就是将核心JAR
文件或者依赖的so
动态库内置到系统中,在进程启动阶段将其注入,注入时机越早,能支持Hook
的范围自然是越广,在注入后,再对模块进行动态加载即可。在前几章中,有详细的讲解如何内置JAR
文件和so
动态库,以及如何动态加载调用,在这一小节中,将会结合前文中学习到的,完整把pine hook
框架内置到AOSP12
中。
首先需要知道pine
的模块需要依赖哪些动态库,按照pine
模块的开发规则,Android Studio
新建项目,在build.gradle
下添加pine
的引用如下。
dependencies {
...
implementation 'top.canyie.pine:core:0.2.6'
...
}
然后添加一个测试`hook` 的目标类和函数。
public class Demo {
public static String ceshi(){
Log.i("Demo","ceshi");
return "1123";
}
}
接着在onCreate
中添加hook
代码如下。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PineConfig.debug = true; // 是否debug,true会输出较详细log
PineConfig.debuggable = BuildConfig.DEBUG; // 该应用是否可调试
try {
Pine.hook(Demo.class.getDeclaredMethod("ceshi"), new MethodHook() {
@Override public void beforeCall(Pine.CallFrame callFrame) {
Log.i(TAG, "Before " + callFrame.thisObject + " ceshi()");
}
@Override public void afterCall(Pine.CallFrame callFrame) {
Log.i(TAG, "After " + callFrame.thisObject + " ceshi()");
callFrame.setResult("aasd");
}
});
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
Log.i(TAG,Demo.ceshi());
}
可以看到hook
代码执行后,再触发函数的调用,运行该应用后,能看到在本进程内成功hook
。说明该模块正常运行,将这个测试模块编译出来的apk
文件解压,查看lib
目录,发现hook
框架添加后,新增了动态库libpine.so
。接下来需要将该动态库内置到系统中。
在目录frameworks/base/packages/apps
下新建一个目录mypine
,然后在该目录中新建文件Android.mk
,将pine
的依赖动态库libpine.so
的,armv7
以及arm64
两个版本拷贝到该目录,并加入配置,配置具体内容如下。
//内容如下
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libpine
LOCAL_SRC_FILES_arm := libpine.so
LOCAL_SRC_FILES_arm64 := libpine_arm64.so
LOCAL_MODULE_TARGET_ARCHS:= arm arm64
LOCAL_MULTILIB := both
LOCAL_MODULE_SUFFIX := .so
LOCAL_MODULE_CLASS := SHARED_LIBRARIES
LOCAL_MODULE_TAGS := optional
LOCAL_SHARED_LIBRARIES := liblog
include $(BUILD_PREBUILT)
然后在build/make/target/product/mainline_system.mk
文件中,将配置好的模块加入PRODUCT_PACKAGES
中,具体实现如下。
PRODUCT_PACKAGES += \
libpine \
依赖的动态库成功内置到了系统中,只需要在应用启动的过程中,将开发的模块动态加载进去即可,模块的开发可以直接参考Xposed
实现的思路,在Xposed
中定义了接口IXposedHookLoadPackage
,然后开发模块时,实现该接口中的入口函数handleLoadPackage
,在进程启动中,动态加载模块后,就调用实现了该接口的函数即可触发模块的入口函数。
参考上面的流程,开发一个要注入的模块,首先创建一个接口文件如下,包名随意,但是要注意的是,模块中的接口包名,必须和AOSP
系统中添加的接口包名一致。
package java.krom;
public interface IHook {
void onStart(Object app);
}
然后创建一个类,实现该接口,并在入口函数中实现需要hook
内容。
package cn.mik.mymodule
public class Module implements IHook {
public static Method GetClsMethod(Class cls,String methodName){
Method methlist[] = cls.getDeclaredMethods();
Method mGoal=null;
for (int i = 0; i < methlist.length; i++) {
Method m = methlist[i];
if(m.getName().equals(methodName)){
mGoal=m;
break;
}
}
return mGoal;
}
@Override
public void onStart(Object app) {
Log.i("dengrui", "Module is running...");
Application application=(Application)app;
ClassLoader classLoader=application.getClassLoader();
try {
Class cls=Class.forName("cn.mik.pinedemo.Demo",false,classLoader);
if(cls==null){
Log.i(TAG, "not found Demo");
return;
}
Method method=GetClsMethod(cls,"ceshi");
if(method!=null){
Log.i(TAG, "success get method");
Pine.hook(method, new MethodHook() {
@Override public void beforeCall(Pine.CallFrame callFrame) {
Toast.makeText(application, "成功注入模块",Toast.LENGTH_LONG).show();
}
@Override public void afterCall(Pine.CallFrame callFrame) {
}
});
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
Log.i(TAG, "err:"+e.getMessage());
}
}
}
在该模块中依然是对前面的例子进行Hook
,而前文是直接在本进程中进行Hook
操作,现在则是将前面例子中,onCreate
的hook
代码删除,并且去掉pine
框架的相关引用。在该进程启动时,在AOSP
源码中将其注入。这里的注入时机选择handleBindApplication
中,创建Application
后进行处理。下面是AOSP
的相关修改。
private void loadModule(Application app){
String apkPath="/data/data/cn.mik.pinedemo/mymodule.apk";
String apkClass="cn.mik.mymodule.Module"
File f=new File(apkPath);
if(!f.exists()){
return;
}
ClassLoader mcl = new PathClassLoader(apkPath, app.getClassLoader());
IHook moduleInstance = null;
try {
Log.i(TAG, "Loading class " + apkClass);
Class<?> moduleClass = mcl.loadClass(apkClass);
moduleInstance = (IHook) moduleClass.newInstance();
} catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
Log.e(TAG, "", e);
} finally {
}
if (moduleInstance != null) {
moduleInstance.onStart(app);
}
}
private void handleBindApplication(AppBindData data) {
...
app = data.info.makeApplication(data.restrictedBackupMode, null);
// Propagate autofill compat state
app.setAutofillOptions(data.autofillOptions);
// Propagate Content Capture options
app.setContentCaptureOptions(data.contentCaptureOptions);
sendMessage(H.SET_CONTENT_CAPTURE_OPTIONS_CALLBACK, data.appInfo.packageName);
mInitialApplication = app;
// 非系统进程则注入jar包
int flags = mBoundApplication == null ? 0 : mBoundApplication.appInfo.flags;
if(flags>0&&((flags&ApplicationInfo.FLAG_SYSTEM)!=1)){
loadModule(app)
}
}
注入代码添加完成后,需要在AOSP
中相同包名目录下也添加IHook.java
的接口文件。该例子中接口文件存放在openjdk
,也可以选择直接放android.app
包名或任意包名下,只需要和模块中的一致即可。
在该例子中,为了简化过程,模块路径以及模块实现接口的类名固定写在代码中,所以在刷入手机测试时,需要手动将该模块上传到指定路径,并且保证在该目录有权限,才能进行动态加载。
在实际运用常见,可以选择参考Xposed
的做法,写在某个资源文件中,然后解压出单个文件读取内容获取到。而apk
的路径,可以选择从配置文件获取,如果配置路径下的没有权限,可以由代码实现将模块拷贝到当前进程的私有目录下进行动态加载。也可以选择调整selinux
规则,为指定目录添加普通进程的访问权限。
9.5 集成dobby
集成方式与pine
相同,首先开发一个使用dobby
的样例,然后将其中的依赖动态库集成到系统中,最后在进程启动的过程中,将其加载即可。由于dobby
是对native
函数进行hook
的,所以Android Studio
创建一个native c++
的项目,然后使用git
将dobby
项目拉取下来。项目地址:https://github.com/jmpews/Dobby
。然后修改项目中cpp
目录下的CMakeLists.txt
文件,将dobby
加入其中。修改如下。
cmake_minimum_required(VERSION 3.18.1)
// 设置dobby源码的目录
set(DobbyHome ~/git_src/Dobby)
enable_language(C ASM)
include_directories(
dlfc
utils
)
project("mydobby")
add_library(
mydobby
SHARED
native-lib.cpp
utils/parse.cpp)
find_library(
log-lib
log)
target_link_libraries(
mydobby
dobby
${log-lib})
# 使用设置的路径,引入Dobby
include_directories(
${DobbyHome}/include
${DobbyHome}/source
${DobbyHome}/builtin-plugin
${DobbyHome}/builtin-plugin/AndroidRestriction
${DobbyHome}/builtin-plugin/SymbolResolver
${DobbyHome}/external/logging
)
macro(SET_OPTION option value)
set(${option} ${value} CACHE INTERNAL "" FORCE)
endmacro()
SET_OPTION(DOBBY_DEBUG ON)
SET_OPTION(DOBBY_GENERATE_SHARED ON)
SET_OPTION(Plugin.LinkerLoadCallback OFF)
add_subdirectory(~/git_src/Dobby dobby.build)
if(${CMAKE_ANDROID_ARCH_ABI} STREQUAL "arm64-v8a")
add_definitions(-DCORE_SO_NAME="${LIBRARY_NAME}")
elseif(${CMAKE_ANDROID_ARCH_ABI} STREQUAL "armeabi-v7a")
add_definitions(-DCORE_SO_NAME="${LIBRARY_NAME}")
endif()
将dobby
的源码引入后,就可以在项目中使用dobby
进行hook
处理了。修改native-lib.cpp
文件,添加测试的hook
代码,内容如下。
#include <jni.h>
#include <string>
#include <android/log.h>
#include "dobby.h"
#define LOG_TAG "native-lib"
#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG , LOG_TAG, __VA_ARGS__)
int (*source_openat)(int fd, const char *path, int oflag, int mode) = nullptr;
// 替换后的新函数
int MyOpenAt(int fd, const char *pathname, int flags, int mode) {
ALOGD("mik MyOpenAt pathname :%s",pathname);
if (strcmp(pathname, "/sbin/su") == 0 || strcmp(pathname, "/system/bin/su") == 0) {
pathname = "/system/xbin/Mysu";
}
// 执行原来的openat函数
return source_openat(fd, pathname, flags, mode);
}
void HookOpenAt() {
// 找到函数对应的地址
void *__openat =
DobbySymbolResolver("libc.so", "__openat");
if (__openat == nullptr) {
ALOGD("__openat null ");
return;
}
ALOGD("拿到 __openat 地址 ");
//dobby hook 函数
if (DobbyHook((void *) __openat,
(dobby_dummy_func_t) MyOpenAt,
(dobby_dummy_func_t*) &source_openat) == RT_SUCCESS) {
ALOGD("DobbyHook __openat sucess");
}
}
jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
ALOGD("Hello JNI_OnLoad 开始加载");
JNIEnv *env = nullptr;
//改变openat 指定函数 函数地址 替换成自己的
HookOpenAt();
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) == JNI_OK) {
return JNI_VERSION_1_6;
}
return 0;
}
样例应用准备完毕,将该样例编译并运行后,就能成功看到对openat
进行hook
的输出如下。
...
D mik MyOpenAt pathname :/data/vendor/gpu/esx_config_cn.mik.devchangemodule.txt
D mik MyOpenAt pathname :/data/vendor/gpu/esx_config.txt
D mik MyOpenAt pathname :/data/misc/gpu/esx_config_cn.mik.devchangemodule.txt
D mik MyOpenAt pathname :/data/misc/gpu/esx_config.txt
D mik MyOpenAt pathname :/data/vendor/gpu/esx_config_cn.mik.devchangemodule.txt
D mik MyOpenAt pathname :/data/vendor/gpu/esx_config.txt
D mik MyOpenAt pathname :/data/misc/gpu/esx_config_cn.mik.devchangemodule.txt
D mik MyOpenAt pathname :/data/misc/gpu/esx_config.txt
...
接下来将该样例应用编译的apk
文件进行解压,在lib
目录中找到依赖的动态库,分别是libdobby.so
和libmydobby.so
,其中前者是hook
框架的核心库,后者是刚刚对openat
进行hook
的业务代码。只需要在任何进程启动前,按顺序将依赖的核心动态库,和业务代码加载,即可完成集成的工作,libdobby.so
可以选择集成到系统中,也可以选择跟业务代码动态库一起放同一个目录进行加载。下面看实现加载的代码。
private static void loadSoModule(String soName){
String soPath="";
if(System.getProperty("os.arch").indexOf("64") >= 0) {
soPath = String.format("/data/data/cn.mik.dobbydemo/%s", soName);
}else{
soPath = String.format("/data/data/cn.mik.dobbydemo/%s", soName);
}
File file = new File(soPath);
if (file.exists()){
Log.e("mikrom", "load so "+soPath);
System.load(tmpPath);
Log.e("mikrom", "load over so "+soPath);
}else{
Log.e("mikrom", "load so "+soPath+" not exist");
}
}
private void handleBindApplication(AppBindData data) {
...
app = data.info.makeApplication(data.restrictedBackupMode, null);
// Propagate autofill compat state
app.setAutofillOptions(data.autofillOptions);
// Propagate Content Capture options
app.setContentCaptureOptions(data.contentCaptureOptions);
sendMessage(H.SET_CONTENT_CAPTURE_OPTIONS_CALLBACK, data.appInfo.packageName);
mInitialApplication = app;
// 非系统进程则注入jar包
int flags = mBoundApplication == null ? 0 : mBoundApplication.appInfo.flags;
if(flags>0&&((flags&ApplicationInfo.FLAG_SYSTEM)!=1)){
loadSoModule("libdobby.so");
loadSoModule("libmydobby.so");
}
}
这只是简单的演示加载样例,安装目标应用后,还需要把两个动态库拷贝到对应目录中,在实际运用场景,尽量不要将动态库的路径,以及要加载的库名称固定写在源码中,最好通过配置的方式,来管理这些需要加载的参数,加载动态库需要目录有执行权限,所以要将文件放在当前应用的私有目录中。完成修改后,随意安装任何应用,打开后,都会被hook openat
函数。