diff --git a/.gitmodules b/.gitmodules index 340c732..e46b92c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "loader/src/external/lsplt"] path = loader/src/external/lsplt - url = https://github.com/LSPosed/lsplt + url = https://github.com/JingMatrix/LSPlt diff --git a/loader/src/external/lsplt b/loader/src/external/lsplt index 5d2b820..9848042 160000 --- a/loader/src/external/lsplt +++ b/loader/src/external/lsplt @@ -1 +1 @@ -Subproject commit 5d2b820cf968fcd8162697d208ad406805b6db25 +Subproject commit 984804293e6f7f87604479fefd57976a2c222a89 diff --git a/loader/src/include/solist.hpp b/loader/src/include/solist.hpp index c65798b..f55d4b1 100644 --- a/loader/src/include/solist.hpp +++ b/loader/src/include/solist.hpp @@ -11,20 +11,27 @@ namespace SoList { class SoInfo { public: #ifdef __LP64__ - inline static size_t solist_next_offset = 0x30; + inline static size_t solist_size_offset = 0x18; + inline static size_t solist_next_offset = 0x28; constexpr static size_t solist_realpath_offset = 0x1a8; #else + inline static size_t solist_size_offset = 0x90; inline static size_t solist_next_offset = 0xa4; constexpr static size_t solist_realpath_offset = 0x174; #endif inline static const char *(*get_realpath_sym)(SoInfo *) = NULL; inline static const char *(*get_soname_sym)(SoInfo *) = NULL; + inline static void (*soinfo_free)(SoInfo *) = NULL; inline SoInfo *get_next() { return *(SoInfo **) ((uintptr_t) this + solist_next_offset); } + inline size_t get_size() { + return *(size_t *) ((uintptr_t) this + solist_size_offset); + } + inline const char *get_path() { if (get_realpath_sym) return get_realpath_sym(this); @@ -40,6 +47,10 @@ namespace SoList { void set_next(SoInfo *si) { *(SoInfo **) ((uintptr_t) this + solist_next_offset) = si; } + + void set_size(size_t size) { + *(size_t *) ((uintptr_t) this + solist_size_offset) = size; + } }; class ProtectedDataGuard { @@ -69,8 +80,8 @@ namespace SoList { private: using FuncType = void (ProtectedDataGuard::*)(); - static FuncType ctor; - static FuncType dtor; + inline static FuncType ctor = NULL; + inline static FuncType dtor = NULL; union MemFunc { FuncType f; @@ -86,8 +97,9 @@ namespace SoList { static SoInfo *solist = NULL; static SoInfo *somain = NULL; static SoInfo **sonext = NULL; - ProtectedDataGuard::FuncType ProtectedDataGuard::ctor = NULL; - ProtectedDataGuard::FuncType ProtectedDataGuard::dtor = NULL; + + static uint64_t *g_module_load_counter = NULL; + static uint64_t *g_module_unload_counter = NULL; static bool Initialize(); @@ -98,28 +110,51 @@ namespace SoList { return addr == NULL ? NULL : *addr; } - static void DropSoPath(const char* target_path) { + static bool DropSoPath(const char* target_path) { + bool path_found = false; + if (solist == NULL && !Initialize()) { + LOGE("Failed to initialize solist"); + return path_found; + } + for (auto iter = solist; iter; iter = iter->get_next()) { + if (iter->get_name() && iter->get_path() && strstr(iter->get_path(), target_path)) { + SoList::ProtectedDataGuard guard; + LOGI("dropping solist record for %s loaded at %s with size %zu", iter->get_name(), iter->get_path(), iter->get_size()); + if (iter->get_size() > 0) { + iter->set_size(0); + SoInfo::soinfo_free(iter); + path_found = true; + } + } + } + return path_found; + } + + static void ResetCounters(size_t load, size_t unload) { if (solist == NULL && !Initialize()) { LOGE("Failed to initialize solist"); return; } - SoInfo *prev = NULL; - for (auto iter = solist; iter; iter = iter->get_next()) { - if (prev != NULL && iter->get_name() && iter->get_path() && strstr(iter->get_path(), target_path)) { - SoList::ProtectedDataGuard guard; - prev->set_next(iter->get_next()); - if (iter == *sonext) { - *sonext = prev; - } - LOGI("Dropped solist record for %s loaded at %s", iter->get_name(), iter->get_path()); - } - prev = iter; + if (g_module_load_counter == NULL || g_module_unload_counter == NULL) { + LOGI("g_module counters not defined, skip reseting them"); + return; + } + auto loaded_modules = *g_module_load_counter; + auto unloaded_modules = *g_module_unload_counter; + if (loaded_modules >= load) { + *g_module_load_counter = loaded_modules - load; + LOGD("reset g_module_load_counter to %zu", (size_t) *g_module_load_counter); + } + if (unloaded_modules >= unload) { + *g_module_unload_counter = unloaded_modules - unload; + LOGD("reset g_module_unload_counter to %zu", (size_t) *g_module_unload_counter); } } static bool Initialize() { SandHook::ElfImg linker("/linker"); if (!ProtectedDataGuard::setup(linker)) return false; + LOGD("found symbol ProtectedDataGuard"); /* INFO: Since Android 15, the symbol names for the linker have a suffix, this makes it impossible to hardcode the symbol names. To allow @@ -131,6 +166,11 @@ namespace SoList { std::string_view solist_sym_name = linker.findSymbolNameByPrefix("__dl__ZL6solist"); if (solist_sym_name.empty()) return false; + LOGD("found symbol name %s", solist_sym_name.data()); + + std::string_view soinfo_free_name = linker.findSymbolNameByPrefix("__dl__ZL11soinfo_freeP6soinfo"); + if (soinfo_free_name.empty()) return false; + LOGD("found symbol name %s", soinfo_free_name.data()); /* INFO: The size isn't a magic number, it's the size for the string: .llvm.7690929523238822858 */ char llvm_sufix[25 + 1]; @@ -143,6 +183,7 @@ namespace SoList { solist = getStaticPointer(linker, solist_sym_name.data()); if (solist == NULL) return false; + LOGD("found symbol solist"); char somain_sym_name[sizeof("__dl__ZL6somain") + sizeof(llvm_sufix)]; snprintf(somain_sym_name, sizeof(somain_sym_name), "__dl__ZL6somain%s", llvm_sufix); @@ -150,30 +191,52 @@ namespace SoList { char sonext_sym_name[sizeof("__dl__ZL6sonext") + sizeof(llvm_sufix)]; snprintf(sonext_sym_name, sizeof(somain_sym_name), "__dl__ZL6sonext%s", llvm_sufix); - char vsdo_sym_name[sizeof("__dl__ZL4vdso") + sizeof(llvm_sufix)]; - snprintf(vsdo_sym_name, sizeof(vsdo_sym_name), "__dl__ZL4vdso%s", llvm_sufix); + char vdso_sym_name[sizeof("__dl__ZL4vdso") + sizeof(llvm_sufix)]; + snprintf(vdso_sym_name, sizeof(vdso_sym_name), "__dl__ZL4vdso%s", llvm_sufix); somain = getStaticPointer(linker, somain_sym_name); if (somain == NULL) return false; + LOGD("found symbol somain"); sonext = linker.getSymbAddress(sonext_sym_name); if (sonext == NULL) return false; + LOGD("found symbol sonext"); - SoInfo *vsdo = getStaticPointer(linker, vsdo_sym_name); - if (vsdo == NULL) return false; + SoInfo *vdso = getStaticPointer(linker, vdso_sym_name); + if (vdso != NULL) LOGD("found symbol vdso"); SoInfo::get_realpath_sym = reinterpret_cast(linker.getSymbAddress("__dl__ZNK6soinfo12get_realpathEv")); + if (SoInfo::get_realpath_sym == NULL) return false; + LOGD("found symbol get_realpath_sym"); + SoInfo::get_soname_sym = reinterpret_cast(linker.getSymbAddress("__dl__ZNK6soinfo10get_sonameEv")); + if (SoInfo::get_soname_sym == NULL) return false; + LOGD("found symbol get_soname_sym"); + + SoInfo::soinfo_free = reinterpret_cast(linker.getSymbAddress(soinfo_free_name)); + if (SoInfo::soinfo_free == NULL) return false; + LOGD("found symbol soinfo_free"); + + g_module_load_counter = reinterpret_cast(linker.getSymbAddress("__dl__ZL21g_module_load_counter")); + if (g_module_load_counter != NULL) LOGD("found symbol g_module_load_counter"); + + g_module_unload_counter = reinterpret_cast(linker.getSymbAddress("__dl__ZL23g_module_unload_counter")); + if (g_module_unload_counter != NULL) LOGD("found symbol g_module_unload_counter"); for (size_t i = 0; i < 1024 / sizeof(void *); i++) { - auto *possible_next = *(void **) ((uintptr_t) solist + i * sizeof(void *)); - if (possible_next == somain || (vsdo != NULL && possible_next == vsdo)) { + auto possible_field = (uintptr_t) solist + i * sizeof(void *); + auto possible_size_of_somain = *(size_t *)((uintptr_t) somain + i * sizeof(void *)); + if (possible_size_of_somain < 0x100000 && possible_size_of_somain > 0x100) { + SoInfo::solist_size_offset = i * sizeof(void *); + LOGD("solist_size_offset is %zu * %zu = %p", i, sizeof(void *), (void*) SoInfo::solist_size_offset); + } + if (*(void **)possible_field == somain || (vdso != NULL && *(void **)possible_field == vdso)) { SoInfo::solist_next_offset = i * sizeof(void *); - + LOGD("solist_next_offset is %zu * %zu = %p", i, sizeof(void *), (void*) SoInfo::solist_next_offset); break; } } - return (SoInfo::get_realpath_sym != NULL && SoInfo::get_soname_sym != NULL); + return true; } } diff --git a/loader/src/injector/entry.cpp b/loader/src/injector/entry.cpp index 1cabc27..a927042 100644 --- a/loader/src/injector/entry.cpp +++ b/loader/src/injector/entry.cpp @@ -1,16 +1,17 @@ #include "daemon.h" #include "logging.h" #include "zygisk.hpp" -#include "module.hpp" using namespace std; -void *self_handle = nullptr; +void *start_addr = nullptr; +size_t block_size = 0; extern "C" [[gnu::visibility("default")]] -void entry(void* handle, const char* path) { +void entry(void* addr, size_t size, const char* path) { LOGI("Zygisk library injected, version %s", ZKSU_VERSION); - self_handle = handle; + start_addr = addr; + block_size = size; zygiskd::Init(path); if (!zygiskd::PingHeartbeat()) { @@ -22,6 +23,7 @@ void entry(void* handle, const char* path) { logging::setfd(zygiskd::RequestLogcatFd()); #endif - LOGI("Start hooking"); + LOGI("start plt hooking"); hook_functions(); + clean_trace(path, 1, 0, false); } diff --git a/loader/src/injector/hook.cpp b/loader/src/injector/hook.cpp index a1b715c..ae41b6a 100644 --- a/loader/src/injector/hook.cpp +++ b/loader/src/injector/hook.cpp @@ -122,6 +122,7 @@ struct ZygiskContext { vector> *plt_hook_list; map, StringCmp> *jni_hook_list; bool should_unmap_zygisk = false; +std::vector cached_map_infos = {}; } // namespace @@ -176,22 +177,24 @@ DCL_HOOK_FUNC(void, android_log_close) { // We cannot directly call `dlclose` to unload ourselves, otherwise when `dlclose` returns, // it will return to our code which has been unmapped, causing segmentation fault. -// Instead, we hook `pthread_attr_destroy` which will be called when VM daemon threads start. -DCL_HOOK_FUNC(int, pthread_attr_destroy, void *target) { - int res = old_pthread_attr_destroy((pthread_attr_t *)target); +// Instead, we hook `pthread_attr_setstacksize` which will be called when VM daemon threads start. +DCL_HOOK_FUNC(int, pthread_attr_setstacksize, void *target, size_t size) { + int res = old_pthread_attr_setstacksize((pthread_attr_t *)target, size); + LOGV("Call pthread_attr_setstacksize in [tid, pid]: %d, %d", gettid(), getpid()); // Only perform unloading on the main thread if (gettid() != getpid()) return res; - LOGV("pthread_attr_destroy"); if (should_unmap_zygisk) { unhook_functions(); + cached_map_infos.clear(); if (should_unmap_zygisk) { - // Because both `pthread_attr_destroy` and `dlclose` have the same function signature, + // Because both `pthread_attr_setstacksize` and `dlclose` have the same function signature, // we can use `musttail` to let the compiler reuse our stack frame and thus - // `dlclose` will directly return to the caller of `pthread_attr_destroy`. - [[clang::musttail]] return dlclose(self_handle); + // `dlclose` will directly return to the caller of `pthread_attr_setstacksize`. + LOGI("unmap libzygisk.so loaded at %p with size %zu", start_addr, block_size); + [[clang::musttail]] return munmap(start_addr, block_size); } } @@ -204,6 +207,8 @@ DCL_HOOK_FUNC(char *, strdup, const char *s) { if (strcmp(s, "com.android.internal.os.ZygoteInit") == 0) { LOGV("strdup %s", s); initialize_jni_hook(); + cached_map_infos = lsplt::MapInfo::Scan(); + LOGD("cached_map_infos updated"); } return old_strdup(s); @@ -267,7 +272,7 @@ void initialize_jni_hook() { auto get_created_java_vms = reinterpret_cast( dlsym(RTLD_DEFAULT, "JNI_GetCreatedJavaVMs")); if (!get_created_java_vms) { - for (auto &map: lsplt::MapInfo::Scan()) { + for (auto &map: cached_map_infos) { if (!map.path.ends_with("/libnativehelper.so")) continue; void *h = dlopen(map.path.data(), RTLD_LAZY); if (!h) { @@ -348,7 +353,7 @@ bool ZygiskModule::RegisterModuleImpl(ApiTable *api, long *module) { api->v2.getFlags = [](auto) { return ZygiskModule::getFlags(); }; } if (api_version >= 4) { - api->v4.pltHookCommit = lsplt::CommitHook; + api->v4.pltHookCommit = []() { return lsplt::CommitHook(cached_map_infos); }; api->v4.pltHookRegister = [](dev_t dev, ino_t inode, const char *symbol, void *fn, void **backup) { if (dev == 0 || inode == 0 || symbol == nullptr || fn == nullptr) return; @@ -382,7 +387,7 @@ void ZygiskContext::plt_hook_exclude(const char *regex, const char *symbol) { void ZygiskContext::plt_hook_process_regex() { if (register_info.empty()) return; - for (auto &map : lsplt::MapInfo::Scan()) { + for (auto &map : cached_map_infos) { if (map.offset != 0 || !map.is_private || !(map.perms & PROT_READ)) continue; for (auto ®: register_info) { if (regexec(®.regex, map.path.data(), 0, nullptr, 0) != 0) @@ -410,7 +415,7 @@ bool ZygiskContext::plt_hook_commit() { register_info.clear(); ignore_info.clear(); } - return lsplt::CommitHook(); + return lsplt::CommitHook(cached_map_infos); } @@ -573,88 +578,20 @@ void ZygiskContext::run_modules_pre() { void ZygiskContext::run_modules_post() { flags[POST_SPECIALIZE] = true; + + size_t modules_unloaded = 0; for (const auto &m : modules) { if (flags[APP_SPECIALIZE]) { m.postAppSpecialize(args.app); } else if (flags[SERVER_FORK_AND_SPECIALIZE]) { m.postServerSpecialize(args.server); } - m.tryUnload(); + if (m.tryUnload()) modules_unloaded++; } - SoList::DropSoPath("jit-cache"); - - // Remap as well to avoid checking of /memfd:jit-cache - for (auto &info : lsplt::MapInfo::Scan()) { - if (strstr(info.path.c_str(), "jit-cache-zygisk")) - { - void *addr = (void *)info.start; - size_t size = info.end - info.start; - // MAP_SHARED should fix the suspicious mapping. - void *copy = mmap(nullptr, size, PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0); - if (copy == MAP_FAILED) { - LOGE("Failed to mmap jit-cache-zygisk"); - continue; - } - - if ((info.perms & PROT_READ) == 0) { - mprotect(addr, size, PROT_READ); - } - memcpy(copy, addr, size); - mremap(copy, size, size, MREMAP_MAYMOVE | MREMAP_FIXED, addr); - mprotect(addr, size, info.perms); - } - } - - // Don't know if there's a header for things like this - // so I just put it into a lambda - auto generateRandomString = [](char *str, int length) { - const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - srand(time(NULL)); - - for (int i = 0; i < length; i++) { - int key = rand() % (sizeof(charset) - 1); - str[i] = charset[key]; - } - - str[length] = '\0'; - }; - - // Randomize name of anonymous mappings - // We don't run this in the previous loop because LSPosed might also add - // mappings that are not related to /memfd:jit-zygisk-cache - // - // Since we changed to MAP_SHARED, I don't think this is still needed but let's - // leave it here just in case. - for (auto info : lsplt::MapInfo::Scan()) { - // I had some problems with info.perms & PROT_EXEC so I had to change lsplt source a bit. - // If that problem occurs here, do strchr(info.perms_str.c_str(), 'x') instead and add perms_str - // to the lsplt MapInfo struct and set it to the raw perms string in Scan(); - if (info.perms & PROT_EXEC && info.path.empty()) { - // Generate Random Name - char randomString[11]; - generateRandomString(randomString, 10); - LOGI("Randomized Memory map name: %s", randomString); - - // Memory address of random string - uintptr_t strAddr = (uintptr_t)&randomString; - - // https://lore.kernel.org/lkml/1383170047-21074-2-git-send-email-ccross@android.com/ - prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, info.start, info.end - info.start, strAddr); - } - - // Remap as MAP_SHARED - if (info.perms & PROT_EXEC && info.dev == 0 && info.path.find("anon") != std::string::npos) { - void *addr = reinterpret_cast(info.start); - size_t size = info.end - info.start; - void *copy = mmap(nullptr, size, PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0); - if ((info.perms & PROT_READ) == 0) { - mprotect(addr, size, PROT_READ); - } - memcpy(copy, addr, size); - mremap(copy, size, size, MREMAP_MAYMOVE | MREMAP_FIXED, addr); - mprotect(addr, size, info.perms); - } + if (modules.size() > 0) { + LOGD("modules unloaded: %zu/%zu", modules_unloaded, modules.size()); + clean_trace("jit-cache-zygisk", modules.size(), modules_unloaded, true); } } @@ -791,8 +728,8 @@ ZygiskContext::~ZygiskContext() { } // namespace -static bool hook_commit() { - if (lsplt::CommitHook()) { +static bool hook_commit(std::vector &map_infos = cached_map_infos) { + if (lsplt::CommitHook(map_infos)) { return true; } else { LOGE("plt_hook failed"); @@ -814,6 +751,37 @@ static void hook_register(dev_t dev, ino_t inode, const char *symbol, void *new_ #define PLT_HOOK_REGISTER(DEV, INODE, NAME) \ PLT_HOOK_REGISTER_SYM(DEV, INODE, #NAME, NAME) +void clean_trace(const char* path, size_t load, size_t unload, bool spoof_maps) { + LOGD("cleaning trace for path %s", path); + + if (load > 0 || unload >0) SoList::ResetCounters(load, unload); + bool path_found = SoList::DropSoPath(path); + if (!path_found || !spoof_maps) return; + + LOGD("spoofing virtual maps for %s", path); + // spoofing map names is futile in Android, we do it simply + // to avoid Zygisk detections based on string comparison + for (auto &map : lsplt::MapInfo::Scan()) { + if (strstr(map.path.c_str(), path)) + { + void *addr = (void *)map.start; + size_t size = map.end - map.start; + void *copy = mmap(nullptr, size, PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0); + if (copy == MAP_FAILED) { + LOGE("failed to backup block %s [%p, %p]", map.path.c_str(), addr, (void*)map.end); + continue; + } + + if ((map.perms & PROT_READ) == 0) { + mprotect(addr, size, PROT_READ); + } + memcpy(copy, addr, size); + mremap(copy, size, size, MREMAP_MAYMOVE | MREMAP_FIXED, addr); + mprotect(addr, size, map.perms); + } + } +} + void hook_functions() { default_new(plt_hook_list); default_new(jni_hook_list); @@ -824,7 +792,8 @@ void hook_functions() { // ino_t native_bridge_inode = 0; // dev_t native_bridge_dev = 0; - for (auto &map : lsplt::MapInfo::Scan()) { + cached_map_infos = lsplt::MapInfo::Scan(); + for (auto &map : cached_map_infos) { if (map.path.ends_with("libandroid_runtime.so")) { android_runtime_inode = map.inode; android_runtime_dev = map.dev; @@ -850,7 +819,7 @@ static void hook_unloader() { ino_t art_inode = 0; dev_t art_dev = 0; - for (auto &map : lsplt::MapInfo::Scan()) { + for (auto &map : cached_map_infos) { if (map.path.ends_with("/libart.so")) { art_inode = map.inode; art_dev = map.dev; @@ -858,7 +827,13 @@ static void hook_unloader() { } } - PLT_HOOK_REGISTER(art_dev, art_inode, pthread_attr_destroy); + if (art_dev == 0 || art_inode == 0) { + LOGE("virtual map for libart.so is not cached"); + return; + } else { + LOGD("hook_unloader called with libart.so [%zu:%lu]", art_dev, art_inode); + } + PLT_HOOK_REGISTER(art_dev, art_inode, pthread_attr_setstacksize); hook_commit(); } diff --git a/loader/src/injector/module.hpp b/loader/src/injector/module.hpp index dfbde57..24111fc 100644 --- a/loader/src/injector/module.hpp +++ b/loader/src/injector/module.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include "api.hpp" @@ -209,7 +210,7 @@ case 5: \ int getModuleDir() const; void setOption(zygisk::Option opt); static uint32_t getFlags(); - void tryUnload() const { if (unload) dlclose(handle); } + bool tryUnload() const { return unload && dlclose(handle) == 0; }; void clearApi() { memset(&api, 0, sizeof(api)); } int getId() const { return id; } @@ -235,4 +236,4 @@ case 5: \ } mod; }; -} // namespace \ No newline at end of file +} // namespace diff --git a/loader/src/injector/zygisk.hpp b/loader/src/injector/zygisk.hpp index 5c06f8b..793ba78 100644 --- a/loader/src/injector/zygisk.hpp +++ b/loader/src/injector/zygisk.hpp @@ -1,13 +1,14 @@ #pragma once -#include #include -#include -extern void *self_handle; +extern void *start_addr; +extern size_t block_size; void hook_functions(); +void clean_trace(const char* path, size_t load = 1, size_t unload = 0, bool spoof_maps = false); + void revert_unmount_ksu(); void revert_unmount_magisk(); diff --git a/loader/src/ptracer/ptracer.cpp b/loader/src/ptracer/ptracer.cpp index 9182ddd..f790f98 100644 --- a/loader/src/ptracer/ptracer.cpp +++ b/loader/src/ptracer/ptracer.cpp @@ -222,10 +222,24 @@ bool inject_on_main(int pid, const char *lib_path) { return false; } - /* call injector entry(handle, path) */ + /* record the address range of libzygisk.so */ + map = MapInfo::Scan(std::to_string(pid)); + void *start_addr = nullptr; + size_t block_size = 0; + for (auto &info : map) { + if (strstr(info.path.c_str(), "libzygisk.so")) { + void *addr = (void *)info.start; + if (start_addr == nullptr) start_addr = addr; + size_t size = info.end - info.start; + block_size += size; + LOGD("found block %s: [%p-%p] with size %zu", info.path.c_str(), addr, (void *)info.end, size); + } + } + /* call injector entry(start_addr, block_size, path) */ args.clear(); - args.push_back(remote_handle); + args.push_back((uintptr_t) start_addr); + args.push_back(block_size); str = push_string(pid, regs, zygiskd::GetTmpPath().c_str()); args.push_back((long) str);