Refactor dex2oat wrapper to solve long-standing issues (#515)

Manual library path injection via LD_LIBRARY_PATH has become unreliable due to symbol mismatches in core libraries (e.g., `libc++`) between the system and APEX partitions. Recent updates to `liblog` and `libbase` (in Android 16) have resulted in missing symbols like `__hash_memory` or `fmt` when the ART APEX binaries are forced to load system-partition shims.

This commit switches the wrapper to execute the runtime APEX linker directly (e.g., `/apex/com.android.runtime/bin/linker64`). By passing the dex2oat binary to the linker via `/proc/self/fd/`, the linker can properly initialize internal namespaces and resolve dependencies from the correct APEX and bootstrap locations.

Moreover, for the OatHeader hook, a bug introduced in 6703b45350 is now fixed, where the target functions of PLT hooks are overwritten by the our helper functions.

Details of the refactored project are explained in README.
This commit is contained in:
JingMatrix 2026-01-25 10:13:57 +01:00 committed by GitHub
parent 211bd5f115
commit 1436f692fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 357 additions and 236 deletions

View File

@ -229,7 +229,8 @@ void Logcat::ProcessBuffer(struct log_msg *buf) {
tag == "APatchD"sv || tag == "Dobby"sv || tag.starts_with("dex2oat"sv) || tag == "APatchD"sv || tag == "Dobby"sv || tag.starts_with("dex2oat"sv) ||
tag == "KernelSU"sv || tag == "LSPlant"sv || tag == "LSPlt"sv || tag == "KernelSU"sv || tag == "LSPlant"sv || tag == "LSPlt"sv ||
tag.starts_with("LSPosed"sv) || tag == "Magisk"sv || tag == "SELinux"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()); verbose_print_count_ += PrintLogLine(entry, verbose_file_.get());
} }
if (entry.pid == my_pid_ && tag == "LSPosedLogcat"sv) [[unlikely]] { if (entry.pid == my_pid_ && tag == "LSPosedLogcat"sv) [[unlikely]] {

37
dex2oat/README.md Normal file
View File

@ -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.

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2022 LSPosed Contributors
*/
plugins { plugins {
alias(libs.plugins.agp.lib) alias(libs.plugins.agp.lib)
} }
android { android {
namespace = "org.lsposed.dex2oat" namespace = "org.matrix.vector.dex2oat"
buildFeatures { androidResources.enable = false
androidResources = false
buildConfig = false
prefab = true
prefabPublishing = true
}
defaultConfig {
minSdk = 29
}
externalNativeBuild { externalNativeBuild {
cmake { cmake {
path("src/main/cpp/CMakeLists.txt") path("src/main/cpp/CMakeLists.txt")
} }
} }
prefab {
register("dex2oat")
}
} }

View File

@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10)
project(dex2oat) project(dex2oat)
add_executable(dex2oat dex2oat.cpp) 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) OPTION(LSPLT_BUILD_SHARED OFF)
add_subdirectory(${EXTERNAL_ROOT}/lsplt/lsplt/src/main/jni external) add_subdirectory(${EXTERNAL_ROOT}/lsplt/lsplt/src/main/jni external)

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2022 LSPosed Contributors
*/
//
// Created by Nullptr on 2022/4/1.
//
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/un.h> #include <sys/un.h>
#include <unistd.h> #include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include "logging.h" #include "logging.h"
// Access to the process environment variables
extern "C" char **environ;
#if defined(__LP64__) #if defined(__LP64__)
#define LP_SELECT(lp32, lp64) lp64 #define LP_SELECT(lp32, lp64) lp64
#else #else
#define LP_SELECT(lp32, lp64) lp32 #define LP_SELECT(lp32, lp64) lp32
#endif #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<int>(is64) << 1) | static_cast<int>(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) { if (rec < 0) {
PLOGE("recvmsg"); PLOGE("recvmsg");
} }
return rec; 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 = { struct iovec iov = {
.iov_base = &cnt, .iov_base = &cnt,
.iov_len = sizeof(cnt), .iov_len = sizeof(cnt),
}; };
struct msghdr msg = { struct msghdr msg = {.msg_name = nullptr,
.msg_iov = &iov, .msg_iovlen = 1, .msg_control = cmsgbuf, .msg_controllen = bufsz}; .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); 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_len != CMSG_LEN(sizeof(int) * cnt) || cmsg->cmsg_level != SOL_SOCKET ||
cmsg->cmsg_type != SCM_RIGHTS) { cmsg->cmsg_type != SCM_RIGHTS) {
return NULL; return nullptr;
} }
return CMSG_DATA(cmsg); 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))]; char cmsgbuf[CMSG_SPACE(sizeof(int))];
void *data = recv_fds(sockfd, cmsgbuf, sizeof(cmsgbuf), 1); void *data = recv_fds(sockfd, cmsgbuf, sizeof(cmsgbuf), 1);
if (data == NULL) return -1; if (data == nullptr) return -1;
int result; int result;
memcpy(&result, data, sizeof(int)); std::memcpy(&result, data, sizeof(int));
return result; return result;
} }
static int read_int(int fd) { /**
* Reads an integer acknowledgment from the socket.
*/
int read_int(int fd) {
int val; int val;
if (read(fd, &val, sizeof(val)) != sizeof(val)) return -1; if (read(fd, &val, sizeof(val)) != sizeof(val)) return -1;
return val; 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; if (fd < 0) return;
write(fd, &val, sizeof(val)); (void)write(fd, &val, sizeof(val));
} }
} // namespace
int main(int argc, char **argv) { int main(int argc, char **argv) {
LOGD("dex2oat wrapper ppid=%d", getppid()); LOGD("dex2oat wrapper ppid=%d", getppid());
// Prepare Unix domain socket address (Abstract Namespace)
struct sockaddr_un sock = {}; struct sockaddr_un sock = {};
sock.sun_family = AF_UNIX; 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); 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, reinterpret_cast<struct sockaddr *>(&sock), len)) {
if (connect(sock_fd, (struct sockaddr *)&sock, len)) {
PLOGE("failed to connect to %s", sock.sun_path + 1); PLOGE("failed to connect to %s", sock.sun_path + 1);
return 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); int stock_fd = recv_fd(sock_fd);
read_int(sock_fd); read_int(sock_fd); // Sync
close(sock_fd); close(sock_fd);
// 2. Get liboat_hook.so FD
sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (connect(sock_fd, (struct sockaddr *)&sock, len)) { if (connect(sock_fd, reinterpret_cast<struct sockaddr *>(&sock), len)) {
PLOGE("failed to connect to %s", sock.sun_path + 1); PLOGE("failed to connect to %s", sock.sun_path + 1);
return 1; return 1;
} }
write_int(sock_fd, LP_SELECT(4, 5)); write_int(sock_fd, LP_SELECT(4, 5));
int hooker_fd = recv_fd(sock_fd); int hooker_fd = recv_fd(sock_fd);
read_int(sock_fd); read_int(sock_fd); // Sync
close(sock_fd); close(sock_fd);
if (hooker_fd == -1) { if (hooker_fd == -1) {
PLOGE("failed to read liboat_hook.so"); LOGE("failed to read liboat_hook.so");
} }
LOGD("sock: %s %d", sock.sun_path + 1, stock_fd); LOGD("sock: %s stock_fd: %d", sock.sun_path + 1, stock_fd);
const char *new_argv[argc + 2]; // Prepare arguments for execve
for (int i = 0; i < argc; i++) new_argv[i] = argv[i]; // Logic: [linker] [/proc/self/fd/stock_fd] [original_args...] [--inline-max-code-units=0]
new_argv[argc] = "--inline-max-code-units=0"; std::vector<const char *> exec_argv;
new_argv[argc + 1] = NULL;
if (getenv("LD_LIBRARY_PATH") == NULL) { const char *linker_path =
char const *libenv = LP_SELECT( LP_SELECT("/apex/com.android.runtime/bin/linker", "/apex/com.android.runtime/bin/linker64");
"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"); char stock_fd_path[64];
putenv((char *)libenv); 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]);
} }
// Set LD_PRELOAD to load liboat_hook.so // Append hooking flags to disable inline, which is our purpose of this wrapper, since we cannot
const int STRING_BUFFER = 50; // hook inlined target methods.
char env_str[STRING_BUFFER]; exec_argv.push_back("--inline-max-code-units=0");
snprintf(env_str, STRING_BUFFER, "LD_PRELOAD=/proc/%d/fd/%d", getpid(), hooker_fd); exec_argv.push_back(nullptr);
putenv(env_str);
LOGD("Set env %s", env_str);
fexecve(stock_fd, (char **)new_argv, environ); // Setup Environment variables
// Clear LD_LIBRARY_PATH to let the linker use internal config
unsetenv("LD_LIBRARY_PATH");
PLOGE("fexecve failed"); // 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]);
}
LOGI("Executing via linker: %s executing %s", linker_path, stock_fd_path);
// Perform the execution
execve(linker_path, const_cast<char *const *>(exec_argv.data()), environ);
// If we reach here, execve failed
PLOGE("execve failed");
return 2; return 2;
} }

View File

@ -1,10 +1,10 @@
#pragma once #pragma once
#include <errno.h>
#include <android/log.h> #include <android/log.h>
#include <errno.h>
#ifndef LOG_TAG #ifndef LOG_TAG
#define LOG_TAG "LSPosedDex2Oat" #define LOG_TAG "VectorDex2Oat"
#endif #endif
#ifdef LOG_DISABLED #ifdef LOG_DISABLED
@ -15,11 +15,7 @@
#define LOGE(...) 0 #define LOGE(...) 0
#else #else
#ifndef NDEBUG #ifndef NDEBUG
#define LOGD(fmt, ...) \ #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, \
"%s:%d#%s" \
": " fmt, \
__FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__)
#define LOGV(fmt, ...) \ #define LOGV(fmt, ...) \
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, \ __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, \
"%s:%d#%s" \ "%s:%d#%s" \

View File

@ -72,17 +72,9 @@ public:
static constexpr const char kTrueValue[] = "true"; static constexpr const char kTrueValue[] = "true";
static constexpr const char kFalseValue[] = "false"; static constexpr const char kFalseValue[] = "false";
static constexpr size_t Get_key_value_store_size_Offset() { // Added helper to access the key_value_store_ field, which could be fragile across
return offsetof(OatHeader, key_value_store_size_); // different Android versions and compiler optimizations.
} const uint8_t* getKeyValueStore() const { return key_value_store_; }
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);
void ComputeChecksum(/*inout*/ uint32_t* checksum) const; void ComputeChecksum(/*inout*/ uint32_t* checksum) const;

View File

@ -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<uint32_t*>((uintptr_t)this + Get_key_value_store_size_Offset()) = new_size;
}
} // namespace art

View File

@ -1,122 +1,206 @@
#include <dlfcn.h> #include <dlfcn.h>
#include <cinttypes> #include <algorithm>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <lsplt.hpp> #include <lsplt.hpp>
#include <map>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector>
#include "logging.h" #include "logging.h"
#include "oat.h" #include "oat.h"
const std::string_view param_to_remove = " --inline-max-code-units=0"; /**
* 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<std::string> 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));
// 1. Replace the command path (argv[0])
if (!tokens.empty()) {
tokens[0] = std::string(new_cmd_path);
}
// 2. Remove the injected parameter if it exists
auto it = std::remove(tokens.begin(), tokens.end(), std::string(kParamToRemove));
tokens.erase(it, tokens.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;
}
/**
* Re-serializes the Key-Value map back into the OAT header memory space.
*/
uint8_t* WriteKeyValueStore(const std::map<std::string, std::string>& key_values, uint8_t* store) {
LOGD("Writing KeyValueStore back to memory");
char* data_ptr = reinterpret_cast<char*>(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<uint8_t*>(data_ptr) - store);
return reinterpret_cast<uint8_t*>(data_ptr);
}
// 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; });
}
/**
* 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<uint32_t*>(store - sizeof(uint32_t));
uint32_t const store_size = *store_size_ptr;
const char* ptr = reinterpret_cast<const char*>(store);
const char* const store_end = ptr + store_size;
std::map<std::string, std::string> 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<const char*>(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<const char*>(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<char*>(value_start), 0, original_capacity);
// Write the new command.
std::memcpy(const_cast<char*>(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());
}
ptr = value_end + 1;
if (has_padding) {
while (*ptr == '\0') {
ptr++;
}
}
}
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 false;
}
#define DCL_HOOK_FUNC(ret, func, ...) \ #define DCL_HOOK_FUNC(ret, func, ...) \
ret (*old_##func)(__VA_ARGS__); \ ret (*old_##func)(__VA_ARGS__) = nullptr; \
ret new_##func(__VA_ARGS__) ret new_##func(__VA_ARGS__)
bool store_resized = false; // For Android version < 16
bool ModifyStoreInPlace(uint8_t* store, uint32_t store_size) {
if (store == nullptr || store_size == 0) {
return false;
}
// Define the search space
uint8_t* const store_begin = store;
uint8_t* const store_end = store + store_size;
// 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());
// 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;
}
uint8_t* location_of_param = it;
LOGD("Parameter found at offset %td.", location_of_param - store_begin);
// 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;
// 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;
}
}
// 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);
}
// 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();
}
return size;
}
DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) { DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) {
LOGD("OatHeader::GetKeyValueStore() called on object at %p\n", header); uint8_t* const key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(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_);
return key_value_store_; SpoofKeyValueStore(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) { DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32_t* checksum) {
art::OatHeader* oat_header = reinterpret_cast<art::OatHeader*>(header); auto* oat_header = reinterpret_cast<art::OatHeader*>(header);
const uint8_t* key_value_store_ = oat_header->GetKeyValueStore(); uint8_t* const store = const_cast<uint8_t*>(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_); SpoofKeyValueStore(store);
store_resized =
ModifyStoreInPlace(const_cast<uint8_t*>(key_value_store_), key_value_store_size_); // Call original to compute checksum on our modified data
if (store_resized) {
oat_header->SetKeyValueStoreSize(key_value_store_size_ - param_to_remove.size());
}
old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum); old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum);
LOGD("ComputeChecksum called: %" PRIu32, *checksum); LOGV("OAT Checksum recalculated: 0x%08X", *checksum);
} }
#undef DCL_HOOK_FUNC #undef DCL_HOOK_FUNC
void register_hook(dev_t dev, ino_t inode, const char* symbol, void* new_func, void** old_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)) { 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() { __attribute__((constructor)) static void initialize() {
dev_t dev = 0; dev_t dev = 0;
ino_t inode = 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; dev = info.dev;
inode = info.inode; 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; 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); PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader16GetKeyValueStoreEv);
// If the standard store hook fails (e.g., on Android 16+), try the Checksum hook
if (!lsplt::CommitHook()) { if (!lsplt::CommitHook()) {
PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader15ComputeChecksumEPj); PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader15ComputeChecksumEPj);
lsplt::CommitHook(); lsplt::CommitHook();

View File

@ -1,4 +1,5 @@
allow dex2oat dex2oat_exec file execute_no_trans allow dex2oat dex2oat_exec file execute_no_trans
allow dex2oat system_linker_exec file execute_no_trans
allow shell shell dir write allow shell shell dir write