一 Lsposed 技术原理探讨 && 基本安装使用
目前市场上主流的Hook框架有两款,一个是Frida,另一个是Xposed。他们之间各有优缺点,简单总结来说:Frida快,但是不稳定;Xposed稳定,但是操作繁琐,减缓了分析的操作的速度。
1.1 Xposed && Lsposed
1.1.1 Xposed 系列工具发展历程
本章,先对Xposed展开讲解。那Lsposed是什么呢?和xposed有什么联系呢?既然大家能读到这本书,相信大家的技术水平已经不是停留在小白的水平了。这里只讲述核心的知识点。
对于Xposed、Edposed、Lsposed,非常有必要说明下它们的发展历程,方便于读者的理解。Xposed是早期的Hook框架,并且有成熟的社区以及API来支撑,但是它的作者在2017年就停止了项目的维护,如图1-1所示,Github的上Xposed的最新版本是v89,这个是后来增加的,最稳定的新版是v82版本,我们现在的框架依赖依然使用v82的版本。
图1-1 Xposed API
现对于2017年,已经过去5年的时间。技术是在不停的迭代升级的,虽然Xposed还能是使用,但是它本身繁琐的操作,每次编写完Hook代码后,需要重启手机,这样大大的减缓了分析的过程,并且浪费我们的生命。由于技术的升级,Xposed的特征也越来越多,被反调成了常有的事情。Xposed的作者对Xposed停止维护后,此框架依然起着很大的作用,后来出现了Edposed,并接管了Xposed的位置。但是Edposed的存在期间很短,框架本身也有很多弊病。于是,对于Edposed的改良框架Lspoded脱颖而出。Edposed我们在后面也不会提及,因为它只是一个过渡版本。
Lsposed是在Edposed的基础上进行改良的新框架。并且接管了Xposed的API,可以很好的兼容Xposed的API。所以我们后面的开发工作都是基于Xposed的API进行开发,再配合上Lsposed的优秀特性,体验感十分良好。
对于Xposed的弊端,这里有必要说明一下:Xposed会对所有的应用都进行注入,也就是全局模式,导致应用启动变得非常的慢,这个在Lsposed上有了很大的改良。在Lsposed上,我们可以对目标app选择注入,并且支持多选。这项改进也不算是重大的技术升级,说到底就是引导用户正确的使用Xposed,确保Xposed框架和模块不会做额外的事情。
图1-2 Lsposed github official declare
图1-2是Lsposed的官方声明,下面是对介绍的翻译:
Riru / Zygisk 模块试图提供一个 ART Hook 框架,该框架利用 LSPlant 挂钩框架提供与 OG Xposed 一致的 API。
Xposed 是一个模块框架,可以在不触及任何 APK 的情况下改变系统和应用程序的行为。这很棒,因为这意味着模块可以在不同版本甚至 ROM 上工作而无需任何更改(只要原始代码没有太大更改)。它也很容易撤消。由于所有更改都在内存中完成,您只需停用模块并重新启动即可恢复原始系统。还有许多其他优点,但这里只是一个优点:多个模块可以对系统或应用程序的同一部分进行更改。对于修改后的 APK,您必须选择一个。没有办法组合它们,除非作者用不同的组合构建了多个 APK。
从图1-2中还能看到它支持Android 8.1-13 的系统版本。补充说明下,Xposed旧版的API只支持的Android7,后来更新的出来的一些版本,如v89,是支持Android 8的版本的。
1.1.2 Xposed && Lsposed 框架原理
Xposed的Hook原理是从整个Android的启动流程入手而设计出来的框架,看懂Android的启动流程,也是从从按了开机键后,从硬件到软件,到底做了什么事情,我们才能更好的理解Xposed框架。
1.1.2.1 Android 启动流程
如图1-3所示:是Android启动的整个流程图,下面我们一一对每个节点进行介绍。
图1-3 Android 启动流程图
Android整系统分为四层,分别为kernel、Native、FrameWork、应用层(APP),loader也可以单独算一层,是硬件的启动加载预置项。
首先当我们长按开机键(电源按钮)开机,此时会引导芯片开始从固化到ROM中的预设代码处执行,然后加载引导程序到RAM。然后启动加载的引导程序,引导程序主要做一些基本的检查,包括RAM的检查,初始化硬件的参数。
到达内核层的流程后,这里初始化一些进程管理、内存管理、加载各种Driver等相关操作,如Camera Driver、Binder Driver 等。下一步就是内核线程,如软中断线程、内核守护线程。下面一层就是Native层,这里额外提一点知识,层于层之间是不可以直接通信的,所以需要一种中间状态来通信。Native层和Kernel层之间通信用的是syscall,Native层和Java层之间的通信是JNI。
在Native层会初始化init进程,也就是用户组进程的祖先进程。init中加载配置文件init.rc,init.rc中孵化出ueventd、logd、healthd、installd、lmkd等用户守护进程。开机动画启动等操作。核心的一步是孵化出Zygote进程,此进程是所有APP的父进程,这也是Xposed注入的核心,同时也是Android的第一个Java进程(虚拟机进程)。
进入框架层后,加载zygote init类,注册zygote socket套接字,通过此套接字来做进程通信,并加载虚拟机、类、系统资源等。zygote第一个孵化的进程是system_server进程,负责启动和管理整个Java Framework,包含ActivityManager、PowerManager等服务。
应用层的所有APP都是从zygote孵化而来
1.1.2.2 Xposed 注入源码剖析
上述是Android的大致流程,接下来我们从代码执行的角度来看执行链。
从图1-3中,我们可以总结出一条启动链:
1
|
init
=
> init.rc
=
> app_process
=
> zygote
=
> ...
|
一个应用的启动,核心的步骤是Framework层的zygote启动,zygote是所有进程的父进程,也就是说,所有APP应用进程都是由zygote孵化而来。为什么要从Native层开始说明呢?这是因为Native层开始就是Android源码的运行过程,Xposed的注入也就是从Native层开始的。
根据Android启动流程图可知,zygote是由app_process初始化产生的。app_process是一个二进制可执行文件,它的表现形式是一个bin文件,它位于/system/bin/app_process
,如图1-4所示:
图1-4 Android app_process
app_process是由app_main.cpp编译而来,它的源码路径位于/frameworks/base/cmds/app_process/app_main.cpp
,Xposed就是将XposedInstaller.jar包替换原始的app_process来实现全局注入,所以我们每次编写完Hook代码后,需要重启手机才能生效。
从Android启动的流程图可知,首先进行的是init进程的启动,它在源码中对应着/system/core/init/init.cpp
文件,如图1-5所示:
图1-5 /system/core/init/init.cpp
C++文件的启动从main开始,所以我们从main开始追,核心代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
int
main(
int
argc, char
*
*
argv) {
...
if
(bootscript.empty()) {
/
/
第一次开机
parser.ParseConfig(
"/init.rc"
);
/
/
解析配置文件 init.rc
parser.set_is_system_etc_init_loaded(
parser.ParseConfig(
"/system/etc/init"
));
parser.set_is_vendor_etc_init_loaded(
parser.ParseConfig(
"/vendor/etc/init"
));
parser.set_is_odm_etc_init_loaded(parser.ParseConfig(
"/odm/etc/init"
));
}
else
{
parser.ParseConfig(bootscript);
parser.set_is_system_etc_init_loaded(true);
parser.set_is_vendor_etc_init_loaded(true);
parser.set_is_odm_etc_init_loaded(true);
}
...
while
(true) { ... }
return
0
;
}
|
可以看到,第一次开机后,它会解析init.rc配置文件,此配置文件中会创建文件,并做一些用户组分配、权限赋予的操作,它位于/system/core/rootdir/init.rc
,如图1-6所示:
图1-6 /system/core/rootdir/init.rc
此外,还会有一些触发器的操作,源码如下所示:
zygote的生成就是由触发器生成的,在稍高一点的Android版本中,会分为32位和64位的zygote,如图1-7所示:
图1-7 不同位数的zygote
这里以init.zygote32.rc
为例做说明,其它东西都是一样的,就是架构有区别。在init.zygote32.rc
配置文件中,有如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
service zygote
/
system
/
bin
/
app_process
-
Xzygote
/
system
/
bin
-
-
zygote
-
-
start
-
system
-
server
class
main
priority
-
20
user root
group root readproc
socket zygote stream
660
root system
onrestart write
/
sys
/
android_power
/
request_state wake
onrestart write
/
sys
/
power
/
state on
onrestart restart audioserver
onrestart restart cameraserver
onrestart restart media
onrestart restart netd
onrestart restart wificond
writepid
/
dev
/
cpuset
/
foreground
/
tasks
|
我们只看第一句命令,这个是启动zygote进程的核心。这里加载了app_process文件,那么这个文件做了什么呢?app_process对应着Android源码中的app_main.cpp,他的位置如图1-8所示:
图1-8 app_main.cpp 位置
同样,我们还是看它的main函数,只不过这里要看他后面跟的参数是什么含义,参数匹配如图1-9所示:
图1-9 app_main.cpp main 参数匹配
代码中,可以看到对zygote做了标记,那我们继续追踪,看看哪里引用了这个bool变量,全局搜索后,最后匹配到了文件的末尾,代码如图1-10所示:
图1-10 zygote 引用
runtime.start加载了com.android.internal.os.ZygoteInit
类,继续追踪runtime.start,最后定位到/frameworks/base/core/jni/AndroidRuntime.cpp
,核心代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
void AndroidRuntime::start(const char
*
className, const Vector<String8>& options,
bool
zygote)
{
...
/
/
初始化 JNI 接口
/
*
start the virtual machine
*
/
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
/
/
创建 env 指针
JNIEnv
*
env;
/
/
if
(startVm(&mJavaVM, &env, zygote) !
=
0
) {
return
;
}
onVmCreated(env);
/
*
*
Register android functions.
*
/
if
(startReg(env) <
0
) {
ALOGE(
"Unable to register all android natives\n"
);
return
;
}
/
*
*
We want to call main() with a String array with arguments
in
it.
*
At present we have two arguments, the
class
name
and
an option string.
*
Create an array to hold them.
*
/
jclass stringClass;
jobjectArray strArray;
jstring classNameStr;
stringClass
=
env
-
>FindClass(
"java/lang/String"
);
assert
(stringClass !
=
NULL);
strArray
=
env
-
>NewObjectArray(options.size()
+
1
, stringClass, NULL);
assert
(strArray !
=
NULL);
classNameStr
=
env
-
>NewStringUTF(className);
assert
(classNameStr !
=
NULL);
env
-
>SetObjectArrayElement(strArray,
0
, classNameStr);
for
(size_t i
=
0
; i < options.size();
+
+
i) {
jstring optionsStr
=
env
-
>NewStringUTF(options.itemAt(i).string());
assert
(optionsStr !
=
NULL);
env
-
>SetObjectArrayElement(strArray, i
+
1
, optionsStr);
}
/
*
*
Start VM. This thread becomes the main thread of the VM,
and
will
*
not
return
until the VM exits.
*
/
/
/
开启 VM 虚拟机,这个线程变成 VM 主线程,直到 VM 退出才结束
/
/
className
=
> com.android.internal.os.ZygoteInit
char
*
slashClassName
=
toSlashClassName(className !
=
NULL ? className : "");
jclass startClass
=
env
-
>FindClass(slashClassName);
if
(startClass
=
=
NULL) {
ALOGE(
"JavaVM unable to locate class '%s'\n"
, slashClassName);
/
*
keep going
*
/
}
else
{
/
/
把 main 函数调起来
jmethodID startMeth
=
env
-
>GetStaticMethodID(startClass,
"main"
,
"([Ljava/lang/String;)V"
);
if
(startMeth
=
=
NULL) {
ALOGE(
"JavaVM unable to find main() in '%s'\n"
, className);
/
*
keep going
*
/
}
else
{
env
-
>CallStaticVoidMethod(startClass, startMeth, strArray);
if
(env
-
>ExceptionCheck())
threadExitUncaughtException(env);
}
}
free(slashClassName);
...
}
|
在后半段代码可以看出,会把com.android.internal.os.ZygoteInit
类,通过Native层反射调用起来。根据类名,我们找到此文件的位置,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
public static void main(String argv[]) {
try
{
...
/
/
socket 通信注册
registerZygoteSocket(socketName);
/
/
预加载所需资源到VM中,如
class
、resource、OpenGL、公用Library等;
/
/
所有fork的子进程共享这份空间而无需重新加载,减少了应用程序的启动时间,
/
/
但也增加了系统的启动时间,Android启动最耗时的部分之一。
preload();
/
/
初始化gc,只是通知VM进行垃圾回收,具体回收时间、怎么回收,由VM内部算法决定。
/
/
gc()需在fork前完成,这样将来复制的子进程才能有尽可能少的垃圾内存没释放;
gcAndFinalize();
/
/
启动system_server,即fork一个Zygote子进程
if
(startSystemServer) {
Runnable r
=
forkSystemServer(abiList, socketName, zygoteServer);
/
/
{@code r
=
=
null}
in
the parent (zygote) process,
and
{@code r !
=
null}
in
the
/
/
child (system_server) process.
if
(r !
=
null) {
r.run();
return
;
}
}
/
/
进入循环模式,获取客户端连接并处理
runSelectLoop(abiList);
/
/
关闭和清理zygote socket
closeServerSocket();
} catch (MethodAndArgsCaller caller) {
caller.run();
} catch (RuntimeException ex) {
Log.e(TAG,
"Zygote died with exception"
, ex);
closeServerSocket();
throw ex;
}
}
|
这里是zygote的初始化,首先开启了socket的通信。在proload中预加载一些资源到VM中,所有fork后的子进程都可以共享这份资源,而无须重新启动。与此同时,增加了系统启动时间,这个环节是整个Android启动链条上耗时最长的部分。
核心在于forkSystemServer,通过此函数fork系统服务进程,代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
private static Runnable forkSystemServer(String abiList, String socketName,
ZygoteServer zygoteServer) {
...
int
pid;
try
{
parsedArgs
=
new ZygoteConnection.Arguments(args);
ZygoteConnection.applyDebuggerSystemProperty(parsedArgs);
ZygoteConnection.applyInvokeWithSystemProperty(parsedArgs);
/
*
Request to fork the system server process
*
/
pid
=
Zygote.forkSystemServer(
parsedArgs.uid, parsedArgs.gid,
parsedArgs.gids,
parsedArgs.debugFlags,
null,
parsedArgs.permittedCapabilities,
parsedArgs.effectiveCapabilities);
} catch (IllegalArgumentException ex) {
throw new RuntimeException(ex);
}
/
*
For child process
*
/
if
(pid
=
=
0
) {
if
(hasSecondZygote(abiList)) {
waitForSecondaryZygote(socketName);
}
zygoteServer.closeServerSocket();
return
handleSystemServerProcess(parsedArgs);
}
}
|
以上就是zygote的启动流程追踪,相信大家到这里都明白了。那么xposed如何Hook zygote,进而实现应用程序的Hook呢?这也是根据上述流程来的,上面说了,核心是替换app_process,app_process.cpp对应的文件是app_main.cpp,我们翻看Github上的Xposed源码,如图1-11所示:
图1-11 xposed app_main.cpp
在图片中,我们看到有两个app_main.cpp出现,那它们之间有什么区别的,这就需要看编译文件了,编译配置文件就是Android.mk,进去之后,最开始就看到它们是如何编译的,如图1-12所示:
图1-12 Android.mk
PLATFORM_SDK_VERSION是SDK的版本,大于等于21以上使用app_main2.cpp,而SDK21对应着Android5,如图1-13所示:
图1-13 Android 版本
通过Xposed的编译配置文件,可以得出Xposed定制了app_process文件,我们直接看app_main2.cpp,目前大家使用的手机型号都来到了Android 8及以上。在app_main2.cpp文件中,我们同样查看被标记的zygote代码,如图1-14所示:
图1-14 xposed app_main2.cpp 核心
它使用Xposed.initialize进行初始化,我们追进去看它的实现,源码位于xposed.cpp中,源码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
|
/
*
*
Initialize Xposed (unless it
is
disabled).
*
/
bool
initialize(
bool
zygote,
bool
startSystemServer, const char
*
className,
int
argc, char
*
const argv[]) {
/
/
参数接管
xposed
-
>zygote
=
zygote;
xposed
-
>startSystemServer
=
startSystemServer;
xposed
-
>startClassName
=
className;
xposed
-
>xposedVersionInt
=
xposedVersionInt;
/
/
XposedBridge.jar 加载到 ClassPath 中
return
addJarToClasspath();
}
|
初始化完成后进入魔改的runtimeStart:
1
|
runtimeStart(runtime, isXposedLoaded ? XPOSED_CLASS_DOTS_ZYGOTE :
"com.android.internal.os.ZygoteInit"
, args, zygote);
|
调用 XPOSED_CLASS_DOTS_ZYGOTE,即 xposedBridge 类的 main 方法,如图1-15所示:
图1-15 XposedBridge
查看xposedBridge类中的main方法,源码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
protected static void main(String[] args) {
/
/
Initialize the Xposed framework
and
modules
try
{
if
(!hadInitErrors()) {
initXResources();
SELinuxHelper.initOnce();
SELinuxHelper.initForProcess(null);
runtime
=
getRuntime();
XPOSED_BRIDGE_VERSION
=
getXposedVersion();
/
/
初始化
if
(isZygote) {
XposedInit.hookResources();
XposedInit.initForZygote();
}
XposedInit.loadModules();
/
/
加载 Xposed 模块
}
else
{
Log.e(TAG,
"Not initializing Xposed because of previous errors"
);
}
} catch (Throwable t) {
Log.e(TAG,
"Errors during Xposed initialization"
, t);
disableHooks
=
true;
}
/
/
Call the original startup code
=
> 原始执行链
if
(isZygote) {
ZygoteInit.main(args);
}
else
{
RuntimeInit.main(args);
}
}
|
从源码得知,它会在此加载Xposed的资源文件,以此完成后续的Hook操作。
1.1.2.3 zygisk 技术原理
而新的Lsposed是基于Magisk的插件zigisk完成的,它也是在Android启动链上入手,实现Hook。
zygisk相当于zygote的magisk,它会在zygote进程中运行magisk的一部分,这也使得Magisk模块更加强大。我们对于Magisk的使用需求一般是获取手机的root权限。使用zygisk可以实现动态替换app_process。
1.2 Lsposed 环境搭建
测试机硬件条件
- 机器型号:Nexux 5X
- 系统:Android 8.1.0
- 镜像下载:Android 8.1.0
可以到https://developers.google.com/android/images#bullhead
下载
- root手机,使用Magisk root即可。在官网下载Magisk-v25.1.apk,安装到手机上。
- 对下载好的镜像进行boot.img提取,如图1-16所示:
图1-16 boot.img 提取
- 按照官网的方式进行boot.img patch,如图1-17所示:
图1-17 boot.img patch
patch完成后,会生成一个patch后的boot.img文件,带有magisk的标识,如图1-18所示:
图1-18 patch 后的boot.img
将patch后的boot.img在bootloader模式使用fastboot flash boot boot.img命令刷入手机,至此Magisk刷入完成。
在Magisk app 设置中打开zigisk
新版的Magisk仓库没有插件,需要手动进行下载,我们到Github上去下载,如图1-19所示:
图1-19 Lsposed安装包
- 本地安装,点击模块后,有一个本地安装的按钮,如图1-20所示:
图1-20 Lsposed安装
本地安装完成后就可以使用Lsposed了
1.2.1 Lsposed Hook 环境搭建
Lsposed的开发环境同Xposed的一致。
- app级build.gradle加入图1-21所示的配置
图1-21 build.gradle
- AndroidManifest.xml 配置,如图1-22所示
图1-22 AndroidManifest.xml
- 入口文件建立
在main下新建asserts资源目录,如图1-23所示:
图1-23 asserts
并在下一级建立xposed_init文件,如图1-24所示:
图1-24 xposed_init文件
- 添加入口类,如图1-25所示
图1-25 入口类
HookTest是新建的一个类,用来测试我们的demo
- 编写测试代码
图1-26 测试代码
测试代码的功能是打印目标app的包名
1.2.2 基本使用
- 激活模块
运行Hook代码后,打开Lsposed,模块第一次启动还未激活,如图1-27所示:
图1-27 未激活的模块
- 选择应用
激活模块后,我们可以选择待Hook的应用,应用是可以多选的,如图1-28所示:
图1-28 应用选择
- 重启应用,Hook代码即可生效,如图1-29所示:
图1-29 Hook 生效