From 82f2bc240d4d1077212122d7b141b1e938d15ff8 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Sun, 24 Oct 2021 04:46:46 -0700 Subject: [PATCH] Add Zygisk module files --- .gitmodules | 3 + README.md | 45 +++++++ build.gradle | 2 +- module/jni/Android.mk | 14 +- module/jni/example.cpp | 58 ++++++++ module/jni/libcxx | 1 + module/jni/zygisk.hpp | 294 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 414 insertions(+), 3 deletions(-) create mode 100644 .gitmodules create mode 100644 README.md create mode 160000 module/jni/libcxx create mode 100644 module/jni/zygisk.hpp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9017ca2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libcxx"] + path = module/jni/libcxx + url = https://github.com/topjohnwu/libcxx.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c64db3 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Developing Zygisk Modules + +This repository hosts a template zygisk module for developers to start developing Zygisk modules. Before developing Zygisk modules, you should first check out the official documentation for [Magisk Modules](https://topjohnwu.github.io/Magisk/guides.html). Do not fork this repository for your new module; either manually clone this repository, or press the "Use this template" button in the GitHub UI. + +This repository is archived because it is meant to be read-only; the project is not abandoned. For any issues, please report them to the main Magisk repository. + +## API + +- The canonical URL of the public Zygisk API is [module/jni/zygisk.hpp](https://github.com/topjohnwu/zygisk-module-sample/blob/master/module/jni/zygisk.hpp). +- The header file is self documented; directly refer to the header source code for all Zygisk API details. +- Magisk is committed to maintain backwards compatibility forever. That is, whenever there is an API update for Zygisk in a newer Magisk version, Magisk can always load Zygisk modules built for an older Zygisk API. + +## Notes + +- This repository can be opened with Android Studio. +- Developing Zygisk modules requires a modern C++ compiler. Please use NDK r21 or higher. +- All the C++ code is in the [module/jni](https://github.com/topjohnwu/zygisk-module-sample/tree/master/module/jni) folder. +- DO NOT modify the default configurations in `Application.mk` unless you know what you are doing. + +## C++ STL + +- The `APP_STL` variable in `Application.mk` is set to `none`. **DO NOT** use any C++ STL included in NDK. +- If you'd like to use C++ STL, you **have to** use the `libcxx` included as a git submodule in this repository. Zygisk modules' code are injected into Zygote, and the included `libc++` is setup to be lightweight and fully self contained that prevents conflicts with the hosting program. +- If you do not need STL, link to the system `libstdc++` so that you can at least call the `new` operator. +- Both configurations are demonstrated in the example `Android.mk`. + +## Building + +- In the `module` folder, call [`ndk-build`](https://developer.android.com/ndk/guides/ndk-build) to compile your modules. +- Your module libraries will be in `libs//lib.so`. +- Copy the libraries into your module's `zygisk` folder, with the ABI as it's file name: + +``` +module_id +├── module.prop +└── zygisk +    ├── arm64-v8a.so +    ├── armeabi-v7a.so +    ├── x86.so +    └── x86_64.so +``` + +## License + +Although the main Magisk project is licensed under GPLv3, the Zygisk API and its headers are not. Every file in this repository is released to the public domain, so you don't have to worry about any licensing issues while developing Zygisk modules. You can create a closed source module, or publish your source code under any open source license you prefer; there is no restrictions at all. diff --git a/build.gradle b/build.gradle index d7661cf..8ca4829 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath "com.android.tools.build:gradle:7.0.2" + classpath "com.android.tools.build:gradle:7.0.3" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/module/jni/Android.mk b/module/jni/Android.mk index 403c184..0e485e7 100644 --- a/module/jni/Android.mk +++ b/module/jni/Android.mk @@ -1,9 +1,19 @@ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) - LOCAL_MODULE := example LOCAL_SRC_FILES := example.cpp +LOCAL_STATIC_LIBRARIES := libcxx LOCAL_LDLIBS := -llog - include $(BUILD_SHARED_LIBRARY) + +include jni/libcxx/Android.mk + +# If you do not want to use libc++, link to system stdc++ +# so that you can at least call the new operator in your code + +# include $(CLEAR_VARS) +# LOCAL_MODULE := example +# LOCAL_SRC_FILES := example.cpp +# LOCAL_LDLIBS := -llog -lstdc++ +# include $(BUILD_SHARED_LIBRARY) diff --git a/module/jni/example.cpp b/module/jni/example.cpp index e69de29..0c29ce2 100644 --- a/module/jni/example.cpp +++ b/module/jni/example.cpp @@ -0,0 +1,58 @@ +#include +#include +#include +#include + +#include "zygisk.hpp" + +using zygisk::Api; +using zygisk::AppSpecializeArgs; +using zygisk::ServerSpecializeArgs; + +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "Magisk", __VA_ARGS__) + +class MyModule : public zygisk::ModuleBase { +public: + void onLoad(Api *api, JNIEnv *env) override { + this->api = api; + this->env = env; + } + + void preAppSpecialize(AppSpecializeArgs *args) override { + // Use JNI to fetch our process name + const char *process = env->GetStringUTFChars(args->nice_name, nullptr); + preSpecialize(process); + env->ReleaseStringUTFChars(args->nice_name, process); + } + + void preServerSpecialize(ServerSpecializeArgs *args) override { + preSpecialize("system_server"); + } + +private: + Api *api; + JNIEnv *env; + + void preSpecialize(const char *process) { + // Demonstrate connecting to to companion process + // We ask the companion for a random number + int r = 0; + int fd = api->connectCompanion(); + read(fd, &r, sizeof(r)); + close(fd); + LOGD("example: process=[%s], r=[%u]\n", process, r); + } + +}; + +static void companion_handler(int i) { + int fd = open("/dev/urandom", O_RDONLY); + int r; + read(fd, &r, sizeof(r)); + close(fd); + LOGD("example: companion r=[%u]\n", r); + write(i, &r, sizeof(r)); +} + +REGISTER_ZYGISK_MODULE(MyModule) +REGISTER_ZYGISK_COMPANION(companion_handler) diff --git a/module/jni/libcxx b/module/jni/libcxx new file mode 160000 index 0000000..b74fd59 --- /dev/null +++ b/module/jni/libcxx @@ -0,0 +1 @@ +Subproject commit b74fd5905f1064c1a33b213fca58b45441651ad1 diff --git a/module/jni/zygisk.hpp b/module/jni/zygisk.hpp new file mode 100644 index 0000000..edf3acb --- /dev/null +++ b/module/jni/zygisk.hpp @@ -0,0 +1,294 @@ +// This is the public API for Zygisk modules. +// DO NOT MODIFY ANY CODE IN THIS HEADER. + +#pragma once + +#include + +#define ZYGISK_API_VERSION 1 + +/* + +Define a class and inherit zygisk::ModuleBase to implement the functionality of your module. +Use the macro REGISTER_ZYGISK_MODULE(className) to register that class to Zygisk. + +Please note that modules will only be loaded after zygote has forked the child process. +THIS MEANS ALL OF YOUR CODE RUNS IN THE APP/SYSTEM SERVER PROCESS, NOT THE ZYGOTE DAEMON! + +Example code: + +static jint (*orig_logger_entry_max)(JNIEnv *env); +static jint my_logger_entry_max(JNIEnv *env) { return orig_logger_entry_max(env); } + +static void example_handler(int socket) { ... } + +class ExampleModule : public zygisk::ModuleBase { +public: + void onLoad(zygisk::Api *api, JNIEnv *env) override { + this->api = api; + this->env = env; + } + void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { + JNINativeMethod methods[] = { + { "logger_entry_max_payload_native", "()I", (void*) my_logger_entry_max }, + }; + api->hookJniNativeMethods(env, "android/util/Log", methods, 1); + *(void **) &orig_logger_entry_max = methods[0].fnPtr; + } +private: + zygisk::Api *api; + JNIEnv *env; +}; + +REGISTER_ZYGISK_MODULE(ExampleModule) + +REGISTER_ZYGISK_COMPANION(example_handler) + +*/ + +namespace zygisk { + +struct Api; +struct AppSpecializeArgs; +struct ServerSpecializeArgs; + +class ModuleBase { +public: + + // This function is called when the module is loaded into the target process. + // A Zygisk API handle will be sent as an argument; call utility functions or interface + // with Zygisk through this handle. + virtual void onLoad(Api *api, JNIEnv *env) {} + + // This function is called before the app process is specialized. + // At this point, the process just got forked from zygote, but no app specific specialization + // is applied. This means that the process does not have any sandbox restrictions and + // still runs with the same privilege of zygote. + // + // All the arguments that will be sent and used for app specialization is passed as a single + // AppSpecializeArgs object. You can read and overwrite these arguments to change how the app + // process will be specialized. + // + // If you need to run some operations as superuser, you can call Api::connectCompanion() to + // get a socket to do IPC calls with a root companion process. + // See Api::connectCompanion() for more info. + virtual void preAppSpecialize(AppSpecializeArgs *args) {} + + // This function is called after the app process is specialized. + // At this point, the process has all sandbox restrictions enabled for this application. + // This means that this function runs as the same privilege of the app's own code. + virtual void postAppSpecialize(const AppSpecializeArgs *args) {} + + // This function is called before the system server process is specialized. + // See preAppSpecialize(args) for more info. + virtual void preServerSpecialize(ServerSpecializeArgs *args) {} + + // This function is called after the system server process is specialized. + // At this point, the process runs with the privilege of system_server. + virtual void postServerSpecialize(const ServerSpecializeArgs *args) {} +}; + +struct AppSpecializeArgs { + // Required arguments. These arguments are guaranteed to exist on all Android versions. + jint &uid; + jint &gid; + jintArray &gids; + jint &runtime_flags; + jint &mount_external; + jstring &se_info; + jstring &nice_name; + jstring &instruction_set; + jstring &app_data_dir; + + // Optional arguments. Please check whether the pointer is null before de-referencing + jboolean *const is_child_zygote; + jboolean *const is_top_app; + jobjectArray *const pkg_data_info_list; + jobjectArray *const whitelisted_data_info_list; + jboolean *const mount_data_dirs; + jboolean *const mount_storage_dirs; + + AppSpecializeArgs() = delete; +}; + +struct ServerSpecializeArgs { + jint &uid; + jint &gid; + jintArray &gids; + jint &runtime_flags; + jlong &permitted_capabilities; + jlong &effective_capabilities; + + ServerSpecializeArgs() = delete; +}; + +namespace internal { +struct api_table; +template void entry_impl(api_table *, JNIEnv *); +} + +// These values are used in Api::setOption(Option) +enum Option : int { + // Force Magisk's denylist unmount routines to run on this process. + // + // Setting this option only makes sense in preAppSpecialize. + // The actual unmounting happens during app process specialization. + // + // Processes added to Magisk's denylist will have all Magisk and its modules' files unmounted + // from its mount namespace. In addition, all Zygisk code will be unloaded from memory, which + // also implies that no Zygisk modules (including yours) are loaded. + // + // However, if for any reason your module still wants the unmount part of the denylist + // operation to be enabled EVEN IF THE PROCESS IS NOT ON THE DENYLIST, set this option. + FORCE_DENYLIST_UNMOUNT = 0, + + // When this option is set, your module's library will be dlclose-ed after post[XXX]Specialize. + // Be aware that after dlclose-ing your module, all of your code will be unmapped. + // YOU SHOULD NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTION IN THE PROCESS. + DLCLOSE_MODULE_LIBRARY = 1, +}; + +struct Api { + + // Connect to a root companion process and get a Unix domain socket for IPC. + // + // This API only works in the pre[XXX]Specialize functions due to SELinux restrictions. + // + // The pre[XXX]Specialize functions run with the same privilege of zygote. + // If you would like to do some operations with superuser permissions, register a handler + // function that would be called in the root process with REGISTER_ZYGISK_COMPANION(func). + // Another good use case for a companion process is that if you want to share some resources + // across multiple processes, hold the resources in the companion process and pass it over. + // + // Returns a file descriptor to a socket that is connected to the socket passed to your + // module's companion request handler. Returns -1 if the connection attempt failed. + int connectCompanion(); + + // Set various options for your module. + // Please note that this function accepts one single option at a time. + // Check zygisk::Option for the full list of options available. + void setOption(Option opt); + + // Hook JNI native methods for a class + // + // Lookup all registered JNI native methods and replace it with your own functions. + // The original function pointer will be saved in each JNINativeMethod's fnPtr. + // If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr + // will be set to nullptr. + void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods); + + // For ELFs loaded in memory matching `regex`, replace function `symbol` with `newFunc`. + // If `oldFunc` is not nullptr, the original function pointer will be saved to `oldFunc`. + void pltHookRegister(const char *regex, const char *symbol, void *newFunc, void **oldFunc); + + // For ELFs loaded in memory matching `regex`, exclude hooks registered for `symbol`. + // If `symbol` is nullptr, then all symbols will be excluded. + void pltHookExclude(const char *regex, const char *symbol); + + // Commit all the hooks that was previously registered. + // Returns false if an error occurred. + bool pltHookCommit(); + +private: + internal::api_table *impl; + template friend void internal::entry_impl(internal::api_table *, JNIEnv *); +}; + +// Register a class as a Zygisk module + +#define REGISTER_ZYGISK_MODULE(clazz) \ +void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \ + zygisk::internal::entry_impl(table, env); \ +} + +// Register a root companion request handler function for your module +// +// The function runs in a superuser daemon process and handles a root companion request from +// your module running in a target process. The function has to accept an integer value, +// which is a socket that is connected to the target process. +// See Api::connectCompanion() for more info. +// +// NOTE: the function can run concurrently on multiple threads. +// Be aware of race conditions if you have a globally shared resource. + +#define REGISTER_ZYGISK_COMPANION(func) \ +void zygisk_companion_entry(int client) { func(client); } + +/************************************************************************************ + * All the code after this point is internal code used to interface with Zygisk + * and guarantee ABI stability. You do not have to understand what it is doing. + ************************************************************************************/ + +namespace internal { + +struct module_abi { + long api_version; + ModuleBase *_this; + + void (*preAppSpecialize)(ModuleBase *, AppSpecializeArgs *); + void (*postAppSpecialize)(ModuleBase *, const AppSpecializeArgs *); + void (*preServerSpecialize)(ModuleBase *, ServerSpecializeArgs *); + void (*postServerSpecialize)(ModuleBase *, const ServerSpecializeArgs *); + + module_abi(ModuleBase *module) : api_version(ZYGISK_API_VERSION), _this(module) { + preAppSpecialize = [](auto self, auto args) { self->preAppSpecialize(args); }; + postAppSpecialize = [](auto self, auto args) { self->postAppSpecialize(args); }; + preServerSpecialize = [](auto self, auto args) { self->preServerSpecialize(args); }; + postServerSpecialize = [](auto self, auto args) { self->postServerSpecialize(args); }; + } +}; + +struct api_table { + // These first 2 entries are permanent, shall never change + void *_this; + bool (*registerModule)(api_table *, module_abi *); + + // Utility functions + void (*hookJniNativeMethods)(JNIEnv *, const char *, JNINativeMethod *, int); + void (*pltHookRegister)(const char *, const char *, void *, void **); + void (*pltHookExclude)(const char *, const char *); + bool (*pltHookCommit)(); + + // Zygisk functions + int (*connectCompanion)(void * /* _this */); + void (*setOption)(void * /* _this */, Option); +}; + +template +void entry_impl(api_table *table, JNIEnv *env) { + ModuleBase *module = new T(); + if (!table->registerModule(table, new module_abi(module))) + return; + auto api = new Api(); + api->impl = table; + module->onLoad(api, env); +} + +} // namespace internal + +int Api::connectCompanion() { + return impl->connectCompanion(impl->_this); +} +void Api::setOption(Option opt) { + impl->setOption(impl->_this, opt); +} +void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) { + impl->hookJniNativeMethods(env, className, methods, numMethods); +} +void Api::pltHookRegister(const char *regex, const char *symbol, void *newFunc, void **oldFunc) { + impl->pltHookRegister(regex, symbol, newFunc, oldFunc); +} +void Api::pltHookExclude(const char *regex, const char *symbol) { + impl->pltHookExclude(regex, symbol); +} +bool Api::pltHookCommit() { + return impl->pltHookCommit(); +} + +} // namespace zygisk + +[[gnu::visibility("default")]] [[gnu::used]] +extern "C" void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *); + +[[gnu::visibility("default")]] [[gnu::used]] +extern "C" void zygisk_companion_entry(int);