首页
社区
课程
招聘
[原创]关于安卓注入几种方式的讨论,开源注入模块实现
发表于: 2024-10-5 12:30 1992

[原创]关于安卓注入几种方式的讨论,开源注入模块实现

2024-10-5 12:30
1992

概念

在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框架

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
        }
}

这种方式的优点:

免root、便于分发、打包速度一般

缺点:

对于签名检测的pass难度比较高

动态注入(基于系统提供的调试API注入)

ptrace注入

**由于Android是基于linux内核的操作系统,所以Android下的注入也是基于Linux下的系统调用函数ptrace()实现的。**即在获得root权限后,通过ptrace()系统调用将stub(桩代码)注入到指定pid的进程中。

常见使用工具:IDA、GDB、LLDB、Frida等常见工具

我们也可以自己写一个ptrace简单的注入so,下面我给出一个项目,感兴趣的大佬可以自己编译进行尝试。

这里进行预告:后面我会自己写一个调试器(基于ptrace),会写出文章进行分享,目前已经在做了。

这里简单附上几篇ptrace的文章,感兴趣的大佬可以尝试。

因为我研究的实在是不多。

https://blog.csdn.net/hp910315/article/details/77335058

https://blog.csdn.net/jinzhuojun/article/details/9900105

这种方式的优点:

注入速度快,注入不容易检测到(ptrace注入完成以后直接取消ptrace,在后面检测不到)

缺点:

需要root、有一定的ptrace检测(像ida这样的注入,会在maps扫描到当前正在被调试)

attach方式被ptrace占坑方式搞得不好绕过(ida表示非常难受)。

zygote注入

常见使用工具:xposed 实现工具:Riru(早期)、Zygisk(常用)

zygote注入是属于全局注入的方式,它主要是依赖于fork()子进程方式进行注入的。
目前市面上比较成熟的注入工具xposed就是基于zygote的全局注入。

它有两大优点:主要在于zygote是系统进程,通过系统进程fork出来后它就具备隐蔽性,强大性。

常见的一些工具都是使用Zygisk注入,比如知名的开源项目Zygisk-Il2CppDumper

以及寒冰大佬开发的FrdiaManager 还有Xposed框架都支持Zygsik注入

下面我来讲一下我开发的模块是如何注入自己的so的(本模块是基于**Zygisk-Il2CppDumper项目进行修改,因为作者写的Gradle实在是太好用啦)**

系统注入(修改AOSP源码,进行插桩)

通过修改aosp系统的源码,在app加载之前插桩语句,加载自定义库。

后面会有一个小模块进行讨论。

Zygisk自定义注入so(dex)插件的实现

模块开发前置知识

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
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) {}
};

重点就是这几个api, 看注释理解
用最通俗粗略的理解来表示的话:
pre是刚从zygote fork出来没有沙箱限制的时候
postAppSpecialize 相当于app进程启动, 这里可以做自定义dex加载的一些动作
postServerSpecialize 相当于系统服务也就是system server 运行

官方提供了一个https://github.com/topjohnwu/zygisk-module-sample案例,也可以读一读

模块实现细节:

实现原理非常简单:

从app可以访问的路径copy要注入的so到自己的私有目录(因为有selinux的限制)

之后使用dl_open加载目标so

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
78
79
80
81
#include <cstring>
#include <thread>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cinttypes>
#include "hack.h"
#include "zygisk.hpp"
#include "game.h"
#include "log.h"
#include "dlfcn.h"
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);
 
#if defined(__i386__)
            auto path = "zygisk/armeabi-v7a.so";
#endif
#if defined(__x86_64__)
            auto path = "zygisk/arm64-v8a.so";
#endif
#if defined(__i386__) || defined(__x86_64__)
            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");
            }
#endif
        } else {
            api->setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY);
        }
    }
};
 
REGISTER_ZYGISK_MODULE(MyModule)

这里主要实现了面具模块主要提供的api

1
2
3
4
5
6
7
8
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);
   }

我们的实现主要是这个实现的函数,此时app已经处于沙盒中了,只有app自身的权限

1
2
3
4
5
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);

在这里我们需要修改要注入的包名,不然模块不会进一步注入

CleanShot 2024-10-05 at 11.46.58.png

主要功能实现

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
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);
    }
 
}

复制过程,主要就是把/data/local/tmp/test.so 复制到私有目录,然后修改权限为0755 不然dlopen没法加载

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
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);
    }

尝试打开十次,获取到so的handle

1
2
3
4
5
6
7
8
9
10
11
12
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);
        }
    }

寻找符号并执行

1
2
3
4
5
6
7
8
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);
   }

可以自己写一个自己的函数在用dlsym调用,这里就不多说了

源码导入android studio就可以构建出面具模块了,再次感谢原作者的项目

CleanShot 2024-10-05 at 12.05.49.png

使用方法:

在编译之前应该修改目标app的包名,如果不修改不会注入(后面会考虑做一个和shamiko一样的黑白名单)初代版本大家先手动修改

编译自己的插件so实现自己的功能

这里需要了解的是dlopen的加载流程。见番外篇。

当然可以修改插件,使用dlsym找到自己函数的符号,手动加载。

我已经实现了JNI_ONLOAD的加载。

移动so到/data/local/tmp目录下 命名为test.so

享受注入!

插件so源码:

CleanShot 2024-10-05 at 12.10.51.png

1
2
3
4
5
__attribute__((constructor))
void my_init_function() {
    std::string hello = "我来自其他模块";
    __android_log_print(6, "jiqiu2021", "%s", hello.c_str());
}

注入效果:

CleanShot 2024-10-05 at 12.12.07.png

番外一: dlopen的简单解析

http://aospxref.com/android-12.0.0_r3/xref/bionic/libdl/libdl.cpp

CleanShot 2024-10-05 at 12.13.00.png

之后调用

CleanShot 2024-10-05 at 12.13.17.png

之后调用

CleanShot 2024-10-05 at 12.13.32.png

来到真正的dlopen加载的地方

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
void* do_dlopen(const char* name, int flags,
2064                  const android_dlextinfo* extinfo,
2065                  const void* caller_addr) {
2066    std::string trace_prefix = std::string("dlopen: ") + (name == nullptr ? "(nullptr)" : name);
2067    ScopedTrace trace(trace_prefix.c_str());
2068    ScopedTrace loading_trace((trace_prefix + " - loading and linking").c_str());
2069    soinfo* const caller = find_containing_library(caller_addr);
2070    android_namespace_t* ns = get_caller_namespace(caller);
2071 
2072    LD_LOG(kLogDlopen,
2073           "dlopen(name=\"%s\", flags=0x%x, extinfo=%s, caller=\"%s\", caller_ns=%s@%p, targetSdkVersion=%i) ...",
2074           name,
2075           flags,
2076           android_dlextinfo_to_string(extinfo).c_str(),
2077           caller == nullptr ? "(null)" : caller->get_realpath(),
2078           ns == nullptr ? "(null)" : ns->get_name(),
2079           ns,
2080           get_application_target_sdk_version());
2081 
2082    auto purge_guard = android::base::make_scope_guard([&]() { purge_unused_memory(); });
2083 
2084    auto failure_guard = android::base::make_scope_guard(
2085        [&]() { LD_LOG(kLogDlopen, "... dlopen failed: %s", linker_get_error_buffer()); });
2086 
2087    if ((flags & ~(RTLD_NOW|RTLD_LAZY|RTLD_LOCAL|RTLD_GLOBAL|RTLD_NODELETE|RTLD_NOLOAD)) != 0) {
2088      DL_OPEN_ERR("invalid flags to dlopen: %x", flags);
2089      return nullptr;
2090    }
2091 
2092    if (extinfo != nullptr) {
2093      if ((extinfo->flags & ~(ANDROID_DLEXT_VALID_FLAG_BITS)) != 0) {
2094        DL_OPEN_ERR("invalid extended flags to android_dlopen_ext: 0x%" PRIx64, extinfo->flags);
2095        return nullptr;
2096      }
2097 
2098      if ((extinfo->flags & ANDROID_DLEXT_USE_LIBRARY_FD) == 0 &&
2099          (extinfo->flags & ANDROID_DLEXT_USE_LIBRARY_FD_OFFSET) != 0) {
2100        DL_OPEN_ERR("invalid extended flag combination (ANDROID_DLEXT_USE_LIBRARY_FD_OFFSET without "
2101            "ANDROID_DLEXT_USE_LIBRARY_FD): 0x%" PRIx64, extinfo->flags);
2102        return nullptr;
2103      }
2104 
2105      if ((extinfo->flags & ANDROID_DLEXT_USE_NAMESPACE) != 0) {
2106        if (extinfo->library_namespace == nullptr) {
2107          DL_OPEN_ERR("ANDROID_DLEXT_USE_NAMESPACE is set but extinfo->library_namespace is null");
2108          return nullptr;
2109        }
2110        ns = extinfo->library_namespace;
2111      }
2112    }
2113 
2114    // Workaround for dlopen(/system/lib/<soname>) when .so is in /apex. http://b/121248172
2115    // The workaround works only when targetSdkVersion < Q.
2116    std::string name_to_apex;
2117    if (translateSystemPathToApexPath(name, &name_to_apex)) {
2118      const char* new_name = name_to_apex.c_str();
2119      LD_LOG(kLogDlopen, "dlopen considering translation from %s to APEX path %s",
2120             name,
2121             new_name);
2122      // Some APEXs could be optionally disabled. Only translate the path
2123      // when the old file is absent and the new file exists.
2124      // TODO(b/124218500): Re-enable it once app compat issue is resolved
2125      /*
2126      if (file_exists(name)) {
2127        LD_LOG(kLogDlopen, "dlopen %s exists, not translating", name);
2128      } else
2129      */
2130      if (!file_exists(new_name)) {
2131        LD_LOG(kLogDlopen, "dlopen %s does not exist, not translating",
2132               new_name);
2133      } else {
2134        LD_LOG(kLogDlopen, "dlopen translation accepted: using %s", new_name);
2135        name = new_name;
2136      }
2137    }
2138    // End Workaround for dlopen(/system/lib/<soname>) when .so is in /apex.
2139 
2140    std::string asan_name_holder;
2141 
2142    const char* translated_name = name;
2143    if (g_is_asan && translated_name != nullptr && translated_name[0] == '/') {
2144      char original_path[PATH_MAX];
2145      if (realpath(name, original_path) != nullptr) {
2146        asan_name_holder = std::string(kAsanLibDirPrefix) + original_path;
2147        if (file_exists(asan_name_holder.c_str())) {
2148          soinfo* si = nullptr;
2149          if (find_loaded_library_by_realpath(ns, original_path, true, &si)) {
2150            PRINT("linker_asan dlopen NOT translating \"%s\" -> \"%s\": library already loaded", name,
2151                  asan_name_holder.c_str());
2152          } else {
2153            PRINT("linker_asan dlopen translating \"%s\" -> \"%s\"", name, translated_name);
2154            translated_name = asan_name_holder.c_str();
2155          }
2156        }
2157      }
2158    }
2159 
2160    ProtectedDataGuard guard;
2161    soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
2162    loading_trace.End();
2163 
2164    if (si != nullptr) {
2165      void* handle = si->to_handle();
2166      LD_LOG(kLogDlopen,
2167             "... dlopen calling constructors: realpath=\"%s\", soname=\"%s\", handle=%p",
2168             si->get_realpath(), si->get_soname(), handle);
2169      si->call_constructors();
2170      failure_guard.Disable();
2171      LD_LOG(kLogDlopen,
2172             "... dlopen successful: realpath=\"%s\", soname=\"%s\", handle=%p",
2173             si->get_realpath(), si->get_soname(), handle);
2174      return handle;
2175    }
2176 
2177    return nullptr;
2178  }

这里就是对so的各个段的装载,我们目光聚焦于结尾的部分

CleanShot 2024-10-05 at 12.14.35.png

在这个函数里有:

1
2
3
4
// DT_INIT should be called before DT_INIT_ARRAY if both are present.
547    call_function("DT_INIT", init_func_, get_realpath());
548    call_array("DT_INIT_ARRAY", init_array_, init_array_count_, false, get_realpath());
549 

对DT_INIT和DT_INIT_ARRAY的调用

所以我们dlopen如果成功打开了so,就会对这两个地方调用

所以说插件的入口可以选择在这两个段里 attribute((constructor))

1
2
3
4
5
**__attribute__((constructor))**
void my_init_function() {
    std::string hello = "我来自其他模块";
    __android_log_print(6, "jiqiu2021", "%s", hello.c_str());
}

番外二:定植AOSP进行so的注入

这里我通过修改源码去注入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达到自定义注入

TODO:实战:使用自己写的hook工具分析强检测frida的APP

如果文章反响还不错,我会继续更新一些frida、xposed分析不了的app(被反调试block掉的)

来进一步加深大家对这个框架的使用。

未来的框架的更新方向:

增加第二种注入方式:将插件so打包到框架里,隐藏落地文件的特征。

通过借鉴Riru的注入方式,隐藏注入(对一些厂商管用)

进一步研究完美隐藏方式

项目地址:
https://github.com/jiqiu2022/Zygisk-MyInjector
附件注入的包名已经固定,需要自己编译


[峰会]看雪.第八届安全开发者峰会10月23日上海龙之梦大酒店举办!

最后于 2024-10-5 15:28 被mb_qzwrkwda编辑 ,原因: 整理排版
上传的附件:
收藏
免费 11
支持
分享
最新回复 (17)
雪    币: 1925
活跃值: (2322)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2024-10-5 12:37
0
雪    币: 2199
活跃值: (10153)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
期待实战,怎么定位frida检测和xp检测,现在xp选择加固APP就崩溃了????,frida这是。这种是怎么做检测定位的呢
2024-10-5 12:47
0
雪    币: 3996
活跃值: (1299)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
4
你瞒我瞒 期待实战,怎么定位frida检测和xp检测,现在xp选择加固APP就崩溃了????,frida这是。这种是怎么做检测定位的呢
这些都被厂商特征了,具体检测方法看雪上有,如何定位以及如何用此插件pass我到时候都会讲一下,要做的比较多,会尽快开始。
2024-10-5 12:56
1
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
感谢分享,学习了
2024-10-5 13:13
0
雪    币: 489
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
通过内核模块注入可行吗
2024-10-5 22:11
0
雪    币: 3996
活跃值: (1299)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
7
安卓逆向test 通过内核模块注入可行吗
理论可行,有成果回复你
2024-10-6 04:26
0
雪    币: 1359
活跃值: (2425)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
感谢分享
2024-10-8 11:15
0
雪    币: 2081
活跃值: (2570)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
9

我以前都是用riru做注入 最近也写了一个zygisk的注入 代码思路基本和你一样   tmp目录放了一个txt 判断要注入的app 以及注入的so名称 

1.直接调dlopen

      //    指定要注入的 SO 文件路径
/*          const char* so_path = "/data/data/com.test.xxxxxxx/files/libxxx.so";
          //  const char* so_path =  "/data/local/tmp/libxxx.so";

          // 使用 dlopen 尝试加载 SO 文件
          void *handle = dlopen(so_path, RTLD_NOW); //RTLD_NOW:将库文件中所有的资源都载入内存    RTLD_LAZY:暂时不将库文件中的资源载入内存,使用时才载入。

          if (handle) {
              LOGD("RTLD_LAZY 成功注入 SO 文件: %s", so_path);

              // 查找并调用 JNI_OnLoad
              typedef jint (*JNI_OnLoad_t)(JavaVM*, void*);
              JNI_OnLoad_t JNI_OnLoad = (JNI_OnLoad_t) dlsym(handle, "JNI_OnLoad");
              if (JNI_OnLoad) {
                  JNI_OnLoad(javaVM, nullptr);
                  LOGD("成功调用 JNI_OnLoad");
              } else {
                  LOGD("未找到 JNI_OnLoad 函数");
              }


          } else {
              LOGD("注入 SO 文件失败: %s", dlerror());
          }*/

2.调用load  

  void injectSO(JNIEnv *env) {
    // 获取 System 类
    jclass systemClass = env->FindClass("java/lang/System");

    // 获取 load 方法 ID
    jmethodID loadMethod = env->GetStaticMethodID(systemClass, "load", "(Ljava/lang/String;)V");

    
  

    jstring jSoPath = env->NewStringUTF(so_path.c_str());// 注入 SO 文件路径

    // 调用 System.load 方法注入 SO 文件
    env->CallStaticVoidMethod(systemClass, loadMethod, jSoPath);

    // 检查是否有异常发生
    if (env->ExceptionCheck()) {
        env->ExceptionDescribe();
        env->ExceptionClear();
        LOGD("注入失败: %s - System.load", dlerror());  // 使用 dlerror() 获取详细错误信息
    } else {
        LOGD("注入成功 called System.load");
    }
    env->DeleteLocalRef(jSoPath);  // 释放 jstring 对象
}


2024-10-8 11:37
0
雪    币: 3996
活跃值: (1299)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
10
世界美景 我以前都是用riru做注入 最近也写了一个zygisk的注入 代码思路基本和你一样&nbsp; &nbsp;tmp目录放了一个txt 判断要注入的app 以及注入的so名称&n ...

我打算后面要做隐藏注入和自定义linker注入,感兴趣可以加个联系方式,一起开发一下

最后于 2024-10-8 12:02 被mb_qzwrkwda编辑 ,原因:
2024-10-8 12:01
0
雪    币: 2
活跃值: (1708)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
发个联系方式
2024-10-8 17:31
0
雪    币: 1084
活跃值: (1696)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
学习了
2024-10-8 21:44
0
雪    币: 5
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
师傅能不能请教一下,我用zygisk注入在postAppSpecialize不开线程直接掉dlopen注入so过几秒会闪退,用ptrace注入同一个so不会闪退是为啥
2024-10-9 16:35
0
雪    币: 3996
活跃值: (1299)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
14
mb_fycbgpri 师傅能不能请教一下,我用zygisk注入在postAppSpecialize不开线程直接掉dlopen注入so过几秒会闪退,用ptrace注入同一个so不会闪退是为啥
发一下样本,我去看看
2024-10-9 20:36
0
雪    币: 268
活跃值: (676)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
zygisk注入核心原理之前是改ld_preload,现在好像又成了改nativebridge了。注入方式还有通过修改memmap形式注入的,这个目前我没发现有进程注入不了。
2024-10-10 10:44
0
雪    币: 5
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16
void postAppSpecialize(const AppSpecializeArgs *args) override
    {
        if (enable_hack)
        {
            LOGD("[postAppSpecialize]current PID=%d --- Start Hacking!\n", getpid());
            LOGD("[+] start inject");
            void *handle = dlopen("/data/data/com.xxxxxx.xxxxxx/files/libxxxx.so", RTLD_NOW | RTLD_LOCAL); // 加载你要注入的so
            if (handle == nullptr)
                LOGD("%s\n", dlerror());
            // void *run_hack=nullptr;
            // run_hack=(RUN_HACK)dlsym(handle,"run_hack");//找到你的so中的某个函数
            // if (run_hack==nullptr) LOGE("%s\n",dlerror());
            // run_hack();//执行功能
        }
    }而且如果注入so,libxxxx.so的attribute里什么都不做的话就不会闪退,libxxxx.so里只有有操作就会闪退
2024-10-10 10:47
0
雪    币: 3996
活跃值: (1299)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
17
mb_fycbgpri void postAppSpecialize(const AppSpecializeArgs *args) override { if (enable_hack) ...
方便发一下apk链接吗,我看看解决下
2024-10-10 11:26
0
雪    币: 2886
活跃值: (5400)
能力值: ( LV11,RANK:185 )
在线值:
发帖
回帖
粉丝
18
额,你这个方向目前来看,对抗性上属于死路,因为有些开源项目本身带来的对抗性问题没法解决。
2024-10-10 14:56
0
游客
登录 | 注册 方可回帖
返回
//