Skip to content

Commit

Permalink
allow loading arbitrary so lib files (#8)
Browse files Browse the repository at this point in the history
* feat: add support to inject arbitrary libraries

* adjust readme for arbitrary libraries

* bump module version

* remove unnecessary submodules from ci

* adjust readme
  • Loading branch information
lico-n authored Aug 6, 2023
1 parent d4600c4 commit ee136d4
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 39 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
submodules: 'true'
- uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
with:
distribution: temurin
Expand Down
41 changes: 32 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ more stealthy way.
- The gadget is not embedded into the APK itself. So APK Integrity/Signature checks will still pass.
- The process is not being ptraced like it is with frida-server. Avoiding ptrace based detection.
- Control about the injection time of the gadget.
- Allows you to load multiple arbitrary libraries into the process.

This repo also provides a [Riru](https://github.com/RikkaApps/Riru) flavor in case you are still
using riru with an older magisk version rather than zygisk.
Expand Down Expand Up @@ -40,21 +41,47 @@ might run checks at start up and delaying the injection can help avoid these.
`/data/local/tmp/re.zyg.fri/target_packages` accepts a start up delay in milliseconds.
You can provide it separated by a comma from the package_name.

f.e. `adb shell 'su -c "echo com.example.package,20000 > /data/local/tmp/re.zyg.fri/target_packages"'`
f.e.
```
adb shell 'su -c "echo com.example.package,20000 > /data/local/tmp/re.zyg.fri/target_packages"'
```
would inject the gadget after a delay of 20 seconds.

You get a 10 seconds countdown to injection time in the ZygiskFrida logs `adb logcat -S ZygiskFrida`.
This can help time if you want to time the injection with app interactions.
You get a 10 seconds countdown to injection in the ZygiskFrida logs `adb logcat -S ZygiskFrida`.
This can help if you want to time the injection with app interactions.

**Gadget version and config**

The gadget started is located at `/data/local/tmp/re.zyg.fri/libgadget.so`.\
You can follow the [Gadget Docs](https://frida.re/docs/gadget/) and add an additional
The bundled gadget is located at `/data/local/tmp/re.zyg.fri/libgadget.so`.\
You can follow the [Gadget Docs](https://frida.re/docs/gadget/) and add additional
gadget config and scripts in that location.

In case you want to use a different gadget version than the one bundled, you can simply
replace the `libgadget.so` with your own frida gadget.

**Loading arbitrary libraries**

This module also allows you to load arbitrary .so libraries into the process.\
This can allow you to load additional helper libraries for the gadget or
enable any other use case that might need libraries loaded into the app process.

For this you can add the file `/data/local/tmp/re.zyg.fri/injected_libraries`.\
The file should consist of file paths to libraries.
The libraries are loaded in the order they are specified in the file.

Example file content that would first load libhelperexample.so and then the bundled frida-gadget:
```
/data/local/tmp/re.zyg.fri/libhelperexample.so
/data/local/tmp/re.zyg.fri/libgadget.so
```

Make sure the libraries are located somewhere accessible by the app and that
file permissions are properly set.

If you want the frida gadget to start, you need to explicitly specify the bundled frida-gadget at
`/data/local/tmp/re.zyg.fri/libgadget.so`.\
You can also choose to specify your own gadget this way or omit the gadget altogether.

## How to build

- Checkout the project
Expand All @@ -67,10 +94,6 @@ You can also build and install the module to your device directly with `./gradle

- For emulators this will start the gadget in native realm. This means that you will be able to hook Java but not native functions.

- This is not yet tested very well on different devices.\
In case this is not working reports with logs `adb logcat -S ZygiskFrida` are welcome.


## Credits

- Inspired by https://github.com/Perfare/Zygisk-Il2CppDumper
Expand Down
2 changes: 1 addition & 1 deletion cpplint.cfg
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
linelength=100
linelength=120

filter=-build/c++11
filter=-build/header_guard
Expand Down
4 changes: 2 additions & 2 deletions module.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ ext {
moduleName = "ZygiskFrida"
moduleAuthor = "lico-n"
moduleDescription = "Injects frida gadget via zygisk."
moduleVersion = "v1.2.0"
moduleVersionCode = 3
moduleVersion = "v1.3.0"
moduleVersionCode = 4

// Riru
moduleMinRiruApiVersion = 24
Expand Down
42 changes: 35 additions & 7 deletions module/src/jni/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
#include <string>
#include <fstream>
#include <sstream>
#include <vector>

static std::vector<std::string> split(std::string const& str, char delimiter) {
static std::vector<std::string> split(std::string const &str, char delimiter) {
std::vector<std::string> result;
std::stringstream ss(str);

Expand All @@ -17,23 +16,51 @@ static std::vector<std::string> split(std::string const& str, char delimiter) {
return result;
}

static std::unique_ptr<target_config> parse_target_config(std::string const& line) {
std::unique_ptr<target_config> tcfg (new target_config);
// parse_injected_libraries parses a config line from the `target_packages` file.
// Each line consist of the app package name for the app that will be targeted by this module.
// Optionally there might be a start up delay in milliseconds specified separated by a comma
// from the package name.
static std::unique_ptr<target_config> parse_target_config(std::string const &line) {
std::unique_ptr<target_config> tcfg(new target_config);

auto parts = split(line, ',');

tcfg->app_name = parts[0];

if (parts.size() >= 2) {
tcfg->start_up_delay = std::stoul(parts[1]);
tcfg->start_up_delay_ms = std::stoul(parts[1]);
} else {
tcfg->start_up_delay = 0;
tcfg->start_up_delay_ms = 0;
}

return tcfg;
}

std::unique_ptr<target_config> load_config(std::string const& module_dir, std::string const& app_name) {
// parse_injected_libraries returns a vector with all libraries that will be injected
// into the process. By default this is the bundled frida-gadget.
// If the file `injected_libraries` exists in the re.zyg.fri directory, then
// it will load the libraries specified in that file. One lib file path per line.
static std::vector<std::string> parse_injected_libraries(std::string const &module_dir) {
auto config_file_path = module_dir + "/injected_libraries";

std::ifstream config_file(config_file_path);
if (!config_file.is_open()) {
return {module_dir + "/libgadget.so"};
}

std::vector<std::string> injected_libraries;

std::string libpath;
while (getline(config_file, libpath)) {
if (!libpath.empty()) {
injected_libraries.push_back(libpath);
}
}

return injected_libraries;
}

std::unique_ptr<target_config> load_config(std::string const &module_dir, std::string const &app_name) {
auto config_file_path = module_dir + "/target_packages";

std::ifstream config_file(config_file_path);
Expand All @@ -45,6 +72,7 @@ std::unique_ptr<target_config> load_config(std::string const& module_dir, std::s
while (getline(config_file, line)) {
auto cfg = parse_target_config(line);
if (cfg->app_name == app_name) {
cfg->injected_libraries = parse_injected_libraries(module_dir);
return cfg;
}
}
Expand Down
5 changes: 4 additions & 1 deletion module/src/jni/config.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
#ifndef ZYGISKFRIDA_CONFIG_H
#define ZYGISKFRIDA_CONFIG_H

#include <memory>
#include <string>
#include <vector>

struct target_config{
std::string app_name;
uint64_t start_up_delay;
uint64_t start_up_delay_ms;
std::vector<std::string> injected_libraries;
};

std::unique_ptr<target_config> load_config(std::string const& module_dir, std::string const& app_name);
Expand Down
43 changes: 26 additions & 17 deletions module/src/jni/inject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
#include <cinttypes>
#include <filesystem>
#include <fstream>
#include <memory>
#include <sstream>
#include <string>
#include <thread>
#include <utility>

#include "config.h"
#include "log.h"
Expand All @@ -22,7 +24,7 @@ static std::string get_process_name() {
return buffer.str();
}

static void wait_for_init(std::string const& app_name) {
static void wait_for_init(std::string const &app_name) {
LOGI("Wait for process to complete init");

// wait until the process is renamed to the package name
Expand All @@ -36,15 +38,15 @@ static void wait_for_init(std::string const& app_name) {
LOGI("Process init completed");
}

static void delay_start_up(uint64_t start_up_delay) {
if (start_up_delay <= 0) {
static void delay_start_up(uint64_t start_up_delay_ms) {
if (start_up_delay_ms <= 0) {
return;
}

LOGI("Waiting for configured start up delay %" PRIu64"ms", start_up_delay);
LOGI("Waiting for configured start up delay %" PRIu64"ms", start_up_delay_ms);

int countdown = 0;
uint64_t delay = start_up_delay;
uint64_t delay = start_up_delay_ms;

for (int i = 0; i < 10 && delay > 1000; i++) {
delay -= 1000;
Expand All @@ -54,32 +56,33 @@ static void delay_start_up(uint64_t start_up_delay) {
std::this_thread::sleep_for(std::chrono::milliseconds(delay));

for (int i = countdown; i > 0; i--) {
LOGI("Starting gadget in %d seconds", i);
LOGI("Injecting libs in %d seconds", i);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}

void inject_gadget(std::string const gadget_path, std::unique_ptr<target_config> cfg) {
void inject_libs(std::unique_ptr<target_config> cfg) {
// 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(cfg->app_name);

delay_start_up(cfg->start_up_delay);
delay_start_up(cfg->start_up_delay_ms);

LOGI("Starting gadget %s", gadget_path.c_str());
auto *handle = xdl_open(gadget_path.c_str(), XDL_ALWAYS_FORCE_LOAD);
if (handle) {
LOGI("Gadget connected");
} else {
LOGE("Failed to start gadget: %s", dlerror());
for (auto & lib_path : cfg->injected_libraries) {
LOGI("Injecting %s", lib_path.c_str());
auto *handle = xdl_open(lib_path.c_str(), XDL_TRY_FORCE_LOAD);
if (handle) {
LOGI("Injected %s", lib_path.c_str());
} else {
LOGE("Failed to inject %s : %s", lib_path.c_str(), dlerror());
}
}
}

bool check_and_inject(std::string const& app_name) {
bool check_and_inject(std::string const &app_name) {
std::string module_dir = std::string("/data/local/tmp/re.zyg.fri");
std::string gadget_path = module_dir + "/libgadget.so";

std::unique_ptr<target_config> cfg = load_config(module_dir, app_name);
if (cfg == nullptr) {
Expand All @@ -88,7 +91,13 @@ bool check_and_inject(std::string const& app_name) {

LOGI("App detected: %s", app_name.c_str());

std::thread inject_thread(inject_gadget, gadget_path, std::move(cfg));
if (cfg->injected_libraries.empty()) {
LOGI("No libraries configured for injection. "
"Check the content of the `injected_libraries` file or delete it.");
return false;
}

std::thread inject_thread(inject_libs, std::move(cfg));
inject_thread.detach();

return true;
Expand Down

0 comments on commit ee136d4

Please sign in to comment.