diff --git a/daemon/src/main/jni/logcat.cpp b/daemon/src/main/jni/logcat.cpp index 33d796c8b..2e5897a39 100644 --- a/daemon/src/main/jni/logcat.cpp +++ b/daemon/src/main/jni/logcat.cpp @@ -229,7 +229,8 @@ void Logcat::ProcessBuffer(struct log_msg *buf) { tag == "APatchD"sv || tag == "Dobby"sv || tag.starts_with("dex2oat"sv) || tag == "KernelSU"sv || tag == "LSPlant"sv || tag == "LSPlt"sv || tag.starts_with("LSPosed"sv) || tag == "Magisk"sv || tag == "SELinux"sv || - tag == "TEESimulator"sv || tag.starts_with("zygisk"sv))) [[unlikely]] { + tag == "TEESimulator"sv || tag.starts_with("Vector"sv) || + tag.starts_with("zygisk"sv))) [[unlikely]] { verbose_print_count_ += PrintLogLine(entry, verbose_file_.get()); } if (entry.pid == my_pid_ && tag == "LSPosedLogcat"sv) [[unlikely]] { diff --git a/dex2oat/README.md b/dex2oat/README.md new file mode 100644 index 000000000..4d4165c9f --- /dev/null +++ b/dex2oat/README.md @@ -0,0 +1,37 @@ +# VectorDex2Oat + +VectorDex2Oat is a specialized wrapper and instrumentation suite for the Android `dex2oat` (Ahead-of-Time compiler) binary. It is designed to intercept the compilation process, force specific compiler behaviors (specifically disabling method inlining), and transparently spoof the resulting OAT metadata to hide the presence of the wrapper. + +## Overview + +In the Android Runtime (ART), `dex2oat` compiles DEX files into OAT files. Modern ART optimizations often inline methods, making it difficult for instrumentation tools to hook specific function calls. + +This project consists of two primary components: +1. **dex2oat (Wrapper):** A replacement binary that intercepts the execution, communicates via Unix Domain Sockets to obtain the original compiler binary, and executes it with forced flags. +2. **liboat_hook.so (Hooker):** A shared library injected into the `dex2oat` process via `LD_PRELOAD` that utilizes PLT hooking to sanitize the OAT header's command-line metadata. + +## Key Features + +* **Inlining Suppression:** Appends `--inline-max-code-units=0` to the compiler arguments, ensuring all methods remain discrete and hookable. +* **FD-Based Execution:** Executes the original `dex2oat` via the system linker using `/proc/self/fd/` paths, avoiding direct execution of files on the disk. +* **Metadata Spoofing:** Intercepts `art::OatHeader::ComputeChecksum` or `art::OatHeader::GetKeyValueStore` to remove traces of the wrapper and its injected flags from the final `.oat` file. +* **Abstract Socket Communication:** Uses the Linux Abstract Namespace for Unix sockets to coordinate file descriptor passing between the controller and the wrapper. + +## Architecture + +### The Wrapper [dex2oat.cpp](src/main/cpp/dex2oat.cpp) +The wrapper acts as a "man-in-the-middle" for the compiler. When called by the system, it +1. connects to a predefined Unix socket (the stub name `5291374ceda0...` will be replaced during installation of `Vector`); +2. identifies the target architecture (32-bit vs 64-bit) and debug status; +3. receives File Descriptors (FDs) for both the original `dex2oat` binary and the `oat_hook` library; +4. reconstructs the command line, replacing the wrapper path with the original binary path and appending the "no-inline" flags; +5. clears `LD_LIBRARY_PATH` and sets `LD_PRELOAD` to the hooker library's FD; +6. invokes the dynamic linker (`linker64`) to execute the compiler. + +### The Hooker [oat_hook.cpp](src/main/cpp/oat_hook.cpp) +The hooker library is preloaded into the compiler's address space. It uses the [LSPlt](https://github.com/JingMatrix/LSPlt) library to: +1. Scan the memory map to find the `dex2oat` binary. +2. Locate and hook internal ART functions: + * [art::OatHeader::GetKeyValueStore](https://cs.android.com/android/platform/superproject/+/android-latest-release:art/runtime/oat/oat.cc;l=366) + * [art::OatHeader::ComputeChecksum](https://cs.android.com/android/platform/superproject/+/android-latest-release:art/runtime/oat/oat.cc;l=366) +3. When the compiler attempts to write the "dex2oat-cmdline" key into the OAT header, the hooker intercepts the call, parses the key-value store, and removes the wrapper-specific flags and paths. diff --git a/dex2oat/build.gradle.kts b/dex2oat/build.gradle.kts index 2390988a9..fd155f31c 100644 --- a/dex2oat/build.gradle.kts +++ b/dex2oat/build.gradle.kts @@ -1,47 +1,15 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - plugins { alias(libs.plugins.agp.lib) } android { - namespace = "org.lsposed.dex2oat" - - buildFeatures { - androidResources = false - buildConfig = false - prefab = true - prefabPublishing = true - } + namespace = "org.matrix.vector.dex2oat" - defaultConfig { - minSdk = 29 - } + androidResources.enable = false externalNativeBuild { cmake { path("src/main/cpp/CMakeLists.txt") } } - - prefab { - register("dex2oat") - } } diff --git a/dex2oat/src/main/cpp/CMakeLists.txt b/dex2oat/src/main/cpp/CMakeLists.txt index b181e3ffb..c4f5ba333 100644 --- a/dex2oat/src/main/cpp/CMakeLists.txt +++ b/dex2oat/src/main/cpp/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10) project(dex2oat) add_executable(dex2oat dex2oat.cpp) -add_library(oat_hook SHARED oat_hook.cpp oat.cpp) +add_library(oat_hook SHARED oat_hook.cpp) OPTION(LSPLT_BUILD_SHARED OFF) add_subdirectory(${EXTERNAL_ROOT}/lsplt/lsplt/src/main/jni external) diff --git a/dex2oat/src/main/cpp/dex2oat.cpp b/dex2oat/src/main/cpp/dex2oat.cpp index a3e4863d3..bf441c320 100644 --- a/dex2oat/src/main/cpp/dex2oat.cpp +++ b/dex2oat/src/main/cpp/dex2oat.cpp @@ -1,148 +1,195 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - -// -// Created by Nullptr on 2022/4/1. -// - -#include -#include -#include #include #include #include +#include +#include +#include +#include +#include + #include "logging.h" +// Access to the process environment variables +extern "C" char **environ; + #if defined(__LP64__) #define LP_SELECT(lp32, lp64) lp64 #else #define LP_SELECT(lp32, lp64) lp32 #endif -#define ID_VEC(is64, is_debug) (((is64) << 1) | (is_debug)) +namespace { -const char kSockName[] = "5291374ceda0aef7c5d86cd2a4f6a3ac\0"; +constexpr char kSockName[] = "5291374ceda0aef7c5d86cd2a4f6a3ac"; -static ssize_t xrecvmsg(int sockfd, struct msghdr *msg, int flags) { - int rec = recvmsg(sockfd, msg, flags); +/** + * Calculates a vector ID based on architecture and debug status. + */ +inline int get_id_vec(bool is64, bool is_debug) { + return (static_cast(is64) << 1) | static_cast(is_debug); +} + +/** + * Wraps recvmsg with error logging. + */ +ssize_t xrecvmsg(int sockfd, struct msghdr *msg, int flags) { + ssize_t rec = recvmsg(sockfd, msg, flags); if (rec < 0) { PLOGE("recvmsg"); } return rec; } -static void *recv_fds(int sockfd, char *cmsgbuf, size_t bufsz, int cnt) { +/** + * Receives file descriptors passed over a Unix domain socket using SCM_RIGHTS. + * + * @return Pointer to the FD data on success, nullptr on failure. + */ +void *recv_fds(int sockfd, char *cmsgbuf, size_t bufsz, int cnt) { struct iovec iov = { .iov_base = &cnt, .iov_len = sizeof(cnt), }; - struct msghdr msg = { - .msg_iov = &iov, .msg_iovlen = 1, .msg_control = cmsgbuf, .msg_controllen = bufsz}; + struct msghdr msg = {.msg_name = nullptr, + .msg_namelen = 0, + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = cmsgbuf, + .msg_controllen = bufsz, + .msg_flags = 0}; + + if (xrecvmsg(sockfd, &msg, MSG_WAITALL) < 0) return nullptr; - xrecvmsg(sockfd, &msg, MSG_WAITALL); struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); - if (msg.msg_controllen != bufsz || cmsg == NULL || + if (msg.msg_controllen != bufsz || cmsg == nullptr || cmsg->cmsg_len != CMSG_LEN(sizeof(int) * cnt) || cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) { - return NULL; + return nullptr; } return CMSG_DATA(cmsg); } -static int recv_fd(int sockfd) { +/** + * Helper to receive a single FD from the socket. + */ +int recv_fd(int sockfd) { char cmsgbuf[CMSG_SPACE(sizeof(int))]; - void *data = recv_fds(sockfd, cmsgbuf, sizeof(cmsgbuf), 1); - if (data == NULL) return -1; + if (data == nullptr) return -1; int result; - memcpy(&result, data, sizeof(int)); + std::memcpy(&result, data, sizeof(int)); return result; } -static int read_int(int fd) { +/** + * Reads an integer acknowledgment from the socket. + */ +int read_int(int fd) { int val; if (read(fd, &val, sizeof(val)) != sizeof(val)) return -1; return val; } -static void write_int(int fd, int val) { +/** + * Writes an integer command/ID to the socket. + */ +void write_int(int fd, int val) { if (fd < 0) return; - write(fd, &val, sizeof(val)); + (void)write(fd, &val, sizeof(val)); } +} // namespace + int main(int argc, char **argv) { LOGD("dex2oat wrapper ppid=%d", getppid()); + + // Prepare Unix domain socket address (Abstract Namespace) struct sockaddr_un sock = {}; sock.sun_family = AF_UNIX; - strlcpy(sock.sun_path + 1, kSockName, sizeof(sock.sun_path) - 1); + // sock.sun_path[0] is already \0, so we copy name into sun_path + 1 + std::strncpy(sock.sun_path + 1, kSockName, sizeof(sock.sun_path) - 2); + + // Abstract socket length: family + leading \0 + string length + socklen_t len = sizeof(sock.sun_family) + strlen(kSockName) + 1; + // 1. Get original dex2oat binary FD int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); - size_t len = sizeof(sa_family_t) + strlen(sock.sun_path + 1) + 1; - if (connect(sock_fd, (struct sockaddr *)&sock, len)) { + if (connect(sock_fd, reinterpret_cast(&sock), len)) { PLOGE("failed to connect to %s", sock.sun_path + 1); return 1; } - write_int(sock_fd, ID_VEC(LP_SELECT(0, 1), strstr(argv[0], "dex2oatd") != NULL)); + + bool is_debug = (argv[0] != nullptr && std::strstr(argv[0], "dex2oatd") != nullptr); + write_int(sock_fd, get_id_vec(LP_SELECT(false, true), is_debug)); + int stock_fd = recv_fd(sock_fd); - read_int(sock_fd); + read_int(sock_fd); // Sync close(sock_fd); + // 2. Get liboat_hook.so FD sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (connect(sock_fd, (struct sockaddr *)&sock, len)) { + if (connect(sock_fd, reinterpret_cast(&sock), len)) { PLOGE("failed to connect to %s", sock.sun_path + 1); return 1; } + write_int(sock_fd, LP_SELECT(4, 5)); int hooker_fd = recv_fd(sock_fd); - read_int(sock_fd); + read_int(sock_fd); // Sync close(sock_fd); if (hooker_fd == -1) { - PLOGE("failed to read liboat_hook.so"); + LOGE("failed to read liboat_hook.so"); + } + LOGD("sock: %s stock_fd: %d", sock.sun_path + 1, stock_fd); + + // Prepare arguments for execve + // Logic: [linker] [/proc/self/fd/stock_fd] [original_args...] [--inline-max-code-units=0] + std::vector exec_argv; + + const char *linker_path = + LP_SELECT("/apex/com.android.runtime/bin/linker", "/apex/com.android.runtime/bin/linker64"); + + char stock_fd_path[64]; + std::snprintf(stock_fd_path, sizeof(stock_fd_path), "/proc/self/fd/%d", stock_fd); + + exec_argv.push_back(linker_path); + exec_argv.push_back(stock_fd_path); + + // Append original arguments starting from argv[1] + for (int i = 1; i < argc; ++i) { + exec_argv.push_back(argv[i]); } - LOGD("sock: %s %d", sock.sun_path + 1, stock_fd); - - const char *new_argv[argc + 2]; - for (int i = 0; i < argc; i++) new_argv[i] = argv[i]; - new_argv[argc] = "--inline-max-code-units=0"; - new_argv[argc + 1] = NULL; - - if (getenv("LD_LIBRARY_PATH") == NULL) { - char const *libenv = LP_SELECT( - "LD_LIBRARY_PATH=/apex/com.android.art/lib:/apex/com.android.os.statsd/lib", - "LD_LIBRARY_PATH=/apex/com.android.art/lib64:/apex/com.android.os.statsd/lib64"); - putenv((char *)libenv); + + // Append hooking flags to disable inline, which is our purpose of this wrapper, since we cannot + // hook inlined target methods. + exec_argv.push_back("--inline-max-code-units=0"); + exec_argv.push_back(nullptr); + + // Setup Environment variables + // Clear LD_LIBRARY_PATH to let the linker use internal config + unsetenv("LD_LIBRARY_PATH"); + + // Set LD_PRELOAD to point to the hooker library FD + std::string preload_val = "LD_PRELOAD=/proc/self/fd/" + std::to_string(hooker_fd); + setenv("LD_PRELOAD", ("/proc/self/fd/" + std::to_string(hooker_fd)).c_str(), 1); + + // Pass original argv[0] as DEX2OAT_CMD + if (argv[0]) { + setenv("DEX2OAT_CMD", argv[0], 1); + LOGD("DEX2OAT_CMD set to %s", argv[0]); } - // Set LD_PRELOAD to load liboat_hook.so - const int STRING_BUFFER = 50; - char env_str[STRING_BUFFER]; - snprintf(env_str, STRING_BUFFER, "LD_PRELOAD=/proc/%d/fd/%d", getpid(), hooker_fd); - putenv(env_str); - LOGD("Set env %s", env_str); + LOGI("Executing via linker: %s executing %s", linker_path, stock_fd_path); - fexecve(stock_fd, (char **)new_argv, environ); + // Perform the execution + execve(linker_path, const_cast(exec_argv.data()), environ); - PLOGE("fexecve failed"); + // If we reach here, execve failed + PLOGE("execve failed"); return 2; } diff --git a/dex2oat/src/main/cpp/include/logging.h b/dex2oat/src/main/cpp/include/logging.h index 07574fa73..ab66ef34c 100644 --- a/dex2oat/src/main/cpp/include/logging.h +++ b/dex2oat/src/main/cpp/include/logging.h @@ -1,10 +1,10 @@ #pragma once -#include #include +#include #ifndef LOG_TAG -#define LOG_TAG "LSPosedDex2Oat" +#define LOG_TAG "VectorDex2Oat" #endif #ifdef LOG_DISABLED @@ -15,11 +15,7 @@ #define LOGE(...) 0 #else #ifndef NDEBUG -#define LOGD(fmt, ...) \ - __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, \ - "%s:%d#%s" \ - ": " fmt, \ - __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define LOGV(fmt, ...) \ __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, \ "%s:%d#%s" \ diff --git a/dex2oat/src/main/cpp/include/oat.h b/dex2oat/src/main/cpp/include/oat.h index 375e90299..46aead829 100644 --- a/dex2oat/src/main/cpp/include/oat.h +++ b/dex2oat/src/main/cpp/include/oat.h @@ -72,17 +72,9 @@ class EXPORT PACKED(4) OatHeader { static constexpr const char kTrueValue[] = "true"; static constexpr const char kFalseValue[] = "false"; - static constexpr size_t Get_key_value_store_size_Offset() { - return offsetof(OatHeader, key_value_store_size_); - } - static constexpr size_t Get_key_value_store_Offset() { - return offsetof(OatHeader, key_value_store_); - } - - uint32_t GetKeyValueStoreSize() const; - const uint8_t* GetKeyValueStore() const; - - void SetKeyValueStoreSize(uint32_t new_size); + // Added helper to access the key_value_store_ field, which could be fragile across + // different Android versions and compiler optimizations. + const uint8_t* getKeyValueStore() const { return key_value_store_; } void ComputeChecksum(/*inout*/ uint32_t* checksum) const; diff --git a/dex2oat/src/main/cpp/oat.cpp b/dex2oat/src/main/cpp/oat.cpp deleted file mode 100644 index c50bb65fd..000000000 --- a/dex2oat/src/main/cpp/oat.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "oat.h" - -namespace art { - -uint32_t OatHeader::GetKeyValueStoreSize() const { - return *(uint32_t*)((uintptr_t)this + OatHeader::Get_key_value_store_size_Offset()); -} - -const uint8_t* OatHeader::GetKeyValueStore() const { - return (const uint8_t*)((uintptr_t)this + OatHeader::Get_key_value_store_Offset()); -} - -void OatHeader::SetKeyValueStoreSize(uint32_t new_size) { - *reinterpret_cast((uintptr_t)this + Get_key_value_store_size_Offset()) = new_size; -} - -} // namespace art diff --git a/dex2oat/src/main/cpp/oat_hook.cpp b/dex2oat/src/main/cpp/oat_hook.cpp index 9913abe09..cc4cdb1b9 100644 --- a/dex2oat/src/main/cpp/oat_hook.cpp +++ b/dex2oat/src/main/cpp/oat_hook.cpp @@ -1,122 +1,206 @@ #include -#include +#include #include +#include #include +#include #include #include +#include #include "logging.h" #include "oat.h" -const std::string_view param_to_remove = " --inline-max-code-units=0"; - -#define DCL_HOOK_FUNC(ret, func, ...) \ - ret (*old_##func)(__VA_ARGS__); \ - ret new_##func(__VA_ARGS__) - -bool store_resized = false; +/** + * This library is injected into dex2oat to intercept the generation of OAT headers. Our wrapper + * runs dex2oat via the linker with extra flags. Without this hook, the resulting OAT file would + * record the transferred fd path of wrapper and the extra flags in its "dex2oat-cmdline" key, which + * can be used to detect the wrapper. + */ + +namespace { +const std::string_view kParamToRemove = "--inline-max-code-units=0"; +std::string g_binary_path = getenv("DEX2OAT_CMD"); // The original binary path +} // namespace + +/** + * Sanitizes the command line string by: + * 1. Replacing the first token (the linker/binary path) with the original dex2oat path. + * 2. Removing the specific optimization flag we injected. + */ +std::string process_cmd(std::string_view sv, std::string_view new_cmd_path) { + std::vector tokens; + std::string current; + + // Simple split by space + for (char c : sv) { + if (c == ' ') { + if (!current.empty()) { + tokens.push_back(std::move(current)); + current.clear(); + } + } else { + current.push_back(c); + } + } + if (!current.empty()) tokens.push_back(std::move(current)); -bool ModifyStoreInPlace(uint8_t* store, uint32_t store_size) { - if (store == nullptr || store_size == 0) { - return false; + // 1. Replace the command path (argv[0]) + if (!tokens.empty()) { + tokens[0] = std::string(new_cmd_path); } - // Define the search space - uint8_t* const store_begin = store; - uint8_t* const store_end = store + store_size; + // 2. Remove the injected parameter if it exists + auto it = std::remove(tokens.begin(), tokens.end(), std::string(kParamToRemove)); + tokens.erase(it, tokens.end()); - // 1. Search for the parameter in the memory buffer - auto it = std::search(store_begin, store_end, param_to_remove.begin(), param_to_remove.end()); + // 3. Join tokens back into a single string + std::string result; + for (size_t i = 0; i < tokens.size(); ++i) { + result += tokens[i]; + if (i != tokens.size() - 1) result += ' '; + } + return result; +} - // Check if the parameter was found - if (it == store_end) { - LOGD("Parameter '%.*s' not found.", (int)param_to_remove.size(), param_to_remove.data()); - return false; +/** + * Re-serializes the Key-Value map back into the OAT header memory space. + */ +uint8_t* WriteKeyValueStore(const std::map& key_values, uint8_t* store) { + LOGD("Writing KeyValueStore back to memory"); + char* data_ptr = reinterpret_cast(store); + + for (const auto& [key, value] : key_values) { + // Copy key + null terminator + std::memcpy(data_ptr, key.c_str(), key.length() + 1); + data_ptr += key.length() + 1; + // Copy value + null terminator + std::memcpy(data_ptr, value.c_str(), value.length() + 1); + data_ptr += value.length() + 1; } + LOGD("Written KeyValueStore with size: %zu", reinterpret_cast(data_ptr) - store); - uint8_t* location_of_param = it; - LOGD("Parameter found at offset %td.", location_of_param - store_begin); + return reinterpret_cast(data_ptr); +} - // 2. Check if there is padding immediately after the string - uint8_t* const byte_after_param = location_of_param + param_to_remove.size(); - bool has_padding = false; +// Helper function to test if a header field could have variable length +bool IsNonDeterministic(const std::string_view& key) { + auto variable_fields = art::OatHeader::kNonDeterministicFieldsAndLengths; + return std::any_of(variable_fields.begin(), variable_fields.end(), + [&key](const auto& pair) { return pair.first.compare(key) == 0; }); +} - // Boundary check: ensure the byte after the parameter is within the buffer - if (byte_after_param + 1 < store_end) { - if (*(byte_after_param + 1) == '\0') { - has_padding = true; +/** + * Parses the OAT KeyValueStore and spoofs the "dex2oat-cmdline" entry. + * + * @return true if the store was modified in-place or successfully rebuilt. + */ +bool SpoofKeyValueStore(uint8_t* store) { + if (!store) return false; + + uint32_t* const store_size_ptr = reinterpret_cast(store - sizeof(uint32_t)); + uint32_t const store_size = *store_size_ptr; + + const char* ptr = reinterpret_cast(store); + const char* const store_end = ptr + store_size; + std::map new_store_map; + LOGI("Parsing KeyValueStore [%p - %p] of size %u", ptr, store_end, store_size); + + bool store_modified = false; + + while (ptr < store_end && *ptr != '\0') { + // Find key + const char* key_end = reinterpret_cast(std::memchr(ptr, 0, store_end - ptr)); + if (!key_end) break; + std::string_view key(ptr, key_end - ptr); + + // Find value + const char* value_start = key_end + 1; + if (value_start >= store_end) break; + const char* value_end = + reinterpret_cast(std::memchr(value_start, 0, store_end - value_start)); + if (!value_end) break; + std::string_view value(value_start, value_end - value_start); + + const bool has_padding = + value_end + 1 < store_end && *(value_end + 1) == '\0' && IsNonDeterministic(key); + + if (key == art::OatHeader::kDex2OatCmdLineKey && + value.find(kParamToRemove) != std::string_view::npos) { + std::string cleaned_cmd = process_cmd(value, g_binary_path); + LOGI("Spoofing cmdline: Original size %zu -> New size %zu", value.length(), + cleaned_cmd.length()); + + // We can overwrite in-place if the padding is enabled + if (has_padding) { + LOGI("In-place spoofing dex2oat-cmdline (padding detected)"); + + // Zero out the entire original value range to be safe + size_t original_capacity = value.length(); + std::memset(const_cast(value_start), 0, original_capacity); + + // Write the new command. + std::memcpy(const_cast(value_start), cleaned_cmd.c_str(), + std::min(cleaned_cmd.length(), original_capacity)); + return true; + } + + // Standard logic: store in map and rebuild later + new_store_map[std::string(key)] = std::move(cleaned_cmd); + store_modified = true; + } else { + new_store_map[std::string(key)] = std::string(value); + LOGI("Parsed item:\t[%s:%s]", key.data(), value.data()); } - } - // 3. Perform the conditional action - if (has_padding) { - // CASE A: Padding exists. Overwrite the parameter with zeros. - LOGD("Padding found. Overwriting parameter with zeros."); - memset(location_of_param, 0, param_to_remove.size()); - return false; // Size did not change - } else { - // CASE B: No padding exists (or parameter is at the very end). - // Remove the parameter by shifting the rest of the memory forward. - LOGD("No padding found. Removing parameter and shifting memory."); - - // Calculate what to move - uint8_t* source = byte_after_param; - uint8_t* destination = location_of_param; - size_t bytes_to_move = store_end - source; - - // memmove is required because the source and destination buffers overlap - if (bytes_to_move > 0) { - memmove(destination, source, bytes_to_move); + ptr = value_end + 1; + if (has_padding) { + while (*ptr == '\0') { + ptr++; + } } - - // 4. Update the total size of the store - store_size -= param_to_remove.size(); - LOGD("Store size changed. New size: %u", store_size); - - return true; // Size changed } -} -DCL_HOOK_FUNC(uint32_t, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv, void* header) { - uint32_t size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); - if (store_resized) { - LOGD("OatHeader::GetKeyValueStoreSize() called on object at %p\n", header); - size = size - param_to_remove.size(); + if (store_modified) { + uint8_t* const new_store_end = WriteKeyValueStore(new_store_map, store); + *store_size_ptr = new_store_end - store; + LOGI("Store size set to %u", *store_size_ptr); + return true; } - return size; + return false; } +#define DCL_HOOK_FUNC(ret, func, ...) \ + ret (*old_##func)(__VA_ARGS__) = nullptr; \ + ret new_##func(__VA_ARGS__) + +// For Android version < 16 DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) { - LOGD("OatHeader::GetKeyValueStore() called on object at %p\n", header); - uint8_t* key_value_store_ = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); - uint32_t key_value_store_size_ = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); - LOGD("KeyValueStore via hook: [addr: %p, size: %u]", key_value_store_, key_value_store_size_); - store_resized = ModifyStoreInPlace(key_value_store_, key_value_store_size_); + uint8_t* const key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); + + SpoofKeyValueStore(key_value_store); - return key_value_store_; + return key_value_store; } +// For Android version 16+ : Intercept during checksum calculation DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32_t* checksum) { - art::OatHeader* oat_header = reinterpret_cast(header); - const uint8_t* key_value_store_ = oat_header->GetKeyValueStore(); - uint32_t key_value_store_size_ = oat_header->GetKeyValueStoreSize(); - LOGD("KeyValueStore via offset: [addr: %p, size: %u]", key_value_store_, key_value_store_size_); - store_resized = - ModifyStoreInPlace(const_cast(key_value_store_), key_value_store_size_); - if (store_resized) { - oat_header->SetKeyValueStoreSize(key_value_store_size_ - param_to_remove.size()); - } + auto* oat_header = reinterpret_cast(header); + uint8_t* const store = const_cast(oat_header->getKeyValueStore()); + + SpoofKeyValueStore(store); + + // Call original to compute checksum on our modified data old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum); - LOGD("ComputeChecksum called: %" PRIu32, *checksum); + LOGV("OAT Checksum recalculated: 0x%08X", *checksum); } #undef DCL_HOOK_FUNC void register_hook(dev_t dev, ino_t inode, const char* symbol, void* new_func, void** old_func) { - LOGD("RegisterHook: %s, %p, %p", symbol, new_func, old_func); if (!lsplt::RegisterHook(dev, inode, symbol, new_func, old_func)) { - LOGE("Failed to register plt_hook \"%s\"\n", symbol); + LOGE("Failed to register PLT hook: %s", symbol); } } @@ -129,16 +213,28 @@ void register_hook(dev_t dev, ino_t inode, const char* symbol, void* new_func, v __attribute__((constructor)) static void initialize() { dev_t dev = 0; ino_t inode = 0; - for (auto& info : lsplt::MapInfo::Scan()) { - if (info.path.starts_with("/apex/com.android.art/bin/dex2oat")) { + + // Locate the dex2oat binary in memory to get its device and inode for PLT hooking + for (const auto& info : lsplt::MapInfo::Scan()) { + if (info.path.find("bin/dex2oat") != std::string::npos) { dev = info.dev; inode = info.inode; + if (g_binary_path.empty()) g_binary_path = std::string(info.path); + LOGD("Found target: %s (dev: %ju, inode: %ju)", info.path.data(), (uintmax_t)dev, + (uintmax_t)inode); break; } } - PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv); + if (dev == 0) { + LOGE("Could not locate dex2oat memory map"); + return; + } + + // Register hook for the standard KeyValueStore getter PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader16GetKeyValueStoreEv); + + // If the standard store hook fails (e.g., on Android 16+), try the Checksum hook if (!lsplt::CommitHook()) { PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader15ComputeChecksumEPj); lsplt::CommitHook(); diff --git a/magisk-loader/magisk_module/sepolicy.rule b/magisk-loader/magisk_module/sepolicy.rule index 1c7f04d07..d0b254e05 100644 --- a/magisk-loader/magisk_module/sepolicy.rule +++ b/magisk-loader/magisk_module/sepolicy.rule @@ -1,4 +1,5 @@ allow dex2oat dex2oat_exec file execute_no_trans +allow dex2oat system_linker_exec file execute_no_trans allow shell shell dir write