diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a4aad87b --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +/gadget/* +!.gitkeep +out \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..c24893ed --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "module/src/jni/libcxx"] + path = module/src/jni/libcxx + url = https://github.com/topjohnwu/libcxx diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..5a364016 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ + +log: + adb logcat -s ZygiskFrida + +deploy: + .\gradlew :module:assembleRelease + adb push ./out/zygiskfrida-v1.0.0-release.zip /sdcard/Download/ + adb shell "su -c magisk --install-module /sdcard/Download/zygiskfrida-v1.0.0-release.zip" + adb shell "reboot" + + +start: + adb shell "adb forward tcp:27052 tcp:27052" diff --git a/README.md b/README.md new file mode 100644 index 00000000..0570961a --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# ZygiskFrida + +Injects frida Gadget using Zygisk, bypasses APK integrity and ptrace-based anti-tamper checks. diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..52adf6e1 --- /dev/null +++ b/build.gradle @@ -0,0 +1,25 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:7.4.2" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +ext { + minSdkVersion = 23 + targetSdkVersion = 32 + + outDir = file("$rootDir/out") +} + + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/cpplint.cfg b/cpplint.cfg new file mode 100644 index 00000000..0c3899d2 --- /dev/null +++ b/cpplint.cfg @@ -0,0 +1,7 @@ +linelength=100 + +filter=-build/c++11 +filter=-build/header_guard +filter=-legal/copyright +filter=-build/include_subdir + diff --git a/gadget/.gitkeep b/gadget/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..52f5917c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,19 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..f6b961fd Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..2ab173ea --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon May 22 11:22:38 CST 2023 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..cccdd3d5 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..f9553162 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/module.gradle b/module.gradle new file mode 100644 index 00000000..90be0c03 --- /dev/null +++ b/module.gradle @@ -0,0 +1,26 @@ +ext { + // Adjust this for the app you want to inject frida gadget into + + // The module will inject gadget with this process name + appPackageName = "com.example.package" + + // The gadget library you want to inject. + // Search for a release matching the major version of your frida client. + // https://github.com/frida/frida/releases + // Download the gadget for the correct architecture of your phone and + // put it extracted as an .so file into the gadget directory. + // The path specified is relative to the gadget directory + // f.e. if you put it into "gadget/libgadget.so", then specify libgadget.so here. + gadgetPath = "libgadget.so" + + + // magisk module + moduleLibraryName = "zygiskfrida" + magiskModuleId = "zygiskfrida" + moduleName = "ZygiskFrida" + moduleAuthor = "lico-n" + moduleDescription = "Injects frida gadget via zygisk (${appPackageName})" + moduleVersion = "v1.0.0" + moduleVersionCode = 1 + modulePackageName = "re.zyg.fri" +} diff --git a/module/.gitignore b/module/.gitignore new file mode 100644 index 00000000..a264cd9a --- /dev/null +++ b/module/.gitignore @@ -0,0 +1,3 @@ +/build +/libs +/obj diff --git a/module/build.gradle b/module/build.gradle new file mode 100644 index 00000000..fc403ade --- /dev/null +++ b/module/build.gradle @@ -0,0 +1,113 @@ +import org.apache.tools.ant.filters.FixCrLfFilter + +import java.nio.file.Paths +import java.nio.file.Files + +apply plugin: 'com.android.library' +apply from: file(rootProject.file('module.gradle')) + +task prepareConfig(type: Copy) { + inputs.file("$rootDir/module.gradle") + + from "$rootDir/template/config.h" + expand([ + header: "// DO NOT EDIT. Generated file from template/config.h. Values from module.gradle\n", + appPackageName: appPackageName, + gadgetPath: gadgetPath, + magiskModuleId: magiskModuleId, + modulePackageName: modulePackageName, + ]) + into "$rootDir/module/src/jni" +} + +tasks.whenTaskAdded { task -> + if (task.name.startsWith('buildNdkBuildRelease')) { + task.dependsOn prepareConfig + } +} + +android { + namespace = "zygisk.frida" + compileSdkVersion rootProject.ext.targetSdkVersion + ndkVersion '25.2.9519653' + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + externalNativeBuild { + cmake { + arguments "-DMODULE_NAME:STRING=$moduleLibraryName" + } + } + } + buildFeatures { + prefab true + } + + externalNativeBuild { + ndkBuild { + path "src/jni/Android.mk" + } + } +} + +afterEvaluate { + android.libraryVariants.forEach { variant -> + def variantCapped = variant.name.capitalize() + def variantLowered = variant.name.toLowerCase() + + def zipName = "${magiskModuleId.replace('_', '-')}-${moduleVersion}-${variantLowered}.zip" + def magiskDir = file("$outDir/magisk_module_$variantLowered") + + assert file("$rootDir/gadget/$gadgetPath").exists() + + task("prepareMagiskFiles${variantCapped}", type: Sync) { + dependsOn("assemble$variantCapped") + + def templatePath = "$rootDir/template/magisk_module" + + into magiskDir + from(templatePath) { + exclude 'module.prop' + } + from(templatePath) { + include 'module.prop' + expand([ + id : magiskModuleId, + name : moduleName, + version : moduleVersion, + versionCode: moduleVersionCode.toString(), + author : moduleAuthor, + description: moduleDescription, + ]) + filter(FixCrLfFilter.class, + eol: FixCrLfFilter.CrLf.newInstance("lf")) + } + from("$buildDir/intermediates/stripped_native_libs/$variantLowered/out/lib") { + into 'lib' + } + from("$rootDir/gadget") { + exclude '.gitkeep' + into 'gadget' + } + doLast { + file("$magiskDir/zygisk").mkdir() + fileTree("$magiskDir/lib").visit { f -> + if (!f.directory) return + def srcPath = Paths.get("${f.file.absolutePath}/lib${moduleLibraryName}.so") + def dstPath = Paths.get("$magiskDir/zygisk/${f.path}.so") + Files.move(srcPath, dstPath) + } + new File("$magiskDir/lib").deleteDir() + } + } + + task("zip${variantCapped}", type: Zip) { + dependsOn("prepareMagiskFiles${variantCapped}") + from magiskDir + archiveFileName.set(zipName) + destinationDirectory.set(outDir) + } + + variant.assembleProvider.get().finalizedBy("zip${variantCapped}") + } +} diff --git a/module/src/jni/Android.mk b/module/src/jni/Android.mk new file mode 100644 index 00000000..82513eef --- /dev/null +++ b/module/src/jni/Android.mk @@ -0,0 +1,11 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_MODULE := zygiskfrida +LOCAL_SRC_FILES := main.cpp inject.cpp prepare.cpp +LOCAL_EXPORT_STATIC_LIBRARIES := libcxx +LOCAL_STATIC_LIBRARIES := libcxx +LOCAL_LDLIBS := -llog +include $(BUILD_SHARED_LIBRARY) + +include src/jni/libcxx/Android.mk diff --git a/module/src/jni/Application.mk b/module/src/jni/Application.mk new file mode 100644 index 00000000..96948f88 --- /dev/null +++ b/module/src/jni/Application.mk @@ -0,0 +1,4 @@ +APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 +APP_CPPFLAGS := -std=c++17 -fno-exceptions -fno-rtti -fvisibility=hidden -fvisibility-inlines-hidden +APP_STL := none +APP_PLATFORM := android-21 diff --git a/module/src/jni/config.h b/module/src/jni/config.h new file mode 100644 index 00000000..130ed6fc --- /dev/null +++ b/module/src/jni/config.h @@ -0,0 +1,12 @@ +// DO NOT EDIT. Generated file from template/config.h. Values from module.gradle +#ifndef ZYGISKFRIDA_CONFIG_H +#define ZYGISKFRIDA_CONFIG_H + +#include + +#define AppPackageName "com.example.package" +#define GadgetPath "libgadget.so" +#define ModulePackageName "re.zyg.fri" +#define MagiskModuleId "zygiskfrida" + +#endif //ZYGISKFRIDA_CONFIG_H diff --git a/module/src/jni/inject.cpp b/module/src/jni/inject.cpp new file mode 100644 index 00000000..5c73d591 --- /dev/null +++ b/module/src/jni/inject.cpp @@ -0,0 +1,95 @@ +#include "inject.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "log.h" +#include "config.h" + +bool should_inject(std::string app_name) { + return app_name == AppPackageName; +} + +extern "C" [[gnu::weak]] struct android_namespace_t * + +__loader_android_create_namespace( + [[maybe_unused]] const char *name, + [[maybe_unused]] const char *ld_library_path, + [[maybe_unused]] const char *default_library_path, + [[maybe_unused]] uint64_t type, + [[maybe_unused]] const char *permitted_when_isolated_path, + [[maybe_unused]] android_namespace_t *parent, + [[maybe_unused]] const void *caller_addr +); + +void *open_gadget(const char *path) { + auto info = android_dlextinfo{}; + + auto *dir = dirname(path); + + if (&__loader_android_create_namespace != nullptr) { + auto *ns = __loader_android_create_namespace( + path, + dir, + nullptr, + 2/*ANDROID_NAMESPACE_TYPE_SHARED*/, + nullptr, + nullptr, + reinterpret_cast(&dlopen)); + + info.flags = ANDROID_DLEXT_USE_NAMESPACE; + info.library_namespace = ns; + } + + return android_dlopen_ext(path, 0, &info);; +} + +std::string get_process_name() { + auto path = "/proc/self/cmdline"; + + std::ifstream file(path); + std::stringstream buffer; + + buffer << file.rdbuf(); + return buffer.str(); +} + +void wait_for_init() { + // wait until the process is renamed to the package name + while (get_process_name().find(AppPackageName) == std::string::npos) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + // additional tolerance for the init to complete after process rename + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} + +void inject_gadget(std::string gadget_path) { + LOGI("Wait for process to complete init"); + + // We need to wait for process initialization to complete. + // Loading the gadget before that will freeze the process + // before the init has completed. This make the process + // undiscoverable or otherwise cause issue attaching. + wait_for_init(); + + LOGI("Starting gadget %s", gadget_path.c_str()); + auto *handle = open_gadget(gadget_path.c_str()); + if (handle) { + LOGI("Gadget connected"); + } else { + LOGE("Failed to start gadget: %s", dlerror()); + } +} + diff --git a/module/src/jni/inject.h b/module/src/jni/inject.h new file mode 100644 index 00000000..f602db2c --- /dev/null +++ b/module/src/jni/inject.h @@ -0,0 +1,9 @@ +#ifndef ZYGISKFRIDA_INJECT_H +#define ZYGISKFRIDA_INJECT_H + +#include + +bool should_inject(std::string app_name); +void inject_gadget(std::string gadget_path); + +#endif // ZYGISKFRIDA_INJECT_H diff --git a/module/src/jni/libcxx b/module/src/jni/libcxx new file mode 160000 index 00000000..82090ae7 --- /dev/null +++ b/module/src/jni/libcxx @@ -0,0 +1 @@ +Subproject commit 82090ae75f7d284f2647a67f3f80f28f54eaddfc diff --git a/module/src/jni/log.h b/module/src/jni/log.h new file mode 100644 index 00000000..4af9b17a --- /dev/null +++ b/module/src/jni/log.h @@ -0,0 +1,12 @@ +#ifndef ZYGISKFRIDA_LOG_H +#define ZYGISKFRIDA_LOG_H + +#include + +#define LOG_TAG "ZygiskFrida" +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) + +#endif // ZYGISKFRIDA_LOG_H diff --git a/module/src/jni/main.cpp b/module/src/jni/main.cpp new file mode 100644 index 00000000..3bc76200 --- /dev/null +++ b/module/src/jni/main.cpp @@ -0,0 +1,59 @@ +#include +#include + +#include "inject.h" +#include "log.h" +#include "prepare.h" +#include "zygisk.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 app_name = env->GetStringUTFChars(args->nice_name, nullptr); + + this->inject = should_inject(app_name); + if (!this->inject) { + this->api->setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY); + this->env->ReleaseStringUTFChars(args->nice_name, app_name); + return; + } + + LOGI("App detected: %s", app_name); + LOGI("Preparing for gadget injection"); + + this->gadget_path = prepare_gadget(); + if (this->gadget_path.empty()) { + LOGE("unexpected error preparing gadget"); + this->inject = false; + this->api->setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY); + this->env->ReleaseStringUTFChars(args->nice_name, app_name); + return; + } + + LOGI("Preparation completed"); + } + + void postAppSpecialize(const AppSpecializeArgs *) override { + if (this->inject) { + std::thread inject_thread(inject_gadget, this->gadget_path); + inject_thread.detach(); + } + } + + private: + Api *api; + JNIEnv *env; + bool inject; + std::string gadget_path; +}; + +REGISTER_ZYGISK_MODULE(MyModule) diff --git a/module/src/jni/prepare.cpp b/module/src/jni/prepare.cpp new file mode 100644 index 00000000..1af5daeb --- /dev/null +++ b/module/src/jni/prepare.cpp @@ -0,0 +1,55 @@ +#include "prepare.h" + +#include + +#include +#include + +#include "log.h" +#include "config.h" + +namespace fs = std::filesystem; + +void sync_directories(std::string destination, std::string source) { + fs::copy( + source, + destination, + fs::copy_options::recursive | fs::copy_options::overwrite_existing); + + // delete old files + std::map new_file_map; + for (const auto &entry : fs::recursive_directory_iterator(source)) { + auto path = fs::relative(entry, source); + new_file_map[path]= true; + } + + for (const auto &entry : fs::recursive_directory_iterator(destination)) { + auto path = fs::relative(entry, destination); + if (!new_file_map[path]) { + fs::remove_all(entry.path()); + } + } +} + +std::string prepare_gadget() { + std::string module_dir = "/data/adb/modules/"; + std::string tmp_dir = "/data/local/tmp/"; + std::string destination = tmp_dir + ModulePackageName; + std::string source = module_dir + MagiskModuleId + "/gadget"; + std::string gadget_path = destination + "/" + GadgetPath; + + mkdir(destination.c_str(), 0755); + fs::permissions( + destination, + fs::perms::owner_all | fs::perms::group_read | + fs::perms::group_exec | fs::perms::others_read | + fs::perms::others_exec, + fs::perm_options::replace); + + + sync_directories(destination, source); + + LOGI("Copied gadget files to %s", destination.c_str()); + + return gadget_path; +} diff --git a/module/src/jni/prepare.h b/module/src/jni/prepare.h new file mode 100644 index 00000000..b919790a --- /dev/null +++ b/module/src/jni/prepare.h @@ -0,0 +1,8 @@ +#ifndef ZYGISKFRIDA_PREPARE_H +#define ZYGISKFRIDA_PREPARE_H + +#include + +std::string prepare_gadget(); + +#endif // ZYGISKFRIDA_PREPARE_H diff --git a/module/src/jni/zygisk.h b/module/src/jni/zygisk.h new file mode 100644 index 00000000..a7383e54 --- /dev/null +++ b/module/src/jni/zygisk.h @@ -0,0 +1,326 @@ +// This is the public API for Zygisk modules. +// DO NOT MODIFY ANY CODE IN THIS HEADER. + +#pragma once + +#include + +#define ZYGISK_API_VERSION 2 + +/* + +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([[maybe_unused]] Api *api, [[maybe_unused]] 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([[maybe_unused]] 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([[maybe_unused]] const AppSpecializeArgs *args) {} + + // This function is called before the system server process is specialized. + // See preAppSpecialize(args) for more info. + virtual void preServerSpecialize([[maybe_unused]] 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([[maybe_unused]] 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. + // + // Set this option to force all Magisk and modules' files to be unmounted from the + // mount namespace of the process, regardless of the denylist enforcement status. + 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 from memory. + // YOU MUST NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTIONS IN THE PROCESS. + DLCLOSE_MODULE_LIBRARY = 1, +}; + +// Bit masks of the return value of Api::getFlags() +enum StateFlag : uint32_t { + // The user has granted root access to the current process + PROCESS_GRANTED_ROOT = (1u << 0), + + // The current process was added on the denylist + PROCESS_ON_DENYLIST = (1u << 1), +}; + +// All API functions will stop working after post[XXX]Specialize as Zygisk will be unloaded +// from the specialized process afterwards. +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. + // + // The root companion process is ABI aware; that is, when calling this function from a 32-bit + // process, you will be connected to a 32-bit companion process, and vice versa for 64-bit. + // + // 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(); + + // Get the file descriptor of the root folder of the current module. + // + // This API only works in the pre[XXX]Specialize functions. + // Accessing the directory returned is only possible in the pre[XXX]Specialize functions + // or in the root companion process (assuming that you sent the fd over the socket). + // Both restrictions are due to SELinux and UID. + // + // Returns -1 if errors occurred. + int getModuleDir(); + + // 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); + + // Get information about the current process. + // Returns bitwise-or'd zygisk::StateFlag values. + uint32_t getFlags(); + + // 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); + int (*getModuleDir)(void * /* _this */); + uint32_t (*getFlags)(void * /* _this */); +}; + +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 + +inline int Api::connectCompanion() { + return impl->connectCompanion ? impl->connectCompanion(impl->_this) : -1; +} +inline int Api::getModuleDir() { + return impl->getModuleDir ? impl->getModuleDir(impl->_this) : -1; +} +inline void Api::setOption(Option opt) { + if (impl->setOption) impl->setOption(impl->_this, opt); +} +inline uint32_t Api::getFlags() { + return impl->getFlags ? impl->getFlags(impl->_this) : 0; +} +inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) { + if (impl->hookJniNativeMethods) impl->hookJniNativeMethods(env, className, methods, numMethods); +} +inline void Api::pltHookRegister(const char *regex, const char *symbol, void *newFunc, void **oldFunc) { + if (impl->pltHookRegister) impl->pltHookRegister(regex, symbol, newFunc, oldFunc); +} +inline void Api::pltHookExclude(const char *regex, const char *symbol) { + if (impl->pltHookExclude) impl->pltHookExclude(regex, symbol); +} +inline bool Api::pltHookCommit() { + return impl->pltHookCommit != nullptr && 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); diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..df6fb335 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,8 @@ +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +include ':module' \ No newline at end of file diff --git a/template/config.h b/template/config.h new file mode 100644 index 00000000..5b7a0af9 --- /dev/null +++ b/template/config.h @@ -0,0 +1,11 @@ +${header}#ifndef ZYGISKFRIDA_CONFIG_H +#define ZYGISKFRIDA_CONFIG_H + +#include + +#define AppPackageName "${appPackageName}" +#define GadgetPath "${gadgetPath}" +#define ModulePackageName "${modulePackageName}" +#define MagiskModuleId "${magiskModuleId}" + +#endif //ZYGISKFRIDA_CONFIG_H diff --git a/template/magisk_module/META-INF/com/google/android/update-binary b/template/magisk_module/META-INF/com/google/android/update-binary new file mode 100644 index 00000000..28b48e58 --- /dev/null +++ b/template/magisk_module/META-INF/com/google/android/update-binary @@ -0,0 +1,33 @@ +#!/sbin/sh + +################# +# Initialization +################# + +umask 022 + +# echo before loading util_functions +ui_print() { echo "$1"; } + +require_new_magisk() { + ui_print "*******************************" + ui_print " Please install Magisk v20.4+! " + ui_print "*******************************" + exit 1 +} + +######################### +# Load util_functions.sh +######################### + +OUTFD=$2 +ZIPFILE=$3 + +mount /data 2>/dev/null + +[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk +. /data/adb/magisk/util_functions.sh +[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk + +install_module +exit 0 diff --git a/template/magisk_module/META-INF/com/google/android/updater-script b/template/magisk_module/META-INF/com/google/android/updater-script new file mode 100644 index 00000000..11d5c96e --- /dev/null +++ b/template/magisk_module/META-INF/com/google/android/updater-script @@ -0,0 +1 @@ +#MAGISK diff --git a/template/magisk_module/module.prop b/template/magisk_module/module.prop new file mode 100644 index 00000000..236bd20a --- /dev/null +++ b/template/magisk_module/module.prop @@ -0,0 +1,6 @@ +id=${id} +name=${name} +version=${version} +versionCode=${versionCode} +author=${author} +description=${description}