在android系统中,进程之间是相互隔离的,两个进程之间是没办法直接跨进程访问其他进程的空间信息的。那么在android平台中要对某个app进程进行内存操作,并获取目标进程的地址空间内信息或者修改目标进程的地址空间内的私有信息,就需要涉及到注入技术。
通过注入技术可以将指定so模块或代码注入到目标进程中,只要注入成功后,就可以进行访问和篡改目标进程空间内的信息,包括数据和代码。
在我们进行算法还原再或者进行APP的RPC算法调用的时候,都有对APP注入的需求。
只不过目前的工具比较成熟,大家都忽略了注入的这个过程。
随着厂商对常见注入工具Frida、Xposed的特征检测,注入后APP会发生崩溃不可运行的问题。
众所周知:游戏安全对抗领域往往要比常见的应用安全领先多个领域,当然很多大厂也开始上了策略检测注入,但更多的只是风控策略,不会发生闪退(因为不同的厂商对于系统多少有些修改,万一有些系统会注入辅助so,那么会造成很大的误伤)
例如BB企业版、爱加密企业版、360企业版都对frida、xposed等工具进行检测,那么我们就可以手动注入dobby hook 以及支持Java的一些sandhook 来辅助分析,当然分析效率没有frida高,但是不会触发闪退检测策略。(当然本工具后期有打算进一步开发隐藏注入,这对游戏安全是小儿科,但是应用安全隐藏的话效果还是很可观的)
所以本文章首先讨论多种注入方式,并给出开源的面具模块供大家编译使用,注入自己开发的so,或者是调用成品库,进行hook以及高性能的RPC。
本文会罗列出几个常见的注入技术,以及列出使用该原理的工具,并重点讲一下zygote注入的模块开发。
我会详细讲解我比较熟悉的两种注入方式(修改aosp、zygisk),以及简单带过一些可能的注入方式,并后续补充注入材料。
静态注入,静态解析ELF文件,增加一个依赖SO,或新增一个section节(注入代码在section字段),代码节是自己的注入代码,然后修复ELF文件结构。
修改dex,增加静态dex段,system.load 加载自己的so
实现案例:平头哥,一些虚拟xposed框架
这种方式的优点:
免root、便于分发、打包速度一般
缺点:
对于签名检测的pass难度比较高
**由于Android是基于linux内核的操作系统,所以Android下的注入也是基于Linux下的系统调用函数ptrace()
实现的。**即在获得root权限后,通过ptrace()系统调用将stub(桩代码)注入到指定pid的进程中。
常见使用工具:IDA、GDB、LLDB、Frida等常见工具
我们也可以自己写一个ptrace简单的注入so,下面我给出一个项目,感兴趣的大佬可以自己编译进行尝试。
这里进行预告:后面我会自己写一个调试器(基于ptrace),会写出文章进行分享,目前已经在做了。
这里简单附上几篇ptrace的文章,感兴趣的大佬可以尝试。
因为我研究的实在是不多。
6aaK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8X3S2H3z5e0p5H3x3K6p5#2i4K6u0r3j5i4u0@1K9h3y4D9k6g2)9J5c8X3c8W2N6r3q4A6L8s2y4Q4x3V1j5%4y4K6x3K6y4e0l9#2z5l9`.`.
0baK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8X3A6A6L8Y4A6Z5N6h3!0B7N6h3&6Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3k6r3g2@1j5h3W2D9M7#2)9J5c8U0V1&6x3o6l9I4x3o6f1`.
这种方式的优点:
注入速度快,注入不容易检测到(ptrace注入完成以后直接取消ptrace,在后面检测不到)
缺点:
需要root、有一定的ptrace检测(像ida这样的注入,会在maps扫描到当前正在被调试)
attach方式被ptrace占坑方式搞得不好绕过(ida表示非常难受)。
常见使用工具:xposed 实现工具:Riru(早期)、Zygisk(常用)
zygote注入是属于全局注入的方式,它主要是依赖于fork()子进程方式进行注入的。
目前市面上比较成熟的注入工具xposed就是基于zygote的全局注入。
它有两大优点:主要在于zygote是系统进程,通过系统进程fork出来后它就具备隐蔽性,强大性。
常见的一些工具都是使用Zygisk注入,比如知名的开源项目Zygisk-Il2CppDumper
以及寒冰大佬开发的FrdiaManager 还有Xposed框架都支持Zygsik注入
下面我来讲一下我开发的模块是如何注入自己的so的(本模块是基于**Zygisk-Il2CppDumper项目进行修改,因为作者写的Gradle实在是太好用啦)**
通过修改aosp系统的源码,在app加载之前插桩语句,加载自定义库。
后面会有一个小模块进行讨论。
重点就是这几个api, 看注释理解
用最通俗粗略的理解来表示的话:
pre是刚从zygote fork出来没有沙箱限制的时候
postAppSpecialize 相当于app进程启动, 这里可以做自定义dex加载的一些动作
postServerSpecialize 相当于系统服务也就是system server 运行
官方提供了一个100K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6@1L8%4m8B7L8$3S2F1N6%4g2Q4x3V1k6*7P5h3N6A6M7$3E0Q4x3X3c8E0L8$3c8#2L8r3g2Q4x3X3c8K6j5h3#2H3L8r3g2Q4c8e0k6Q4b7e0q4Q4z5o6S2Q4c8e0c8Q4b7V1g2Q4z5p5u0Q4c8f1k6Q4b7V1y4Q4z5p5y4Q4c8e0c8Q4b7U0W2Q4z5f1k6Q4c8e0g2Q4z5p5k6Q4b7f1k6Q4c8e0c8Q4b7V1u0Q4b7e0g2Q4c8e0S2Q4b7f1k6Q4b7V1u0Q4c8e0c8Q4b7U0S2Q4z5o6m8Q4c8e0S2Q4b7f1k6Q4b7V1t1`.
实现原理非常简单:
从app可以访问的路径copy要注入的so到自己的私有目录(因为有selinux的限制)
之后使用dl_open加载目标so
这里主要实现了面具模块主要提供的api
我们的实现主要是这个实现的函数,此时app已经处于沙盒中了,只有app自身的权限
在这里我们需要修改要注入的包名,不然模块不会进一步注入

主要功能实现
复制过程,主要就是把/data/local/tmp/test.so 复制到私有目录,然后修改权限为0755 不然dlopen没法加载
尝试打开十次,获取到so的handle
寻找符号并执行
可以自己写一个自己的函数在用dlsym调用,这里就不多说了
源码导入android studio就可以构建出面具模块了,再次感谢原作者的项目

在编译之前应该修改目标app的包名,如果不修改不会注入(后面会考虑做一个和shamiko一样的黑白名单)初代版本大家先手动修改
编译自己的插件so实现自己的功能
这里需要了解的是dlopen的加载流程。见番外篇。
当然可以修改插件,使用dlsym找到自己函数的符号,手动加载。
我已经实现了JNI_ONLOAD的加载。
移动so到/data/local/tmp目录下 命名为test.so
享受注入!
插件so源码:

注入效果:

545K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3q4G2M7%4m8^5M7X3g2X3i4K6u0W2j5$3!0E0i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5k6o6p5J5i4K6u0W2x3q4)9J5k6e0m8Q4y4h3k6J5x3#2)9J5c8Y4S2J5k6h3k6Q4x3V1k6T1K9h3!0F1K9h3y4Q4x3V1k6D9K9h3u0V1L8q4)9J5c8X3I4A6j5X3c8D9i4K6u0W2j5%4m8H3

之后调用

之后调用

来到真正的dlopen加载的地方
这里就是对so的各个段的装载,我们目光聚焦于结尾的部分

在这个函数里有:
对DT_INIT和DT_INIT_ARRAY的调用
所以我们dlopen如果成功打开了so,就会对这两个地方调用
所以说插件的入口可以选择在这两个段里 attribute((constructor))
这里我通过修改源码去注入so,so注入的时机我开始的选择是越早越好。
这里选在在handleBindApplication处,创建ContextImpl对象时进行一系列的复制注入操作。
我们流程选择先将需要注入的so放到sd卡目录下,然后判断app为非系统app时进行复制到app目录,注入app等一系列操作。 我们找到源码,目录AOSP/frameworks/base/core/java/android/app/ActivityThread.java,
找到handleBindApplication,定位到”final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);”这一行。

开始加入我们自己的代码:
和上面的实现一样,就是copyso 然后使用system.load即可加载
网上有很多实现,还可以自定义selinux标签,配合系统服务和配套app达到自定义注入
如果文章反响还不错,我会继续更新一些frida、xposed分析不了的app(被反调试block掉的)
来进一步加深大家对这个框架的使用。
增加第二种注入方式:将插件so打包到框架里,隐藏落地文件的特征。
通过借鉴Riru的注入方式,隐藏注入(对一些厂商管用)
进一步研究完美隐藏方式
项目地址:
853K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6B7K9i4q4A6N6e0t1H3x3U0u0Q4x3V1k6K9P5h3N6A6M7$3E0Q4x3X3c8y4P5f1W2F1K9X3g2U0N6r3!0J5
附件注入的包名已经固定,需要自己编译
static {
try
{
String soName;
if
(Process.is64Bit()) {
soName
=
path
/
to
/
lib64;
}
else
{
soName
=
path
/
to
/
lib32;
}
System.loadLibrary(soName);
} catch (Throwable e) {
CLog.e(
"static loadLibrary error"
, e);
}
}
static {
try
{
String soName;
if
(Process.is64Bit()) {
soName
=
path
/
to
/
lib64;
}
else
{
soName
=
path
/
to
/
lib32;
}
System.loadLibrary(soName);
} catch (Throwable e) {
CLog.e(
"static loadLibrary error"
, e);
}
}
class
ModuleBase {
public:
/
/
这个方法在模块被加载到目标进程时立即被调用。
/
/
会传递一个 Zygisk API 句柄作为参数。
virtual void onLoad([[maybe_unused]] Api
*
api, [[maybe_unused]] JNIEnv
*
env) {}
/
/
这个方法在应用进程被专门化之前被调用。
/
/
在这个时候,进程刚刚从 zygote 进程中分叉出来,但尚未应用任何特定于应用的专门化。
/
/
这意味着进程没有任何沙箱限制,并且仍然以 zygote 的相同权限运行。
/
/
/
/
所有将要传递并用于应用程序专门化的参数都被封装在一个 AppSpecializeArgs 对象中。
/
/
您可以读取和覆盖这些参数,以改变应用程序进程的专门化方式。
/
/
/
/
如果您需要以超级用户权限运行一些操作,可以调用 Api::connectCompanion() 来
/
/
获取一个套接字,用于与根陪伴进程进行 IPC 调用。
/
/
请参阅 Api::connectCompanion() 以获取更多信息。
virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs
*
args) {}
/
/
这个方法在应用进程专门化之后被调用。
/
/
在这个时候,进程已经应用了所有沙箱限制,并以应用自身代码的权限运行。
virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs
*
args) {}
/
/
这个方法在系统服务器进程被专门化之前被调用。
/
/
请参阅 preAppSpecialize(args) 以获取更多信息。
virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs
*
args) {}
/
/
这个方法在系统服务器进程专门化之后被调用。
/
/
在这个时候,进程以 system_server 的权限运行。
virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs
*
args) {}
};
class
ModuleBase {
public:
/
/
这个方法在模块被加载到目标进程时立即被调用。
/
/
会传递一个 Zygisk API 句柄作为参数。
virtual void onLoad([[maybe_unused]] Api
*
api, [[maybe_unused]] JNIEnv
*
env) {}
/
/
这个方法在应用进程被专门化之前被调用。
/
/
在这个时候,进程刚刚从 zygote 进程中分叉出来,但尚未应用任何特定于应用的专门化。
/
/
这意味着进程没有任何沙箱限制,并且仍然以 zygote 的相同权限运行。
/
/
/
/
所有将要传递并用于应用程序专门化的参数都被封装在一个 AppSpecializeArgs 对象中。
/
/
您可以读取和覆盖这些参数,以改变应用程序进程的专门化方式。
/
/
/
/
如果您需要以超级用户权限运行一些操作,可以调用 Api::connectCompanion() 来
/
/
获取一个套接字,用于与根陪伴进程进行 IPC 调用。
/
/
请参阅 Api::connectCompanion() 以获取更多信息。
virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs
*
args) {}
/
/
这个方法在应用进程专门化之后被调用。
/
/
在这个时候,进程已经应用了所有沙箱限制,并以应用自身代码的权限运行。
virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs
*
args) {}
/
/
这个方法在系统服务器进程被专门化之前被调用。
/
/
请参阅 preAppSpecialize(args) 以获取更多信息。
virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs
*
args) {}
/
/
这个方法在系统服务器进程专门化之后被调用。
/
/
在这个时候,进程以 system_server 的权限运行。
virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs
*
args) {}
};
using zygisk::Api;
using zygisk::AppSpecializeArgs;
using zygisk::ServerSpecializeArgs;
class
MyModule : public zygisk::ModuleBase {
public:
void onLoad(Api
*
api, JNIEnv
*
env) override {
this
-
>api
=
api;
this
-
>env
=
env;
}
void preAppSpecialize(AppSpecializeArgs
*
args) override {
auto package_name
=
env
-
>GetStringUTFChars(args
-
>nice_name, nullptr);
auto app_data_dir
=
env
-
>GetStringUTFChars(args
-
>app_data_dir, nullptr);
LOGI(
"preAppSpecialize %s %s"
, package_name, app_data_dir);
preSpecialize(package_name, app_data_dir);
env
-
>ReleaseStringUTFChars(args
-
>nice_name, package_name);
env
-
>ReleaseStringUTFChars(args
-
>app_data_dir, app_data_dir);
}
void postAppSpecialize(const AppSpecializeArgs
*
) override {
if
(enable_hack) {
std::thread hack_thread(hack_prepare, _data_dir, data, length);
hack_thread.detach();
}
}
private:
Api
*
api;
JNIEnv
*
env;
bool
enable_hack;
char
*
_data_dir;
void
*
data;
size_t length;
void preSpecialize(const char
*
package_name, const char
*
app_data_dir) {
if
(strcmp(package_name, AimPackageName)
=
=
0
) {
LOGI(
"成功注入目标进程: %s"
, package_name);
enable_hack
=
true;
_data_dir
=
new char[strlen(app_data_dir)
+
1
];
strcpy(_data_dir, app_data_dir);
auto path
=
"zygisk/armeabi-v7a.so"
;
auto path
=
"zygisk/arm64-v8a.so"
;
int
dirfd
=
api
-
>getModuleDir();
int
fd
=
openat(dirfd, path, O_RDONLY);
if
(fd !
=
-
1
) {
struct stat sb{};
fstat(fd, &sb);
length
=
sb.st_size;
data
=
mmap(nullptr, length, PROT_READ, MAP_PRIVATE, fd,
0
);
close(fd);
}
else
{
LOGW(
"Unable to open arm file"
);
}
}
else
{
api
-
>setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY);
}
}
};
REGISTER_ZYGISK_MODULE(MyModule)
using zygisk::Api;
using zygisk::AppSpecializeArgs;
using zygisk::ServerSpecializeArgs;
class
MyModule : public zygisk::ModuleBase {
public:
void onLoad(Api
*
api, JNIEnv
*
env) override {
this
-
>api
=
api;
this
-
>env
=
env;
}
void preAppSpecialize(AppSpecializeArgs
*
args) override {
auto package_name
=
env
-
>GetStringUTFChars(args
-
>nice_name, nullptr);
auto app_data_dir
=
env
-
>GetStringUTFChars(args
-
>app_data_dir, nullptr);
LOGI(
"preAppSpecialize %s %s"
, package_name, app_data_dir);
preSpecialize(package_name, app_data_dir);
env
-
>ReleaseStringUTFChars(args
-
>nice_name, package_name);
env
-
>ReleaseStringUTFChars(args
-
>app_data_dir, app_data_dir);
}
void postAppSpecialize(const AppSpecializeArgs
*
) override {
if
(enable_hack) {
std::thread hack_thread(hack_prepare, _data_dir, data, length);
hack_thread.detach();
}
}
private:
Api
*
api;
JNIEnv
*
env;
bool
enable_hack;
char
*
_data_dir;
void
*
data;
size_t length;
void preSpecialize(const char
*
package_name, const char
*
app_data_dir) {
if
(strcmp(package_name, AimPackageName)
=
=
0
) {
LOGI(
"成功注入目标进程: %s"
, package_name);
enable_hack
=
true;
_data_dir
=
new char[strlen(app_data_dir)
+
1
];
strcpy(_data_dir, app_data_dir);
auto path
=
"zygisk/armeabi-v7a.so"
;
auto path
=
"zygisk/arm64-v8a.so"
;
int
dirfd
=
api
-
>getModuleDir();
int
fd
=
openat(dirfd, path, O_RDONLY);
if
(fd !
=
-
1
) {
struct stat sb{};
fstat(fd, &sb);
length
=
sb.st_size;
data
=
mmap(nullptr, length, PROT_READ, MAP_PRIVATE, fd,
0
);
close(fd);
}
else
{
LOGW(
"Unable to open arm file"
);
}
}
else
{
api
-
>setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY);
}
}
};
REGISTER_ZYGISK_MODULE(MyModule)
void preAppSpecialize(AppSpecializeArgs
*
args) override {
auto package_name
=
env
-
>GetStringUTFChars(args
-
>nice_name, nullptr);
auto app_data_dir
=
env
-
>GetStringUTFChars(args
-
>app_data_dir, nullptr);
LOGI(
"preAppSpecialize %s %s"
, package_name, app_data_dir);
preSpecialize(package_name, app_data_dir);
env
-
>ReleaseStringUTFChars(args
-
>nice_name, package_name);
env
-
>ReleaseStringUTFChars(args
-
>app_data_dir, app_data_dir);
}
void preAppSpecialize(AppSpecializeArgs
*
args) override {
auto package_name
=
env
-
>GetStringUTFChars(args
-
>nice_name, nullptr);
auto app_data_dir
=
env
-
>GetStringUTFChars(args
-
>app_data_dir, nullptr);
LOGI(
"preAppSpecialize %s %s"
, package_name, app_data_dir);
preSpecialize(package_name, app_data_dir);
env
-
>ReleaseStringUTFChars(args
-
>nice_name, package_name);
env
-
>ReleaseStringUTFChars(args
-
>app_data_dir, app_data_dir);
}
if
(strcmp(package_name, AimPackageName)
=
=
0
) {
LOGI(
"成功注入目标进程: %s"
, package_name);
enable_hack
=
true;
_data_dir
=
new char[strlen(app_data_dir)
+
1
];
strcpy(_data_dir, app_data_dir);
if
(strcmp(package_name, AimPackageName)
=
=
0
) {
LOGI(
"成功注入目标进程: %s"
, package_name);
enable_hack
=
true;
_data_dir
=
new char[strlen(app_data_dir)
+
1
];
strcpy(_data_dir, app_data_dir);
void hack_start(const char
*
game_data_dir,JavaVM
*
vm) {
bool
load
=
false;
LOGI(
"hack_start %s"
, game_data_dir);
/
/
构建新文件路径
char new_so_path[
256
];
snprintf(new_so_path, sizeof(new_so_path),
"%s/files/%s.so"
, game_data_dir,
"test"
);
/
/
复制
/
sdcard
/
test.so 到 game_data_dir 并重命名
const char
*
src_path
=
"/data/local/tmp/test.so"
;
int
src_fd
=
open
(src_path, O_RDONLY);
if
(src_fd <
0
) {
LOGE(
"Failed to open %s: %s (errno: %d)"
, src_path, strerror(errno), errno);
return
;
}
int
dest_fd
=
open
(new_so_path, O_WRONLY | O_CREAT | O_TRUNC,
0644
);
if
(dest_fd <
0
) {
LOGE(
"Failed to open %s"
, new_so_path);
close(src_fd);
return
;
}
/
/
复制文件内容
char
buffer
[
4096
];
ssize_t bytes;
while
((bytes
=
read(src_fd,
buffer
, sizeof(
buffer
))) >
0
) {
if
(write(dest_fd,
buffer
, bytes) !
=
bytes) {
LOGE(
"Failed to write to %s"
, new_so_path);
close(src_fd);
close(dest_fd);
return
;
}
}
close(src_fd);
close(dest_fd);
if
(chmod(new_so_path,
0755
) !
=
0
) {
LOGE(
"Failed to change permissions on %s: %s (errno: %d)"
, new_so_path, strerror(errno), errno);
return
;
}
else
{
LOGI(
"Successfully changed permissions to 755 on %s"
, new_so_path);
}
void
*
handle;
/
/
使用 xdl_open 打开新复制的 so 文件
for
(
int
i
=
0
; i <
10
; i
+
+
) {
/
/
void
*
handle
=
xdl_open(new_so_path,
0
);
handle
=
dlopen(new_so_path, RTLD_NOW | RTLD_LOCAL);
if
(handle) {
LOGI(
"Successfully loaded %s"
, new_so_path);
load
=
true;
break
;
}
else
{
LOGE(
"Failed to load %s: %s"
, new_so_path, dlerror());
sleep(
1
);
}
}
if
(!load) {
LOGI(
"test.so not found in thread %d"
, gettid());
}
void (
*
JNI_OnLoad)(JavaVM
*
, void
*
);
*
(void
*
*
) (&JNI_OnLoad)
=
dlsym(handle,
"JNI_OnLoad"
);
if
(JNI_OnLoad) {
LOGI(
"JNI_OnLoad symbol found, calling JNI_OnLoad."
);
JNI_OnLoad(vm, NULL);
}
else
{
LOGE(
"JNI_OnLoad symbol not found in %s"
, new_so_path);
}
}
void hack_start(const char
*
game_data_dir,JavaVM
*
vm) {
bool
load
=
false;
LOGI(
"hack_start %s"
, game_data_dir);
/
/
构建新文件路径
char new_so_path[
256
];
snprintf(new_so_path, sizeof(new_so_path),
"%s/files/%s.so"
, game_data_dir,
"test"
);
/
/
复制
/
sdcard
/
test.so 到 game_data_dir 并重命名
const char
*
src_path
=
"/data/local/tmp/test.so"
;
int
src_fd
=
open
(src_path, O_RDONLY);
if
(src_fd <
0
) {
LOGE(
"Failed to open %s: %s (errno: %d)"
, src_path, strerror(errno), errno);
return
;
}
int
dest_fd
=
open
(new_so_path, O_WRONLY | O_CREAT | O_TRUNC,
0644
);
if
(dest_fd <
0
) {
LOGE(
"Failed to open %s"
, new_so_path);
close(src_fd);
return
;
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2024-10-5 15:28
被棕熊编辑
,原因: 整理排版