From 04d003ff4d991f73b3ff833de5a3bfcf15c869dc Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 26 Jan 2026 23:12:52 +0100 Subject: [PATCH] Fix x86_64 injection: Red Zone adjustment and fallback logic (#91) - Strictly adhere to the System V AMD64 ABI by skipping the 128-byte "Red Zone" before modifying the stack, see page 23 of https://gitlab.com/x86-psABIs/x86-64-ABI/-/jobs/artifacts/master/raw/x86-64-ABI/abi.pdf?job=build for details. - Added `inject_via_staging` as a fallback strategy: 1. Copies the payload to `/data/local/tmp`. 2. Sets permissions/context (`u:object_r:system_file:s0`). 3. Loads via standard `dlopen`. 4. Immediately unlinks the file for stealth. - Introduced `RegisterRestorer` RAII class to guarantee original registers are restored even if the injection logic returns early due to error. --- app/src/main/cpp/inject/main.cpp | 216 +++++++++++++++++++++++++++---- module/sepolicy.rule | 1 + 2 files changed, 192 insertions(+), 25 deletions(-) diff --git a/app/src/main/cpp/inject/main.cpp b/app/src/main/cpp/inject/main.cpp index 94afba8..4042f0d 100644 --- a/app/src/main/cpp/inject/main.cpp +++ b/app/src/main/cpp/inject/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -310,9 +312,12 @@ static std::optional transfer_fd_to_remote(int pid, const char *lib_path, s std::vector args = {AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0}; int remote_fd = static_cast( remote_call(pid, regs, reinterpret_cast(funcs.socket_addr), libc_return_addr, args)); - if (remote_fd == -1) { + if (remote_fd <= 0) { + // remote_call returns 0 on failure. + // socket() returning 0 is technically possible (if stdin closed), + // but highly unlikely for a daemon. We treat 0 as failure here to catch the injection error. errno = get_remote_errno(); // Set local errno for PLOGE. - PLOGE("Failed to create remote socket."); + PLOGE("Failed to create remote socket (returned %d).", remote_fd); return std::nullopt; } LOGD("Successfully created remote socket with FD: %d", remote_fd); @@ -642,6 +647,136 @@ static bool remote_call_entry(int pid, struct user_regs_struct ®s, uintptr_t return true; // Return true if the call itself completed, regardless of its return value. } +/** + * @brief RAII wrapper to ensure a temporary file is deleted (unlinked) + * when the object goes out of scope. + * + * This is crucial for stealth: we want the library to exist on the filesystem + * for the shortest time possible. + */ +class ScopedFileDeleter { +public: + explicit ScopedFileDeleter(std::string path) : path_(std::move(path)) {} + + ~ScopedFileDeleter() { + if (!path_.empty()) { + LOGD("Cleaning up staged file: %s", path_.c_str()); + unlink(path_.c_str()); + } + } + + // Disable copy to prevent double-deletion issues + ScopedFileDeleter(const ScopedFileDeleter&) = delete; + ScopedFileDeleter& operator=(const ScopedFileDeleter&) = delete; + +private: + std::string path_; +}; + +/** + * @brief Copies a file from source to destination. + * + * @param src Absolute path to source file. + * @param dst Absolute path to destination file. + * @return True on success, false on failure. + */ +static bool copy_file(const char* src, const char* dst) { + std::ifstream src_file(src, std::ios::binary); + std::ofstream dst_file(dst, std::ios::binary); + + if (!src_file) { + PLOGE("Failed to open source file for copying: %s", src); + return false; + } + if (!dst_file) { + PLOGE("Failed to open destination file for copying: %s", dst); + return false; + } + + dst_file << src_file.rdbuf(); + return src_file.good() && dst_file.good(); +} + +/** + * @brief Performs injection via the "Staging" method. + * + * This strategy is used when direct FD passing fails (e.g., due to Seccomp filters). + * 1. Copies the library to a world-readable location (/data/local/tmp). + * 2. Sets permissions and SELinux context to mimic a system library. + * 3. Loads it via standard dlopen(). + * 4. Immediately deletes the file to hide tracks. + * + * @param pid The target process ID. + * @param regs The target process registers (must be Red-Zone adjusted if x86_64). + * @param local_map Local memory map. + * @param remote_map Remote memory map. + * @param lib_path The path to the original library. + * @param libc_return_addr Return address for remote calls. + * @return The handle of the loaded library, or std::nullopt on failure. + */ +static std::optional inject_via_staging(int pid, struct user_regs_struct ®s, + const std::vector &local_map, + const std::vector &remote_map, + const char *lib_path, uintptr_t libc_return_addr) { + LOGI("Initiating Staging Fallback mechanism..."); + + // 1. Generate a random path in /data/local/tmp + // /data/local/tmp is chosen because it is traversable by most contexts. + std::string staged_path = "/data/local/tmp/lib" + generateMagic(8) + ".so"; + + // Ensure the file is deleted when this function exits (Success or Failure). + // The kernel keeps the inode alive for the mapped process even after unlink. + ScopedFileDeleter file_guard(staged_path); + + LOGD("Staging library to: %s", staged_path.c_str()); + + // 2. Copy the library + if (!copy_file(lib_path, staged_path.c_str())) { + LOGE("Failed to copy library during staging."); + return std::nullopt; + } + + // 3. Set Permissions to 644 (RW-R--R--) + // This allows the target process (likely running as a specific UID) to read the file. + if (chmod(staged_path.c_str(), 0644) != 0) { + PLOGE("Failed to chmod staged file."); + return std::nullopt; + } + + // 4. Set SELinux Context + if (setfilecon(staged_path.c_str(), constants::kSystemFileContext) != 0) { + LOGW("Failed to set SELinux context on staged file. Injection might fail if target is enforcing."); + } + + // 5. Resolve 'dlopen' in the remote process + auto dlopen_addr = find_func_addr(local_map, remote_map, constants::kLibdlModule, "dlopen"); + if (!dlopen_addr) { + LOGE("Failed to find 'dlopen' in remote process."); + return std::nullopt; + } + + // 6. Push the staged path to remote memory + uintptr_t remote_path_addr = push_string(pid, regs, staged_path.c_str()); + if (remote_path_addr == 0) { + LOGE("Failed to push staged path string to remote memory."); + return std::nullopt; + } + + // 7. Call dlopen(path, RTLD_NOW) + std::vector args = {remote_path_addr, RTLD_NOW}; + uintptr_t handle = remote_call(pid, regs, reinterpret_cast(dlopen_addr), + libc_return_addr, args); + + if (handle == 0) { + std::string error_msg = get_remote_dlerror(pid, regs, local_map, remote_map, libc_return_addr); + LOGE("Staged dlopen failed. dlerror: %s", error_msg.c_str()); + return std::nullopt; + } + + LOGI("Successfully loaded staged library. Handle: %p", reinterpret_cast(handle)); + return handle; +} + /** * @brief RAII wrapper for ptrace attachment and detachment. * @@ -694,12 +829,31 @@ private: bool attached_; // Flag indicating current attachment status. }; +// RAII Class to ensure registers are always restored +class RegisterRestorer { +public: + RegisterRestorer(int pid, const struct user_regs_struct& original_regs) + : pid_(pid), regs_(original_regs) {} + + ~RegisterRestorer() { + // Always restore registers when this object goes out of scope + if (set_regs(pid_, regs_)) { + LOGD("Original registers for process %d restored.", pid_); + } else { + PLOGE("Failed to restore original registers for process %d.", pid_); + } + } +private: + int pid_; + struct user_regs_struct regs_; +}; + /** * @brief Injects a shared library into a target process using ptrace. * * This is the main orchestration function for the library injection. * It handles attachment, remote memory/register manipulation, FD transfer, - * remote dlopen/dlsym, and remote entry point execution. + * staging fallback, remote dlopen/dlsym, and remote entry point execution. * * @param pid The target process ID. * @param lib_path The absolute path to the shared library to inject. @@ -742,6 +896,14 @@ bool inject_library(int pid, const char *lib_path, const char *entry_name) { backup_regs = current_regs; // Store a copy for restoration. LOGD("Process %d registers backed up.", pid); + // Skip the Red Zone (128 bytes) on x86_64 to prevent stack corruption + #if defined(__x86_64__) + current_regs.rsp -= 128; + #endif + + // Ensures original state is restored even if injection fails/crashes mid-way. + RegisterRestorer reg_guard(pid, backup_regs); + // Create a scope to ensure RAII objects are destroyed BEFORE register restoration { // 4. Scan local and remote memory maps to resolve function addresses. @@ -760,53 +922,57 @@ bool inject_library(int pid, const char *lib_path, const char *entry_name) { } LOGD("Found libc return address: %p", reinterpret_cast(libc_return_addr)); - // 6. Transfer the library's file descriptor to the remote process. + // 6. Attempt to transfer the library's file descriptor to the remote process. + int remote_fd = -1; auto lib_fd_opt = transfer_fd_to_remote(pid, lib_path, current_regs, local_map, remote_map, reinterpret_cast(libc_return_addr)); - if (!lib_fd_opt) { - LOGE("Failed to transfer library file descriptor for '%s' to target process %d.", lib_path, pid); - return false; - } - RemoteLibraryHandle remote_lib_guard(pid, *lib_fd_opt); - LOGD("Library FD %d transferred to remote process %d.", remote_lib_guard.fd(), pid); - remote_lib_guard.set_libc_return_addr(reinterpret_cast(libc_return_addr)); + std::optional remote_lib_guard; + std::optional handle_opt; - // 7. Remotely load the library using the transferred file descriptor. - auto handle_opt = remote_dlopen(pid, current_regs, local_map, remote_map, remote_lib_guard.fd(), lib_path, - reinterpret_cast(libc_return_addr)); + if (lib_fd_opt) { + remote_fd = *lib_fd_opt; + remote_lib_guard.emplace(pid, remote_fd); + remote_lib_guard->set_libc_return_addr(reinterpret_cast(libc_return_addr)); + + LOGD("FD Transfer successful (FD: %d). Attempting android_dlopen_ext...", remote_fd); + handle_opt = remote_dlopen(pid, current_regs, local_map, remote_map, remote_fd, lib_path, + reinterpret_cast(libc_return_addr)); + } else { + LOGW("Failed to transfer library file descriptor for '%s' to target process %d.", lib_path, pid); + } + + // 7. Staging Fallback (Copy-Inject-Delete) if FD transfer failed. if (!handle_opt) { + handle_opt = inject_via_staging(pid, current_regs, local_map, remote_map, + lib_path, reinterpret_cast(libc_return_addr)); + } + if (!handle_opt || *handle_opt == 0) { LOGE("Failed to load library '%s' in remote process %d.", lib_path, pid); // If dlopen fails, the remote_lib_guard.fd() is still valid in the target process and needs to be closed. // The RemoteLibraryHandle constructor takes care of this. return false; } - remote_lib_guard.set_handle(*handle_opt); + uintptr_t handle = *handle_opt; + if (remote_lib_guard) remote_lib_guard->set_handle(handle); // 8. Find the entry point symbol in the remotely loaded library. auto entry_opt = remote_find_entry(pid, current_regs, entry_name, local_map, remote_map, - remote_lib_guard.handle(), reinterpret_cast(libc_return_addr)); + handle, reinterpret_cast(libc_return_addr)); if (!entry_opt) { LOGE("Failed to find entry point '%s' in remote library (handle %p).", entry_name, - reinterpret_cast(remote_lib_guard.handle())); + reinterpret_cast(handle)); return false; } uintptr_t entry_addr = *entry_opt; // 9. Call the remote entry point function. - if (!remote_call_entry(pid, current_regs, entry_addr, remote_lib_guard.handle(), + if (!remote_call_entry(pid, current_regs, entry_addr, handle, reinterpret_cast(libc_return_addr))) { LOGE("Failed to call remote entry point '%s'.", entry_name); return false; } } - // 10. Restore original registers of the target process. - if (!set_regs(pid, backup_regs)) { - LOGE("Failed to restore original registers for process %d.", pid); - return false; - } - LOGD("Original registers for process %d restored.", pid); - LOGI("Library injection completed successfully for process %d.", pid); return true; } diff --git a/module/sepolicy.rule b/module/sepolicy.rule index c1d02a4..bf6a57b 100644 --- a/module/sepolicy.rule +++ b/module/sepolicy.rule @@ -1,4 +1,5 @@ allow keystore system_file unix_dgram_socket * allow system_file keystore unix_dgram_socket * allow keystore system_file file * +allow keystore shell_data_file dir * allow crash_dump keystore process *