因为最近在用EdXposed,对于magisk和riru很是好奇,之前也大致了解过edxp通过riru实现zygote注入进而完成ART Hook实现类Xposed,但是在准备看源码的时候发现不知道入口在哪,本来想找找有没有现成的大佬总结,发现貌似没有,于是自力更生,从magisk插件开发到riru插件到riru加载逻辑,一步步找到了edxp的代码入口。
根据面具的官方文档(https://topjohnwu.github.io/Magisk/guides.html)一个Magisk插件模块是/data/adb/modules下的一个文件夹,其中至少包含着以下几个文件:
EdXposed在gradle构建出产物时,会将整个项目打包成一个magisk module installer,该installer与magisk module的区别在于installer为一个zip压缩文件,同时包含META-INF文件夹,其中的update-binary.sh将作为安装前执行的脚本。
但是我们在EdXposed中并没有搜到相关的module.prop文件,只能看到module.prop.tpl模版文件,那么这两个文件是如何关联的呢。
在edxp-core的build.gradle中可以看到声明了一系列构建Task,从EdXposed的构建命令./gradlew clean :edxp-core:[zip|push]YahfaRelease开始追踪,
可以发现 pushTask只是将构建产物推送到设备sdcard下,真正打包任务在zipTask中,其依赖于prepareMagiskFilesTask,该task的作用是:
但是EdXposed作为一个Magisk插件,插件加载时所会执行的update-binary、post-fs-data.sh、customize.sh等脚本中并没有相关执行EdXp的命令,那么EdXposed是如何在Zygote进程加载时所执行呢。
EdXposed实际上还是一个riru插件,即基于riru注入zygote进程时机和riru相关的API声明来实现zygote进程执行时的加载。
Riru模块本质上是一个Magisk插件,额外的地方在于新增了一个目录/data/adb/riru/modules(旧版本在/data/misc/riru/modules)下多了一个插件目录,该目录的作用在于在riru加载时通过该目录名称加载对应的插件so,具体逻辑下面具体分析。
首先看一个Riru模块如何编译构建的,入口依然是build.gradle,即从构建命令zipMagiskMoudle入手:
源码:https://github.com/RikkaApps/Riru-ModuleTemplate/blob/master/module/build.gradle
可以看到主要做了以下操作:
一句话总结:将项目下template/magisk_module下的如动态库、module.prop、post-fs-data.sh等magisk插件相关文件打包成magisk module install所需的zip格式。剩下的就是安装到手机中由Magisk进行加载。
一个典型的riru插件的加载顺序可以简单理解为:
update_binary -> customize.sh -> post-fs-data.sh
其中customize.sh是riru重写update_binary进行执行的,其他都是遵循magisk的插件执行顺序。
接下来就通过这些shell脚本的执行来找到riru是如何定位插件、解析插件、执行插件和完成zygote注入的。
首先看riru是如何被magisk执行的,因为riru也是一个magisk插件,所以直接先看post-fs-data.sh,可以看到:
这个版本的riru直接通过/system/etc/public.libraries.txt自动完成so的dlopen加载。
因此可以直接奔向riru的so,找到.initarray方法(\_attribute__((constructor))),可以看到两个关键方法调用,分别为
XHOOK_REGISTER(".*\libandroid_runtime.so$", jniRegisterNativeMethods);
和
load_modules();
具体.init_array代码可以看main.cpp中:
这里XHOOK_REGISTER是一个宏定义,
XHOOK_REGISTER(".*\libandroid_runtime.so$", jniRegisterNativeMethods);
实际上相当于
new_jniRegisterNativeMethods和old_jniRegisterNativeMethods则是通过NEW_FUNC_DEF宏定义来进行声明:
可以看到在com/android/internal/os/Zygote注册JNI方法时会回调onRegisterZygote方法获取新的jniMethods列表进行替换,在onRegisterZygote方法中主要替换了三个方法的fnPtr指针并兼容不同的安卓版本:
这三个方法是应用进程或者系统服务进程被fork 出来的时候会调用的方法,这里以nativeForkAndSpecialize举例分析nativeForkAndSpecialize的AOP逻辑和模块中声明的forkAndSpecializePre/Post等系列方法如何被调用及调用时机。
load_modules解析如下:
EdXposed本身也是一个riru插件,但是如何证明呢。
我们可以知道如果作为一个riru插件,必然需要
在edxp-core中,依然通过build.gradle为入口分析zip打包的逻辑,可以看到也是一个标准的magisk module installer,其中customize.sh中有一行关键代码创建了riru插件的相关目录:
通过这段脚本可以看出edxp插件在magisk加载时创建了一个riru插件目录,并且把libriru_edxp.so重命名成一个随机后缀的so文件由riru进行加载。
接着分析libriru_edxp.so,通过CMakeLists.txt可以知道是由edxp-core中编译得到,查看edxp-core/src/main/cpp/main/src/main.cpp中可以看到确实声明了riru中的一些方法模版钩子:
等方法,一切都可以想明白了。接下来就是edxp如何进行ART Hook。
def
zipTask
=
task(
"zip${backendCapped}${variantCapped}"
,
type
:
Zip
) {
dependsOn prepareMagiskFilesTask
archiveName
"${module_name}-${project.version}-${variantLowered}.zip"
destinationDir
file
(
"$projectDir/release"
)
from
"$zipPathMagiskRelease"
}
task(
"push${backendCapped}${variantCapped}"
,
type
: Exec) {
dependsOn zipTask
workingDir
"${projectDir}/release"
def
commands
=
[
"adb"
,
"push"
,
"${module_name}-${project.version}-${variantLowered}.zip"
,
"/sdcard/"
]
if
(is_windows) {
commandLine
'cmd'
,
'/c'
, commands.join(
" "
)
}
else
{
commandLine commands
}
}
}
/
/
backward compatible
task(
"zip${variantCapped}"
) {
dependsOn
"zipYahfa${variantCapped}"
}
task(
"push${variantCapped}"
) {
dependsOn
"pushYahfa${variantCapped}"
}
def
zipTask
=
task(
"zip${backendCapped}${variantCapped}"
,
type
:
Zip
) {
dependsOn prepareMagiskFilesTask
archiveName
"${module_name}-${project.version}-${variantLowered}.zip"
destinationDir
file
(
"$projectDir/release"
)
from
"$zipPathMagiskRelease"
}
task(
"push${backendCapped}${variantCapped}"
,
type
: Exec) {
dependsOn zipTask
workingDir
"${projectDir}/release"
def
commands
=
[
"adb"
,
"push"
,
"${module_name}-${project.version}-${variantLowered}.zip"
,
"/sdcard/"
]
if
(is_windows) {
commandLine
'cmd'
,
'/c'
, commands.join(
" "
)
}
else
{
commandLine commands
}
}
}
/
/
backward compatible
task(
"zip${variantCapped}"
) {
dependsOn
"zipYahfa${variantCapped}"
}
task(
"push${variantCapped}"
) {
dependsOn
"pushYahfa${variantCapped}"
}
def
prepareMagiskFilesTask
=
task(
"prepareMagiskFiles${backendCapped}${variantCapped}"
,
type
: Delete) {
dependsOn prepareJarsTask,
"assemble${variantCapped}"
delete
file
(zipPathMagiskRelease)
doFirst {
copy {
from
"${projectDir}/tpl/edconfig.tpl"
into templateFrameworkPath
rename
"edconfig.tpl"
,
"edconfig.jar"
expand(version:
"$version"
, backend:
"$backend"
)
}
copy {
from
"${projectDir}/tpl/module.prop.tpl"
into templateRootPath
rename
"module.prop.tpl"
,
"module.prop"
expand(moduleId:
"$magiskModuleId"
, backend:
"$backendCapped"
,
versionName:
"$version"
,
versionCode:
"$versionCode"
, authorList:
"$authorList"
)
filter
(FixCrLfFilter.
class
, eol: FixCrLfFilter.CrLf.newInstance(
"lf"
))
}
}
def
libPathRelease
=
"${buildDir}/intermediates/cmake/${variantLowered}/obj"
doLast {
copy {
from
"${projectDir}/template_override"
into zipPathMagiskRelease
}
copy {
from
"$libPathRelease/armeabi-v7a"
into
"$zipPathMagiskRelease/system/lib"
}
copy {
from
"$libPathRelease/arm64-v8a"
into
"$zipPathMagiskRelease/system/lib64"
}
copy {
from
"$libPathRelease/x86"
into
"$zipPathMagiskRelease/system_x86/lib"
}
copy {
from
"$libPathRelease/x86_64"
into
"$zipPathMagiskRelease/system_x86/lib64"
}
}
}
def
prepareMagiskFilesTask
=
task(
"prepareMagiskFiles${backendCapped}${variantCapped}"
,
type
: Delete) {
dependsOn prepareJarsTask,
"assemble${variantCapped}"
delete
file
(zipPathMagiskRelease)
doFirst {
copy {
from
"${projectDir}/tpl/edconfig.tpl"
into templateFrameworkPath
rename
"edconfig.tpl"
,
"edconfig.jar"
expand(version:
"$version"
, backend:
"$backend"
)
}
copy {
from
"${projectDir}/tpl/module.prop.tpl"
into templateRootPath
rename
"module.prop.tpl"
,
"module.prop"
expand(moduleId:
"$magiskModuleId"
, backend:
"$backendCapped"
,
versionName:
"$version"
,
versionCode:
"$versionCode"
, authorList:
"$authorList"
)
filter
(FixCrLfFilter.
class
, eol: FixCrLfFilter.CrLf.newInstance(
"lf"
))
}
}
def
libPathRelease
=
"${buildDir}/intermediates/cmake/${variantLowered}/obj"
doLast {
copy {
from
"${projectDir}/template_override"
into zipPathMagiskRelease
}
copy {
from
"$libPathRelease/armeabi-v7a"
into
"$zipPathMagiskRelease/system/lib"
}
copy {
from
"$libPathRelease/arm64-v8a"
into
"$zipPathMagiskRelease/system/lib64"
}
copy {
from
"$libPathRelease/x86"
into
"$zipPathMagiskRelease/system_x86/lib"
}
copy {
from
"$libPathRelease/x86_64"
into
"$zipPathMagiskRelease/system_x86/lib64"
}
}
}
android.libraryVariants.
all
{ variant
-
>
def
task
=
variant.assembleProvider.get()
task.doLast {
/
/
clear
delete { delete magiskDir }
/
/
copy
from
template
copy {
from
"$rootDir/template/magisk_module"
into magiskDir.path
exclude
'riru.sh'
}
/
/
copy riru.sh
copy {
from
"$rootDir/template/magisk_module"
into magiskDir.path
include
'riru.sh'
filter
{ line
-
>
line.replaceAll(
'%%%RIRU_MODULE_ID%%%'
, moduleId)
.replaceAll(
'%%%RIRU_MIN_API_VERSION%%%'
, moduleMinRiruApiVersion.toString())
.replaceAll(
'%%%RIRU_MIN_VERSION_NAME%%%'
, moduleMinRiruVersionName)
}
filter
(FixCrLfFilter.
class
,
eol: FixCrLfFilter.CrLf.newInstance(
"lf"
))
}
/
/
copy .git files manually since gradle exclude it by default
Files.copy(
file
(
"$rootDir/template/magisk_module/.gitattributes"
).toPath(),
file
(
"${magiskDir.path}/.gitattributes"
).toPath())
/
/
generate module.prop
def
modulePropText
=
""
magiskModuleProp.each { k, v
-
> modulePropText
+
=
"$k=$v\n"
}
modulePropText
=
modulePropText.trim()
file
(
"$magiskDir/module.prop"
).text
=
modulePropText
/
/
generate module.prop
for
Riru
def
riruModulePropText
=
""
moduleProp.each { k, v
-
> riruModulePropText
+
=
"$k=$v\n"
}
riruModulePropText
=
riruModulePropText.trim()
file
(riruDir).mkdirs()
/
/
module.prop.new will be renamed to module.prop
in
post
-
fs
-
data.sh
file
(
"$riruDir/module.prop.new"
).text
=
riruModulePropText
/
/
copy native files
def
nativeOutDir
=
file
(
"build/intermediates/cmake/$variant.name/obj"
)
file
(
"$magiskDir/system"
).mkdirs()
file
(
"$magiskDir/system_x86"
).mkdirs()
renameOrFail(
file
(
"$nativeOutDir/arm64-v8a"
),
file
(
"$magiskDir/system/lib64"
))
renameOrFail(
file
(
"$nativeOutDir/armeabi-v7a"
),
file
(
"$magiskDir/system/lib"
))
renameOrFail(
file
(
"$nativeOutDir/x86_64"
),
file
(
"$magiskDir/system_x86/lib64"
))
renameOrFail(
file
(
"$nativeOutDir/x86"
),
file
(
"$magiskDir/system_x86/lib"
))
/
/
generate sha1sum
fileTree(
"$magiskDir"
).matching {
exclude
"README.md"
,
"META-INF"
}.visit { f
-
>
if
(f.directory)
return
file
(f.
file
.path
+
".sha256sum"
).text
=
calcSha256(f.
file
)
}
}
task.finalizedBy zipMagiskMoudle
}
task zipMagiskMoudle(
type
:
Zip
) {
from
magiskDir
archiveName zipName
destinationDir outDir
}
android.libraryVariants.
all
{ variant
-
>
def
task
=
variant.assembleProvider.get()
task.doLast {
/
/
clear
delete { delete magiskDir }
/
/
copy
from
template
copy {
from
"$rootDir/template/magisk_module"
into magiskDir.path
exclude
'riru.sh'
}
/
/
copy riru.sh
copy {
from
"$rootDir/template/magisk_module"
into magiskDir.path
include
'riru.sh'
filter
{ line
-
>
line.replaceAll(
'%%%RIRU_MODULE_ID%%%'
, moduleId)
.replaceAll(
'%%%RIRU_MIN_API_VERSION%%%'
, moduleMinRiruApiVersion.toString())
.replaceAll(
'%%%RIRU_MIN_VERSION_NAME%%%'
, moduleMinRiruVersionName)
}
filter
(FixCrLfFilter.
class
,
eol: FixCrLfFilter.CrLf.newInstance(
"lf"
))
}
/
/
copy .git files manually since gradle exclude it by default
Files.copy(
file
(
"$rootDir/template/magisk_module/.gitattributes"
).toPath(),
file
(
"${magiskDir.path}/.gitattributes"
).toPath())
/
/
generate module.prop
def
modulePropText
=
""
magiskModuleProp.each { k, v
-
> modulePropText
+
=
"$k=$v\n"
}
modulePropText
=
modulePropText.trim()
file
(
"$magiskDir/module.prop"
).text
=
modulePropText
/
/
generate module.prop
for
Riru
def
riruModulePropText
=
""
moduleProp.each { k, v
-
> riruModulePropText
+
=
"$k=$v\n"
}
riruModulePropText
=
riruModulePropText.trim()
file
(riruDir).mkdirs()
/
/
module.prop.new will be renamed to module.prop
in
post
-
fs
-
data.sh
file
(
"$riruDir/module.prop.new"
).text
=
riruModulePropText
/
/
copy native files
def
nativeOutDir
=
file
(
"build/intermediates/cmake/$variant.name/obj"
)
file
(
"$magiskDir/system"
).mkdirs()
file
(
"$magiskDir/system_x86"
).mkdirs()
renameOrFail(
file
(
"$nativeOutDir/arm64-v8a"
),
file
(
"$magiskDir/system/lib64"
))
renameOrFail(
file
(
"$nativeOutDir/armeabi-v7a"
),
file
(
"$magiskDir/system/lib"
))
renameOrFail(
file
(
"$nativeOutDir/x86_64"
),
file
(
"$magiskDir/system_x86/lib64"
))
renameOrFail(
file
(
"$nativeOutDir/x86"
),
file
(
"$magiskDir/system_x86/lib"
))
/
/
generate sha1sum
fileTree(
"$magiskDir"
).matching {
exclude
"README.md"
,
"META-INF"
}.visit { f
-
>
if
(f.directory)
return
file
(f.
file
.path
+
".sha256sum"
).text
=
calcSha256(f.
file
)
}
}
task.finalizedBy zipMagiskMoudle
}
task zipMagiskMoudle(
type
:
Zip
) {
from
magiskDir
archiveName zipName
destinationDir outDir
}
LIBRARIES_FILE
=
'/system/etc/public.libraries.txt'
mkdir
-
p
"$MODDIR/system/etc"
cp
-
f $LIBRARIES_FILE
"$MODDIR/$LIBRARIES_FILE"
grep
-
qxF
'libriru.so'
"$MODDIR/$LIBRARIES_FILE"
|| echo
'libriru.so'
>>
"$MODDIR/$LIBRARIES_FILE"
LIBRARIES_FILE
=
'/system/etc/public.libraries.txt'
mkdir
-
p
"$MODDIR/system/etc"
cp
-
f $LIBRARIES_FILE
"$MODDIR/$LIBRARIES_FILE"
grep
-
qxF
'libriru.so'
"$MODDIR/$LIBRARIES_FILE"
|| echo
'libriru.so'
>>
"$MODDIR/$LIBRARIES_FILE"
PS:
riru目前存在了三种已知的app_process注入实现:
1.
最早通过替换libmemtrack.so
2.
后来通过
/
system
/
etc
/
public.libraries.txt
3.
目前通过native bridge即设置系统属性ro.dalvik.vm.native.bridge
PS:
riru目前存在了三种已知的app_process注入实现:
1.
最早通过替换libmemtrack.so
2.
后来通过
/
system
/
etc
/
public.libraries.txt
3.
目前通过native bridge即设置系统属性ro.dalvik.vm.native.bridge
extern
"C"
void constructor() __attribute__((constructor));
/
/
_init_array libriru.so被dlopen后最先执行的函数
void constructor() {
hide::hide_modules(nullptr,
0
);
if
(getuid() !
=
0
)
return
;
char cmdline[ARG_MAX
+
1
];
get_self_cmdline(cmdline,
0
);
if
(strcmp(cmdline,
"zygote"
) !
=
0
&& strcmp(cmdline,
"zygote32"
) !
=
0
&& strcmp(cmdline,
"zygote64"
) !
=
0
&& strcmp(cmdline,
"usap32"
) !
=
0
&& strcmp(cmdline,
"usap64"
) !
=
0
) {
LOGW(
"not zygote (cmdline=%s)"
, cmdline);
return
;
}
LOGI(
"Riru %s (%d) in %s"
, RIRU_VERSION_NAME, RIRU_VERSION_CODE, cmdline);
LOGI(
"config dir is %s"
, CONFIG_DIR);
if
(access(CONFIG_DIR
"/disable"
, F_OK)
=
=
0
) {
LOGI(
"%s exists, do nothing"
, CONFIG_DIR
"/disable"
);
return
;
}
read_prop();
/
/
通过GOT表hook libandroid_runtime.so中对jniRegisterNativeMethods方法的调用,因为libandroid_runtime.so中所有JNI方法都是通过该方法进行注册,然后再通过手动调用registeNatives来替换
/
/
因此通过hook该方法可以在com.android.internal.os.Zygote
/
/
Riru也因此完成了zygote进程的注入
XHOOK_REGISTER(
".*\\libandroid_runtime.so$"
, jniRegisterNativeMethods);
if
(xhook_refresh(
0
)
=
=
0
) {
xhook_clear();
LOGI(
"hook installed"
);
}
else
{
LOGE(
"failed to refresh hook"
);
}
/
/
加载插件
load_modules();
status::writeToFile();
}
extern
"C"
void constructor() __attribute__((constructor));
/
/
_init_array libriru.so被dlopen后最先执行的函数
void constructor() {
hide::hide_modules(nullptr,
0
);
if
(getuid() !
=
0
)
return
;
char cmdline[ARG_MAX
+
1
];
get_self_cmdline(cmdline,
0
);
if
(strcmp(cmdline,
"zygote"
) !
=
0
&& strcmp(cmdline,
"zygote32"
) !
=
0
&& strcmp(cmdline,
"zygote64"
) !
=
0
&& strcmp(cmdline,
"usap32"
) !
=
0
&& strcmp(cmdline,
"usap64"
) !
=
0
) {
LOGW(
"not zygote (cmdline=%s)"
, cmdline);
return
;
}
LOGI(
"Riru %s (%d) in %s"
, RIRU_VERSION_NAME, RIRU_VERSION_CODE, cmdline);
LOGI(
"config dir is %s"
, CONFIG_DIR);
if
(access(CONFIG_DIR
"/disable"
, F_OK)
=
=
0
) {
LOGI(
"%s exists, do nothing"
, CONFIG_DIR
"/disable"
);
return
;
}
read_prop();
/
/
通过GOT表hook libandroid_runtime.so中对jniRegisterNativeMethods方法的调用,因为libandroid_runtime.so中所有JNI方法都是通过该方法进行注册,然后再通过手动调用registeNatives来替换
/
/
因此通过hook该方法可以在com.android.internal.os.Zygote
/
/
Riru也因此完成了zygote进程的注入
XHOOK_REGISTER(
".*\\libandroid_runtime.so$"
, jniRegisterNativeMethods);
if
(xhook_refresh(
0
)
=
=
0
) {
xhook_clear();
LOGI(
"hook installed"
);
}
else
{
LOGE(
"failed to refresh hook"
);
}
/
/
加载插件
load_modules();
status::writeToFile();
}
if
(xhook_register(
".*\\libandroid_runtime.so$"
, jniRegisterNativeMethods, (void
*
) new_jniRegisterNativeMethods, (void
*
*
) &old_jniRegisterNativeMethods) !
=
0
) \
LOGE(
"failed to register hook jniRegisterNativeMethods ."
); \
if
(xhook_register(
".*\\libandroid_runtime.so$"
, jniRegisterNativeMethods, (void
*
) new_jniRegisterNativeMethods, (void
*
*
) &old_jniRegisterNativeMethods) !
=
0
) \
LOGE(
"failed to register hook jniRegisterNativeMethods ."
); \
static ret (
*
old_
static ret new_
NEW_FUNC_DEF(
int
, jniRegisterNativeMethods, JNIEnv
*
env, const char
*
className,
const JNINativeMethod
*
methods,
int
numMethods) {
api::putNativeMethod(className, methods, numMethods);
LOGD(
"jniRegisterNativeMethods %s"
, className);
JNINativeMethod
*
newMethods
=
nullptr;
if
(strcmp(
"com/android/internal/os/Zygote"
, className)
=
=
0
) {
/
/
com
/
android
/
internal
/
os
/
Zygote注册时回调onRegisterZygote方法获取新的jniMethods列表进行替换
newMethods
=
onRegisterZygote(env, className, methods, numMethods);
}
else
if
(strcmp(
"android/os/SystemProperties"
, className)
=
=
0
) {
/
/
hook android.os.SystemProperties
/
/
see comment of SystemProperties_set
in
jni_native_method.cpp
for
detail
/
/
回调onRegisterSystemProperties方法
newMethods
=
onRegisterSystemProperties(env, className, methods, numMethods);
}
int
res
=
old_jniRegisterNativeMethods(env, className, newMethods ? newMethods : methods,
numMethods);
/
*
if
(!newMethods) {
NativeMethod::jniRegisterNativeMethodsPost(env, className, methods, numMethods);
}
*
/
delete newMethods;
return
res;
}
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2020-10-27 17:37
被alienhe编辑
,原因: