#include #include #include #include #include #include #include #include #include #include "logging.h" #include "oat.h" /** * 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)); // 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& 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); return reinterpret_cast(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(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()); } 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, ...) \ ret (*old_##func)(__VA_ARGS__) = nullptr; \ ret new_##func(__VA_ARGS__) // For Android version < 16 DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) { uint8_t* const key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); 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) { 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); 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) { if (!lsplt::RegisterHook(dev, inode, symbol, new_func, old_func)) { LOGE("Failed to register PLT hook: %s", symbol); } } #define PLT_HOOK_REGISTER_SYM(DEV, INODE, SYM, NAME) \ register_hook(DEV, INODE, SYM, reinterpret_cast(new_##NAME), \ reinterpret_cast(&old_##NAME)) #define PLT_HOOK_REGISTER(DEV, INODE, NAME) PLT_HOOK_REGISTER_SYM(DEV, INODE, #NAME, NAME) __attribute__((constructor)) static void initialize() { dev_t dev = 0; ino_t inode = 0; // 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; } } 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(); } }