From ffb27915e2872ca507647209bf7b862e686715a6 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 22 Nov 2025 20:58:12 +0100 Subject: [PATCH] Implement shared library injection via ptrace There are still many functions in the header `utils.hpp` not implemented yet, which are however not needed for our purpose. --- app/src/main/cpp/CMakeLists.txt | 3 +- app/src/main/cpp/include/logging.hpp | 10 + app/src/main/cpp/include/utils.hpp | 499 +++++++++++++ app/src/main/cpp/inject/main.cpp | 876 +++++++++++++++++++++- app/src/main/cpp/inject/utils.cpp | 1030 ++++++++++++++++++++++++++ 5 files changed, 2415 insertions(+), 3 deletions(-) create mode 100644 app/src/main/cpp/include/logging.hpp create mode 100644 app/src/main/cpp/include/utils.hpp create mode 100644 app/src/main/cpp/inject/utils.cpp diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 267829d..700db08 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -10,7 +10,8 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions") OPTION(LSPLT_BUILD_SHARED OFF) add_subdirectory(external/LSPlt/lsplt/src/main/jni) -add_executable(libinject.so inject/main.cpp) +add_executable(libinject.so inject/main.cpp inject/utils.cpp) +target_include_directories(libinject.so PUBLIC include) target_link_libraries(libinject.so PRIVATE lsplt_static) add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp) diff --git a/app/src/main/cpp/include/logging.hpp b/app/src/main/cpp/include/logging.hpp new file mode 100644 index 0000000..1b2b19e --- /dev/null +++ b/app/src/main/cpp/include/logging.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +#ifndef LOG_TAG +#define LOG_TAG "TEESimulator" +#endif + +#include "../logging.hpp" diff --git a/app/src/main/cpp/include/utils.hpp b/app/src/main/cpp/include/utils.hpp new file mode 100644 index 0000000..0d671a9 --- /dev/null +++ b/app/src/main/cpp/include/utils.hpp @@ -0,0 +1,499 @@ +#pragma once + +#include // For std::swap in UniqueFd +#include // For PATH_MAX +#include +#include +#include +#include +#include + +#include "lsplt.hpp" + +// Macros for syscall error checking. These are typically used after remote +// syscall emulation. +#define SYSCALL_IS_ERR(e) (((unsigned long)e) > -4096UL) // Checks if a syscall return value indicates an error. +#define SYSCALL_ERR(e) (-(int)(e)) // Converts a syscall error value to a negative errno. + +// Architecture-specific register definitions. +// These macros abstract away the differences in register names across architectures, +// allowing for generic code that manipulates `struct user_regs_struct`. +#if defined(__x86_64__) +# define REG_SP rsp // Stack pointer register +# define REG_IP rip // Instruction pointer register +# define REG_RET rax // Return value register +# define REG_NR orig_rax // Syscall number register +# define REG_SYS_ARG0 rdi // First syscall argument register +#elif defined(__i386__) +# define REG_SP esp +# define REG_IP eip +# define REG_RET eax +# define REG_NR orig_eax +# define REG_SYS_ARG0 ebx +#elif defined(__aarch64__) +# define REG_SP sp // Stack pointer register (AArch64) +# define REG_IP pc // Program counter register (AArch64) +# define REG_RET regs[0] // Return value register (x0) +# define REG_NR regs[8] // Syscall number register (x8) +# define REG_SYS_ARG0 regs[0] // First syscall argument register (x0) +#elif defined(__arm__) +# define REG_SP uregs[13] // Stack pointer register (R13) +# define REG_IP uregs[15] // Program counter register (R15) +# define REG_RET uregs[0] // Return value register (R0) +# define REG_NR uregs[7] // Syscall number register (R7) +# define REG_SYS_ARG0 uregs[0] // First syscall argument register (R0) +# define user_regs_struct user_regs // ARM's equivalent to user_regs_struct is user_regs +# define SYS_mmap SYS_mmap2 // ARM uses mmap2 syscall +#endif + +// --- Remote Memory Operations --- + +/** + * @brief Writes data to the remote process's memory. + * @param pid The target process ID. + * @param remote_addr The target address in the remote process. + * @param buf A pointer to the local buffer containing data to write. + * @param len The number of bytes to write. + * @param use_proc_mem If true, uses /proc//mem; otherwise, uses + * process_vm_writev. + * @return The number of bytes written, or -1 on error. + */ +ssize_t write_proc(int pid, uintptr_t remote_addr, const void *buf, size_t len, bool use_proc_mem = false); + +/** + * @brief Reads data from the remote process's memory. + * @param pid The target process ID. + * @param remote_addr The source address in the remote process. + * @param buf A pointer to the local buffer to store the read data. + * @param len The number of bytes to read. + * @return The number of bytes read, or -1 on error. + */ +ssize_t read_proc(int pid, uintptr_t remote_addr, void *buf, size_t len); + +// --- Remote Register Operations --- + +/** + * @brief Retrieves the current CPU registers of the target process. + * @param pid The target process ID. + * @param regs A reference to a `user_regs_struct` to store the registers. + * @return True on success, false on failure. + */ +bool get_regs(int pid, struct user_regs_struct ®s); + +/** + * @brief Sets the CPU registers of the target process. + * @param pid The target process ID. + * @param regs A reference to a `user_regs_struct` containing the registers to set. + * @return True on success, false on failure. + */ +bool set_regs(int pid, struct user_regs_struct ®s); + +// --- Module and Symbol Resolution --- + +/** + * @brief Gets a descriptive string of the memory region containing a given + * address. + * @param map_info A vector of `lsplt::MapInfo` for the process. + * @param addr The address to look up. + * @return A string representing the memory region (e.g., "path perms"), or "". + */ +std::string get_addr_mem_region(const std::vector &map_info, uintptr_t addr); + +/** + * @brief Finds the base address of a module in a process's memory map. + * @param map_info A vector of `lsplt::MapInfo` for the process. + * @param module_suffix The suffix of the module path (e.g., "libc.so"). + * @return The base address of the module, or nullptr if not found. + */ +void *find_module_base(const std::vector &map_info, std::string_view module_suffix); + +/** + * @brief Finds the address of a function in a remote process by resolving it + * locally and calculating the offset. + * + * This function opens the module locally, finds the symbol address, + * calculates its offset from the local module base, and then adds that offset to the remote module base. + * + * @param local_map_info Memory map of the local (injector) process. + * @param remote_map_info Memory map of the remote (target) process. + * @param module_name The name of the module (e.g., "libc.so"). + * @param function_name The name of the function (e.g., "open"). + * @return The remote address of the function, or nullptr if not found. + */ +void *find_func_addr(const std::vector &local_map_info, + const std::vector &remote_map_info, std::string_view module_name, + std::string_view function_name); + +/** + * @brief Finds a suitable return address within a specific module in the remote + * process. + * + * This typically looks for a non-executable segment of the module to return to, + * as `PTRACE_CONT` will resume execution at the specified instruction pointer. + * + * @param map_info A vector of `lsplt::MapInfo` for the remote process. + * @param module_suffix The suffix of the module path (e.g., "libc.so"). + * @return A pointer to a suitable return address, or nullptr if not found. + */ +void *find_module_return_addr(const std::vector &map_info, std::string_view module_suffix); + +// --- Remote Stack Manipulation --- + +/** + * @brief Aligns the stack pointer (`REG_SP`) to ensure proper stack frame setup. + * @param regs A reference to the `user_regs_struct` to modify. + * @param preserve_bytes Number of bytes to preserve below the new stack pointer. + */ +void align_stack(struct user_regs_struct ®s, uintptr_t preserve_bytes = 0); + +/** + * @brief Pushes a block of memory onto the remote process's stack. + * + * This function decrements the stack pointer, aligns it, and then writes the data. + * + * @param pid The target process ID. + * @param regs A reference to the `user_regs_struct` (its stack pointer will be updated). + * @param data A pointer to the local data to push. + * @param length The number of bytes to push. + * @return The remote address where the data was pushed, or 0 on error. + */ +uintptr_t push_memory(int pid, struct user_regs_struct ®s, const void *data, size_t length); + +/** + * @brief Pushes a null-terminated string onto the remote process's stack. + * @param pid The target process ID. + * @param regs A reference to the `user_regs_struct` (its stack pointer will be updated). + * @param str The null-terminated C-style string to push. + * @return The remote address where the string was pushed, or 0 on error. + */ +uintptr_t push_string(int pid, struct user_regs_struct ®s, const char *str); + +// --- Remote Function Call Emulation --- + +/** + * @brief Prepares and initiates a remote function call in the target process. + * + * This function sets up registers (arguments, return address, instruction pointer) and + * then continues the target process execution using PTRACE_CONT. + * + * @param pid The target process ID. + * @param regs A reference to the `user_regs_struct` (will be modified). + * @param func_addr The remote address of the function to call. + * @param return_addr The address in the remote process where execution should + * resume after the call. + * @param args A vector of `uintptr_t` representing the function arguments. + * @return True if the remote call was successfully initiated, false otherwise. + */ +bool remote_pre_call(int pid, struct user_regs_struct ®s, uintptr_t func_addr, uintptr_t return_addr, + std::vector &args); + +/** + * @brief Waits for and finalizes a remote function call, retrieving its return value. + * + * This function waits for the target process to stop after a remote call and + * then retrieves the return value from the appropriate register. + * + * @param pid The target process ID. + * @param regs A reference to the `user_regs_struct` (will be updated with post-call registers). + * @param expected_return_addr The address where the remote call was expected to return to. + * Used for error checking (e.g., if a crash occurs elsewhere). + * @return The return value of the remote function, or 0 on error. + */ +uintptr_t remote_post_call(int pid, struct user_regs_struct ®s, uintptr_t expected_return_addr); + +/** + * @brief Executes a complete remote function call (pre-call, continue, + * post-call). + * @param pid The target process ID. + * @param regs A reference to the `user_regs_struct` (will be modified). + * @param func_addr The remote address of the function to call. + * @param return_addr The address in the remote process where execution should resume after the call. + * @param args A vector of `uintptr_t` representing the function arguments. + * @return The return value of the remote function, or 0 on error. + */ +uintptr_t remote_call(int pid, struct user_regs_struct ®s, uintptr_t func_addr, uintptr_t return_addr, + std::vector &args); + +// --- Process Management and Ptrace Utilities --- + +/** + * @brief Forks twice to create a daemon process, returning 0 in the daemon, + * or the child pid in parent. + * @return 0 in the grand-child (daemon), PID of first child in parent, or -1 on error. + */ +int fork_dont_care(); + +/** + * @brief Waits for the target process to stop due to ptrace. + * + * This function handles `EINTR` and ensures the process is actually stopped. + * + * @param pid The target process ID. + * @param status A pointer to an integer to store the wait status. + * @param flags Flags for `waitpid` (e.g., `__WALL`). + * @return True if the process successfully stopped, false otherwise. + */ +bool wait_for_trace(int pid, int *status, int flags); + +/** + * @brief Parses the wait status integer into a human-readable string. + * @param status The status integer returned by `waitpid`. + * @return A string describing the wait status. + */ +std::string parse_status(int status); + +/** + * @brief Retrieves the executable path of a process. + * @param pid The target process ID. + * @return The absolute path to the executable, or an empty string on error. + */ +std::string get_program(int pid); + +/** + * @brief Gets the command-line arguments of a process. + * @param pid The target process ID. + * @return A vector of strings representing the command-line arguments. + */ +std::vector get_cmdline(int pid); + +/** + * @brief Parses the `exec` status of a process + * @param pid The target process ID. + * @return A string representing the `exec` status (placeholder). + */ +std::string parse_exec(int pid); + +/** + * @brief Skips the current syscall in the target process + * @param pid The target process ID. + * @return True on success, false on failure (placeholder). + */ +bool skip_syscall(int pid); + +/** + * @brief Executes a syscall in the remote process using ptrace. + * @param pid The target process ID. + * @param ret Reference to store the syscall return value. + * @param nr The syscall number. + * @param arg0 to arg5 - Syscall arguments. + * @return True on success, false on failure. + */ +bool do_syscall(int pid, uintptr_t &ret, int nr, uintptr_t arg0 = 0, uintptr_t arg1 = 0, uintptr_t arg2 = 0, + uintptr_t arg3 = 0, uintptr_t arg4 = 0, uintptr_t arg5 = 0); + +/** + * @brief Switches the mount namespace of the current process to that of the target PID, or restores it. + * @param pid If non-zero, switches to the namespace of `pid`. + * If zero, restores to the namespace stored in `*fd`. + * @param fd On entry (pid != 0), points to an int to store the original namespace FD. + * On entry (pid == 0), points to the FD of the namespace to restore to. + * FD is consumed/set to kInvalidFd on successful restore. + * @return True on success, false on failure. + */ +bool switch_mnt_ns(int pid, int *fd); + +/** + * @brief Remotely calls mmap in the target process. + * @param pid The target process ID. + * @param addr The preferred starting address for the new mapping. + * @param size The length of the mapping. + * @param prot Protection flags (PROT_READ, PROT_WRITE, PROT_EXEC). + * @param flags Mapping flags (MAP_PRIVATE, MAP_ANONYMOUS, etc.). + * @param fd File descriptor to map from (or -1 for anonymous). + * @param offset Offset into the file (or 0 for anonymous). + * @return The starting address of the new mapping, or MAP_FAILED on error. + */ +uintptr_t remote_mmap(int pid, uintptr_t addr, size_t size, int prot, int flags, int fd, off_t offset); + +/** + * @brief Remotely calls munmap in the target process. + * @param pid The target process ID. + * @param addr The starting address of the region to unmap. + * @param size The length of the region to unmap. + * @return True on success, false on failure. + */ +bool remote_munmap(int pid, uintptr_t addr, size_t size); + +/** + * @brief Remotely calls open in the target process. + * @param pid The target process ID. + * @param path_addr The remote address of the path string. + * @param flags Open flags (O_RDONLY, O_WRONLY, O_CREAT, etc.). + * @return The file descriptor in the remote process, or -1 on error. + */ +int remote_open(int pid, uintptr_t path_addr, int flags); + +/** + * @brief Remotely calls close in the target process. + * @param pid The target process ID. + * @param fd The file descriptor in the remote process to close. + * @return True on success, false on failure. + */ +bool remote_close(int pid, int fd); + +/** + * @brief Waits for a child process to terminate. + * @param pid The child process ID. + * @return The exit status of the child, or -1 on error. + */ +int wait_for_child(int pid); + +/** + * @brief Determines the ELF class (32-bit or 64-bit) of an executable file. + * @param path The path to the ELF file. + * @return `ELFCLASS32` for 32-bit, `ELFCLASS64` for 64-bit, or `ELFNONE` on error. + */ +int get_elf_class(std::string_view path); + +// --- Miscellaneous Utilities --- + +constexpr size_t kMaxPathLength = PATH_MAX; // Max path length, consistent with main.cpp +constexpr size_t kDefaultMagicLength = 16; // Default length for generated magic strings. + +/** + * @brief Generates a random alphanumeric string. + * @param length The desired length of the magic string. + * @return The generated magic string. + */ +std::string generateMagic(size_t length); + +/** + * @brief Sets the SELinux security context of a file. + * @param file_path The path to the file. + * @param security_context The new security context string. + * @return 0 on success, -1 on failure. + */ +int setfilecon(const char *file_path, const char *security_context); + +/** + * @brief RAII wrapper for file descriptors. + * + * This class automatically closes the file descriptor when it goes out of scope. + */ +class UniqueFd { + using Fd = int; // Alias for file descriptor type. + +public: + /** + * @brief Default constructor. Initializes with an invalid FD. + */ + UniqueFd() = default; + + /** + * @brief Constructor that takes an existing file descriptor. + * @param fd The file descriptor to manage. + */ + UniqueFd(Fd fd) : fd_(fd) {} + + /** + * @brief Destructor. Closes the managed file descriptor if valid. + */ + ~UniqueFd() { + if (fd_ >= 0) + close(fd_); + } + + // Delete copy constructor and assignment operator to prevent double-free issues. + UniqueFd(const UniqueFd &) = delete; + UniqueFd &operator=(const UniqueFd &) = delete; + + /** + * @brief Move constructor. Transfers ownership of the file descriptor. + * @param other The `UniqueFd` object to move from. + */ + UniqueFd(UniqueFd &&other) noexcept { + std::swap(fd_, other.fd_); + } + + /** + * @brief Move assignment operator. Transfers ownership of the file descriptor. + * @param other The `UniqueFd` object to move from. + * @return A reference to this `UniqueFd` object. + */ + UniqueFd &operator=(UniqueFd &&other) noexcept { + if (this != &other) { // Handle self-assignment + if (fd_ >= 0) + close(fd_); // Close current FD before taking ownership + fd_ = -1; // Invalidate current FD before swap + std::swap(fd_, other.fd_); + } + return *this; + } + + /** + * @brief Assignment from raw int FD. Closes the current FD. + */ + UniqueFd &operator=(Fd fd) { + if (fd_ >= 0) { + close(fd_); + } + fd_ = fd; + return *this; + } + + /** + * @brief Allows implicit conversion to the underlying file descriptor type. + * @return The managed file descriptor. + */ + operator const Fd &() const { + return fd_; + } + +private: + Fd fd_ = -1; // The managed file descriptor, initialized to invalid. +}; + +/** + * @brief Sets the SELinux context for newly created sockets. + * + * This allows the injector to create sockets with a specific security context + * that might be required for interaction with target processes under SELinux. + * It attempts to write to `/proc/thread-self/attr/sockcreate` or a process-specific fallback. + * + * @param security_context The SELinux context string to set. + * @return True on success, false on failure. + */ +bool set_sockcreate_con(const char *security_context); + +// --- Ptrace Event and Signal Parsing --- + +#define WPTEVENT(x) (x >> 16) // Macro to extract the ptrace event code from wait status. +#define CASE_CONST_RETURN(x) \ + case x: \ + return #x; // Helper macro for switch-case to return string literal. + +/** + * @brief Parses a ptrace event code into a human-readable string. + * @param status The wait status containing the ptrace event code. + * @return A string representing the ptrace event. + */ +inline const char *parse_ptrace_event(int status) { + status = WPTEVENT(status); // Extract the event code. + switch (status) { + CASE_CONST_RETURN(PTRACE_EVENT_FORK) + CASE_CONST_RETURN(PTRACE_EVENT_VFORK) + CASE_CONST_RETURN(PTRACE_EVENT_CLONE) + CASE_CONST_RETURN(PTRACE_EVENT_EXEC) + CASE_CONST_RETURN(PTRACE_EVENT_VFORK_DONE) + CASE_CONST_RETURN(PTRACE_EVENT_EXIT) + CASE_CONST_RETURN(PTRACE_EVENT_SECCOMP) + CASE_CONST_RETURN(PTRACE_EVENT_STOP) // Not a standard event, but sometimes + // seen for special stops + default: + return "(no event)"; // Default for unknown or no event. + } +} + +/** + * @brief Returns the abbreviated name of a signal. + * @param sig The signal number. + * @return The abbreviated signal name (e.g., "SIGSEGV"), or "(unknown)". + */ +inline const char *sigabbrev_np(int sig) { + // NSIG is the total number of signals, sys_signame array is indexed by signal + // number. Note: sys_signame is part of glibc and may require _GNU_SOURCE or + // similar. Assuming its availability for professional refactor. + if (sig > 0 && sig < NSIG) + return sys_signame[sig]; + return "(unknown)"; +} diff --git a/app/src/main/cpp/inject/main.cpp b/app/src/main/cpp/inject/main.cpp index f134d92..94afba8 100644 --- a/app/src/main/cpp/inject/main.cpp +++ b/app/src/main/cpp/inject/main.cpp @@ -1,3 +1,875 @@ -#include "lsplt.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -int main(int argc, char **argv) { return 0; } +#include +#include +#include +#include +#include +#include +#include + +#include "logging.hpp" // Custom logging utilities +#include "lsplt.hpp" // Library for scanning memory maps +#include "utils.hpp" // Utility functions for ptrace, remote memory, etc. + +using namespace std::string_literals; + +/* + +-----------------------------------+ + | Injector (main.cpp) | + +-----------------------------------+ + | + | 1. PTRACE_ATTACH: Attach to target process + V ++-----------------------------------------------------------------+ +| Target Process (PID) | +| | +| +-----------------------------------------------------------+ | +| | Registers Backup / Restore (Ptrace) | | +| +-----------------------------------------------------------+ | +| ^ | +| | 2. GET/SET REGS: Save and restore | +| v the target's CPU registers. | +| +-----------------------------------------------------------+ | +| | Memory Map Scanning (lsplt::MapInfo) | | +| +-----------------------------------------------------------+ | +| ^ | +| | 3. Scan Maps: Identify module bases | +| v and their memory regions. | +| +-----------------------------------------------------------+ | +| | Remote FD Transfer (Unix Domain Socket) | | +| |(Library FD from Injector -> Target Process via SCM_RIGHTS)| +| +-----------------------------------------------------------+ | +| ^ | +| | 4. sendmsg/recvmsg: IPC for FD passing | +| v | +| +-----------------------------------------------------------+ | +| | Remote Library Loading (android_dlopen_ext) | | +| | (Loads shared library using the transferred FD) | | +| +-----------------------------------------------------------+ | +| ^ | +| | 5. remote_call: Execute dlopen remotely | +| v | +| +-----------------------------------------------------------+ | +| | Entry Point Resolution (dlsym) | | +| +-----------------------------------------------------------+ | +| ^ | +| | 6. remote_call: Execute dlsym remotely | +| v | +| +-----------------------------------------------------------+ | +| | Entry Point Execution (remote_call) | | +| +-----------------------------------------------------------+ | +| | ++-----------------------------------------------------------------+ + | + | 7. PTRACE_DETACH: Detach from target process + V + +-----------------------------------+ + | Injector (main.cpp) | + +-----------------------------------+ + | + V + DONE +*/ + +namespace inject { + +// Namespace for constants used throughout the injection process. +namespace constants { +constexpr size_t kMagicLength = 16; +// Length of the random magic string for socket paths. + +constexpr size_t kMaxPathLength = PATH_MAX; +// Maximum length for file paths. + +constexpr const char *kSystemFileContext = "u:object_r:system_file:s0"; +// SELinux context for system files, +// used for socket creation and library file context. + +constexpr const char *kLibcModule = "libc.so"; +// Name of the C standard library. + +constexpr const char *kLibdlModule = "libdl.so"; +// Name of the dynamic linker library. +} // namespace constants + +/** + * @brief Manages a remotely loaded library handle and associated file descriptor. + * + * This class uses RAII to ensure the remote file descriptor (if transferred) is closed + * when the object goes out of scope. + * + * Note that this handle does *not* automatically `dlclose` the remotely loaded library. + * The library remains loaded in the target process. + */ +class RemoteLibraryHandle { +public: + /** + * @brief Constructs a RemoteLibraryHandle. + * @param pid The target process ID. + * @param fd The file descriptor transferred to the remote process. + * @param handle The dlopen handle returned by the remote dlopen call. + */ + RemoteLibraryHandle(int pid, int fd, uintptr_t handle = 0) : pid_(pid), fd_(fd), handle_(handle) {} + + /** + * @brief Destructor. Attempts to close the remote file descriptor. + * + * This ensures the transferred FD is closed in the remote process, preventing leaks. + * It requires reading remote registers and calling remote `close()` via ptrace. + */ + ~RemoteLibraryHandle() { + if (fd_ == -1) { + return; + } + // Only attempt to close if a valid FD exists. + + LOGD("Cleaning up remote file descriptor %d in process %d.", fd_, pid_); + + struct user_regs_struct regs{}; + // We need current registers to perform a remote call. + if (!get_regs(pid_, regs)) { + LOGW("Failed to get remote registers for FD cleanup in destructor."); + return; + } + + // Scan maps to find the remote 'close' function address. + std::vector local_map = lsplt::MapInfo::Scan(); + std::vector remote_map = lsplt::MapInfo::Scan(std::to_string(pid_)); + + if (auto close_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "close")) { + std::vector args = {static_cast(fd_)}; + // Perform a remote call to close the file descriptor. + remote_call(pid_, regs, reinterpret_cast(close_addr), libc_return_addr_, args); + } else { + LOGW("Failed to find remote 'close' function to cleanup transferred FD."); + } + } + + // Delete copy constructor and assignment operator to prevent unintended copying. + RemoteLibraryHandle(const RemoteLibraryHandle &) = delete; + RemoteLibraryHandle &operator=(const RemoteLibraryHandle &) = delete; + + /** + * @brief Move constructor. + * @param other The RemoteLibraryHandle to move from. + */ + RemoteLibraryHandle(RemoteLibraryHandle &&other) noexcept + : pid_(other.pid_), fd_(other.fd_), handle_(other.handle_) { + // Invalidate the 'other' object to prevent it from closing the FD. + other.fd_ = -1; + other.handle_ = 0; + } + + /** + * @brief Set the remote dlopen handle. + */ + void set_handle(uintptr_t handle) { + handle_ = handle; + } + + /** + * @brief Get the remote dlopen handle. + * @return The handle to the remotely loaded library. + */ + uintptr_t handle() const { + return handle_; + } + + /** + * @brief Set the return address for remote calls. + */ + void set_libc_return_addr(uintptr_t addr) { + libc_return_addr_ = addr; + } + + /** + * @brief Get the transferred file descriptor. + * @return The file descriptor in the remote process. + */ + int fd() const { + return fd_; + } + +private: + int pid_; // Target process ID. + int fd_; // File descriptor in the remote process. + uintptr_t handle_; // Handle returned by remote dlopen. + uintptr_t libc_return_addr_ = 0x0; // Return address for remote calls. +}; + +/** + * @brief Transfers a file descriptor from the injector process to the remote process. + * + * This function uses Unix domain sockets with SCM_RIGHTS to send a file descriptor. + * It involves setting SELinux contexts, creating local and remote sockets, binding, + * and then coordinating sendmsg/recvmsg calls using ptrace. + * + * @param pid The target process ID. + * @param lib_path The path to the library file being transferred. + * @param regs The current registers of the target process (will be modified). + * @param local_map Memory map of the injector process. + * @param remote_map Memory map of the target process. + * @param libc_return_addr A valid return address within libc.so for remote calls. + * @return An optional integer containing the transferred file descriptor in the + * remote process, or std::nullopt if the transfer fails. + */ +static std::optional transfer_fd_to_remote(int pid, const char *lib_path, struct user_regs_struct ®s, + const std::vector &local_map, + const std::vector &remote_map, + uintptr_t libc_return_addr) { + LOGD("Attempting to transfer file descriptor for library: %s", lib_path); + + // 1. Set SELinux context for socket creation in the injector process. + // This is crucial for Android where SELinux might prevent socket operations. + if (!set_sockcreate_con(constants::kSystemFileContext)) { + LOGE("Failed to set socket creation context."); + return std::nullopt; + } + + // 2. Create a local Unix domain socket for FD transfer. + UniqueFd local_socket = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (local_socket == -1) { + PLOGE("Failed to create local Unix domain socket."); + return std::nullopt; + } + + // 3. Set SELinux context for the library file if possible. + // This might be required for the target process to open/access it later if directly opening by path. + // For FD transfer, this is less critical as the FD's context is inherited, but good practice. + if (setfilecon(lib_path, constants::kSystemFileContext) == -1) { + // Log a warning, but don't fail, as FD transfer might still work. + PLOGE("Failed to set context of library file: %s. This might cause issues.", lib_path); + } + + // 4. Open the local library file to get a file descriptor. + UniqueFd local_lib_fd = open(lib_path, O_RDONLY | O_CLOEXEC); + if (local_lib_fd == -1) { + PLOGE("Failed to open library file: %s", lib_path); + return std::nullopt; + } + + // Struct to hold addresses of remote libc functions needed for socket operations. + struct RemoteFunctions { + void *socket_addr; + void *bind_addr; + void *recvmsg_addr; + void *close_addr; + void *errno_addr; // Address of __errno for getting remote errno. + } funcs{}; + + // 5. Resolve required libc functions in the remote process. + funcs.socket_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "socket"); + funcs.bind_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "bind"); + funcs.recvmsg_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "recvmsg"); + funcs.close_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "close"); + funcs.errno_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "__errno"); + + if (!funcs.socket_addr || !funcs.bind_addr || !funcs.recvmsg_addr || !funcs.close_addr || !funcs.errno_addr) { + LOGE("Failed to resolve all required libc functions in remote process."); + return std::nullopt; + } + + // Lambda to get the remote errno value. + auto get_remote_errno = [&]() -> int { + std::vector args; // No args for __errno. + auto addr = remote_call(pid, regs, reinterpret_cast(funcs.errno_addr), libc_return_addr, args); + int err = 0; + if (!addr || !read_proc(pid, addr, &err, sizeof(err))) { + LOGW("Failed to read remote errno value."); + return 0; + } + return err; + }; + + // Lambda to close a file descriptor in the remote process. + auto close_remote = [&](int fd) { + std::vector args = {static_cast(fd)}; + if (remote_call(pid, regs, reinterpret_cast(funcs.close_addr), libc_return_addr, args) == + static_cast(-1)) { + LOGE("Failed to close remote fd %d. Remote errno: %d", fd, get_remote_errno()); + } else { + LOGV("Successfully closed remote fd %d.", fd); + } + }; + + // 6. Create a Unix domain socket in the remote process. + 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) { + errno = get_remote_errno(); // Set local errno for PLOGE. + PLOGE("Failed to create remote socket."); + return std::nullopt; + } + LOGD("Successfully created remote socket with FD: %d", remote_fd); + + // 7. Generate a unique magic string for the abstract Unix domain socket path. + auto magic = generateMagic(constants::kMagicLength); + struct sockaddr_un sock_addr{.sun_family = AF_UNIX, .sun_path = {0}}; + // Abstract Unix domain sockets have sun_path[0] as null, and the name starts from sun_path[1]. + memcpy(sock_addr.sun_path + 1, magic.c_str(), magic.size()); + socklen_t addr_len = sizeof(sock_addr.sun_family) + 1 + magic.size(); // Length includes null byte and magic. + + // 8. Push the sockaddr_un structure to the remote process's stack. + auto remote_addr = push_memory(pid, regs, &sock_addr, sizeof(sock_addr)); + if (remote_addr == 0) { + LOGE("Failed to push socket address to remote memory."); + close_remote(remote_fd); + return std::nullopt; + } + + // 9. Bind the remote socket to the abstract Unix domain socket path. + args = {static_cast(remote_fd), remote_addr, static_cast(addr_len)}; + auto bind_result = remote_call(pid, regs, reinterpret_cast(funcs.bind_addr), libc_return_addr, args); + if (bind_result == static_cast(-1)) { + errno = get_remote_errno(); + PLOGE("Failed to bind remote socket to path: %s", magic.c_str()); + close_remote(remote_fd); + return std::nullopt; + } + LOGD("Remote socket bound to path: %s", magic.c_str()); + + // Prepare control message buffer for SCM_RIGHTS (file descriptor passing). + char cmsgbuf[CMSG_SPACE(sizeof(int))] = {0}; + + // 10. Push the control message buffer to the remote process's stack. + auto remote_cmsgbuf = push_memory(pid, regs, &cmsgbuf, sizeof(cmsgbuf)); + if (remote_cmsgbuf == 0) { + LOGE("Failed to push control message buffer to remote memory."); + close_remote(remote_fd); + return std::nullopt; + } + + // Prepare msghdr structure for recvmsg call. + struct msghdr msg_hdr{}; + msg_hdr.msg_control = reinterpret_cast(remote_cmsgbuf); + msg_hdr.msg_controllen = sizeof(cmsgbuf); + + // 11. Push the msghdr structure to the remote process's stack. + auto remote_hdr = push_memory(pid, regs, &msg_hdr, sizeof(msg_hdr)); + if (remote_hdr == 0) { + LOGE("Failed to push message header to remote memory."); + close_remote(remote_fd); + return std::nullopt; + } + + // 12. Initiate the remote recvmsg call. This will block the remote process. + args = {static_cast(remote_fd), remote_hdr, MSG_WAITALL}; + if (!remote_pre_call(pid, regs, reinterpret_cast(funcs.recvmsg_addr), 0, args)) { + LOGE("Failed to initiate remote recvmsg call."); + close_remote(remote_fd); + return std::nullopt; + } + LOGD("Remote recvmsg initiated, waiting for FD transfer..."); + + // 13. Prepare the local msghdr for sending the file descriptor. + // The msg_control and msg_name fields of the local msghdr are set up. + msg_hdr.msg_control = &cmsgbuf; // Use local cmsgbuf for sending. + msg_hdr.msg_name = &sock_addr; + msg_hdr.msg_namelen = addr_len; + + // Set up the control message to include the file descriptor. + { + auto *cmsg = CMSG_FIRSTHDR(&msg_hdr); + if (!cmsg) { + LOGE("CMSG_FIRSTHDR returned null, internal error."); + close_remote(remote_fd); + return std::nullopt; + } + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + *reinterpret_cast(CMSG_DATA(cmsg)) = local_lib_fd; // The FD to send. + } + + // 14. Send the file descriptor from the injector to the remote process. + if (sendmsg(local_socket, &msg_hdr, 0) == -1) { + PLOGE("Failed to send file descriptor to remote process."); + // We do not close local_lib_fd here as it might be transferred even if + // sendmsg errors, or could be intended for further use. The destructor of + // UniqueFd will handle it. + close_remote(remote_fd); + return std::nullopt; + } + LOGD("Local FD %d sent to remote process.", local_lib_fd.operator const int &()); + + // 15. Complete the remote recvmsg call. This will retrieve the return value. + auto recvmsg_result = + static_cast(remote_post_call(pid, regs, 0)); // No specific expected return address for recvmsg + if (recvmsg_result == -1) { + errno = get_remote_errno(); + PLOGE("Remote recvmsg call failed."); + close_remote(remote_fd); + return std::nullopt; + } + LOGD("Remote recvmsg completed with result: %zd", recvmsg_result); + + // 16. Read the control message buffer back from the remote process to extract the FD. + if (read_proc(pid, remote_cmsgbuf, &cmsgbuf, sizeof(cmsgbuf)) != sizeof(cmsgbuf)) { + LOGE("Failed to read control message buffer from remote process."); + close_remote(remote_fd); + return std::nullopt; + } + + // Parse the control message to get the transferred FD. + auto *cmsg = CMSG_FIRSTHDR(&msg_hdr); + if (!cmsg || cmsg->cmsg_len != CMSG_LEN(sizeof(int)) || cmsg->cmsg_level != SOL_SOCKET || + cmsg->cmsg_type != SCM_RIGHTS) { + LOGE("Invalid control message received from remote process. Expected " + "SCM_RIGHTS."); + close_remote(remote_fd); + return std::nullopt; + } + + int transferred_fd = *reinterpret_cast(CMSG_DATA(cmsg)); + LOGI("Successfully transferred FD %d to remote process, new remote FD: %d", local_lib_fd.operator const int &(), + transferred_fd); + + // 17. Close the remote socket. + close_remote(remote_fd); + + return transferred_fd; +} + +/** + * @brief Retrieves the error string from dlerror in the remote process. + * + * This function performs remote calls to `dlerror` and `strlen` to read + * the error message from the remote process's memory. + * + * @param pid The target process ID. + * @param regs The current registers of the target process (will be modified). + * @param local_map Memory map of the injector process. + * @param remote_map Memory map of the target process. + * @param libc_return_addr A valid return address within libc.so for remote calls. + * @return The error string from remote dlerror, or an explanatory message if retrieval fails. + */ +static std::string get_remote_dlerror(int pid, struct user_regs_struct ®s, + const std::vector &local_map, + const std::vector &remote_map, uintptr_t libc_return_addr) { + auto dlerror_addr = find_func_addr(local_map, remote_map, constants::kLibdlModule, "dlerror"); + if (!dlerror_addr) { + return "Failed to find dlerror function in remote libdl."; + } + + std::vector args; // dlerror takes no arguments. + // Call dlerror remotely to get the address of the error string. + auto dlerror_str_addr = remote_call(pid, regs, reinterpret_cast(dlerror_addr), libc_return_addr, args); + if (dlerror_str_addr == 0) { + // According to dlerror man page, it can return NULL if no error has occurred. + // For our use case (after a failed dlopen/dlsym), a null return implies a problem. + return "Remote dlerror returned null (no error message available or an issue occurred)."; + } + + // To read the string, we first need its length using remote strlen. + auto strlen_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "strlen"); + if (!strlen_addr) { + return "Failed to find strlen function in remote libc."; + } + + args.clear(); + args.push_back(dlerror_str_addr); + auto dlerror_len = remote_call(pid, regs, reinterpret_cast(strlen_addr), libc_return_addr, args); + if (dlerror_len <= 0 || dlerror_len > 1024) { // Basic sanity check for length. + return "Invalid dlerror string length received from remote strlen."; + } + + std::string err; + err.resize(dlerror_len + 1, 0); // Resize to include null terminator. + // Read the error string from the remote process. + if (read_proc(pid, dlerror_str_addr, err.data(), dlerror_len) != static_cast(dlerror_len)) { + return "Failed to read remote dlerror string from target process memory."; + } + err.resize(dlerror_len); // Trim null terminator if present. + return err; +} + +/** + * @brief Remotely calls android_dlopen_ext to load a shared library. + * + * This function handles pushing the library path and dlextinfo structure + * to the remote process's memory and then executing android_dlopen_ext. + * + * @param pid The target process ID. + * @param regs The current registers of the target process (will be modified). + * @param local_map Memory map of the injector process. + * @param remote_map Memory map of the target process. + * @param lib_fd The file descriptor of the library to load, previously transferred. + * @param lib_path The path to the library (used for debugging/error messages). + * @param libc_return_addr A valid return address within libc.so for remote calls. + * @return An optional uintptr_t containing the handle to the loaded library, or std::nullopt if loading fails. + */ +static std::optional remote_dlopen(int pid, struct user_regs_struct ®s, + const std::vector &local_map, + const std::vector &remote_map, int lib_fd, + const char *lib_path, uintptr_t libc_return_addr) { + LOGD("Attempting remote dlopen for library: %s with FD: %d", lib_path, lib_fd); + + auto dlopen_addr = find_func_addr(local_map, remote_map, constants::kLibdlModule, "android_dlopen_ext"); + if (!dlopen_addr) { + LOGE("Failed to find 'android_dlopen_ext' in remote '%s'.", constants::kLibdlModule); + // Fallback to 'dlopen' if 'android_dlopen_ext' is not found. + // This is a common pattern for broader compatibility. + dlopen_addr = find_func_addr(local_map, remote_map, constants::kLibdlModule, "dlopen"); + if (!dlopen_addr) { + LOGE("Failed to find 'dlopen' in remote '%s' either. Cannot load library.", constants::kLibdlModule); + return std::nullopt; + } + LOGW("Using 'dlopen' as 'android_dlopen_ext' was not found. FD passing might not be supported."); + // If falling back to dlopen, FD passing is not directly supported, and `dlext_info` becomes irrelevant. + // + // In this case, `lib_path` would need to be a valid path accessible to the target process. + } + + // Setup android_dlextinfo structure to pass the file descriptor. + android_dlextinfo dlext_info{}; + dlext_info.flags = ANDROID_DLEXT_USE_LIBRARY_FD; + dlext_info.library_fd = lib_fd; + + // Push the dlext_info structure and library path string to the remote stack. + uintptr_t remote_info = push_memory(pid, regs, &dlext_info, sizeof(dlext_info)); + uintptr_t remote_path = push_string(pid, regs, lib_path); + + if (remote_info == 0 || remote_path == 0) { + LOGE("Failed to push dlopen arguments to remote memory."); + return std::nullopt; + } + + // Perform the remote call to android_dlopen_ext. + // Arguments: const char* filename, int flags, const android_dlextinfo* extinfo + std::vector args = {remote_path, RTLD_NOW, remote_info}; + uintptr_t remote_handle = remote_call(pid, regs, reinterpret_cast(dlopen_addr), libc_return_addr, args); + + if (remote_handle == 0) { + std::string error_msg = get_remote_dlerror(pid, regs, local_map, remote_map, libc_return_addr); + LOGE("Remote dlopen failed for library: %s. dlerror: %s", lib_path, error_msg.c_str()); + return std::nullopt; + } + + LOGI("Successfully loaded library '%s' in remote process. Handle: %p", lib_path, + reinterpret_cast(remote_handle)); + return remote_handle; +} + +/** + * @brief Remotely calls dlsym to find the address of a symbol within a loaded + * library. + * + * @param pid The target process ID. + * @param regs The current registers of the target process (will be modified). + * @param entry_name The name of remote entry point function. + * @param local_map Memory map of the injector process. + * @param remote_map Memory map of the target process. + * @param remote_handle The handle to the remotely loaded library. + * @param libc_return_addr A valid return address within libc.so for remote calls. + * @return An optional uintptr_t containing the address of the resolved symbol, + * or std::nullopt if the symbol is not found. + */ +static std::optional remote_find_entry(int pid, struct user_regs_struct ®s, const char *entry_name, + const std::vector &local_map, + const std::vector &remote_map, + uintptr_t remote_handle, uintptr_t libc_return_addr) { + LOGD("Attempting to find remote entry symbol '%s' in library handle %p.", entry_name, + reinterpret_cast(remote_handle)); + + auto dlsym_addr = find_func_addr(local_map, remote_map, constants::kLibdlModule, "dlsym"); + if (!dlsym_addr) { + LOGE("Failed to find 'dlsym' in remote '%s'.", constants::kLibdlModule); + return std::nullopt; + } + + // Push the entry symbol name string to the remote stack. + uintptr_t remote_symbol = push_string(pid, regs, entry_name); + if (remote_symbol == 0) { + LOGE("Failed to push entry symbol name to remote memory."); + return std::nullopt; + } + + // Perform the remote call to dlsym. + // Arguments: void* handle, const char* symbol + std::vector args = {remote_handle, remote_symbol}; + uintptr_t entry_addr = remote_call(pid, regs, reinterpret_cast(dlsym_addr), libc_return_addr, args); + + if (entry_addr == 0) { + std::string error_msg = get_remote_dlerror(pid, regs, local_map, remote_map, libc_return_addr); + LOGE("Failed to find entry symbol '%s' in remote library (handle %p). dlerror: %s", entry_name, + reinterpret_cast(remote_handle), error_msg.c_str()); + return std::nullopt; + } + + LOGI("Found entry point '%s' at remote address: %p", entry_name, reinterpret_cast(entry_addr)); + return entry_addr; +} + +/** + * @brief Remotely calls the found entry point function in the injected library. + * + * The entry point is assumed to take the library handle as its single argument. + * + * @param pid The target process ID. + * @param regs The current registers of the target process (will be modified). + * @param entry_addr The remote address of the entry point function. + * @param remote_handle The handle to the remotely loaded library. + * @param libc_return_addr A valid return address within libc.so for remote calls. + * @return True if the remote call was initiated successfully, false otherwise. + */ +static bool remote_call_entry(int pid, struct user_regs_struct ®s, uintptr_t entry_addr, uintptr_t remote_handle, + uintptr_t libc_return_addr) { + LOGD("Attempting to call remote entry point at address %p with handle %p.", reinterpret_cast(entry_addr), + reinterpret_cast(remote_handle)); + + // Arguments for the entry point (typically just the library handle). + std::vector args = {remote_handle}; + uintptr_t result = remote_call(pid, regs, entry_addr, libc_return_addr, args); + + // The return value of the entry point is logged, but not necessarily checked for success. + // The interpretation of the return value depends on the injected library's contract. + LOGI("Remote entry point call completed. Return value: %p", reinterpret_cast(result)); + return true; // Return true if the call itself completed, regardless of its return value. +} + +/** + * @brief RAII wrapper for ptrace attachment and detachment. + * + * This class ensures that PTRACE_ATTACH is followed by PTRACE_DETACH, even if exceptions or early returns occur. + */ +class PtraceAttachment { +public: + /** + * @brief Constructs a PtraceAttachment and attaches to the target process. + * @param target_pid The PID of the process to attach to. + */ + explicit PtraceAttachment(int target_pid) : pid_(target_pid), attached_(false) { + LOGD("Attempting to attach to process %d...", pid_); + if (ptrace(PTRACE_ATTACH, pid_, 0, 0) == -1) { + PLOGE("Failed to attach to process %d.", pid_); + return; + } + attached_ = true; + LOGI("Successfully attached to process %d.", pid_); + } + + /** + * @brief Destructor. Detaches from the target process if currently attached. + */ + ~PtraceAttachment() { + if (attached_) { + LOGD("Attempting to detach from process %d...", pid_); + if (ptrace(PTRACE_DETACH, pid_, 0, 0) == -1) { + PLOGE("Failed to detach from process %d. Manual cleanup might be required.", pid_); + } else { + LOGI("Successfully detached from process %d.", pid_); + } + } + } + + /** + * @brief Checks if the ptrace attachment was successful. + * @return True if attached, false otherwise. + */ + bool is_attached() const { + return attached_; + } + + // Delete copy constructor and assignment operator. Ptrace attachments are unique. + PtraceAttachment(const PtraceAttachment &) = delete; + PtraceAttachment &operator=(const PtraceAttachment &) = delete; + +private: + int pid_; // The PID of the attached process. + bool attached_; // Flag indicating current attachment status. +}; + +/** + * @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. + * + * @param pid The target process ID. + * @param lib_path The absolute path to the shared library to inject. + * @param entry_name The name of the entry point function within the library. + * (Currently hardcoded to 'entry' internally but kept as param for future flexibility) + * @return True if injection was successful, false otherwise. + */ +bool inject_library(int pid, const char *lib_path, const char *entry_name) { + LOGI("Starting injection of library '%s' (entry: '%s') into process %d.", lib_path, entry_name, pid); + + // 1. Ptrace attachment using RAII. + PtraceAttachment ptrace_guard(pid); + if (!ptrace_guard.is_attached()) { + LOGE("Failed to attach to target process %d.", pid); + return false; + } + + // 2. Wait for the target process to stop after attachment. + int status; + if (!wait_for_trace(pid, &status, __WALL)) { + LOGE("Failed to wait for target process %d to stop after attachment.", pid); + return false; + } + + // Verify the stop reason is SIGSTOP (expected after PTRACE_ATTACH). + if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { + LOGE("Target process %d stopped for an unexpected reason: %s (expected SIGSTOP).", pid, + parse_status(status).c_str()); + return false; + } + LOGD("Target process %d successfully stopped by SIGSTOP.", pid); + + // 3. Backup and retrieve current registers. + // Registers are manipulated during remote calls and must be restored afterwards. + struct user_regs_struct current_regs{}, backup_regs{}; + if (!get_regs(pid, current_regs)) { + LOGE("Failed to get registers for target process %d.", pid); + return false; + } + backup_regs = current_regs; // Store a copy for restoration. + LOGD("Process %d registers backed up.", pid); + + // Create a scope to ensure RAII objects are destroyed BEFORE register restoration + { + // 4. Scan local and remote memory maps to resolve function addresses. + LOGD("Scanning memory maps for target process %d...", pid); + std::vector remote_map = lsplt::MapInfo::Scan(std::to_string(pid)); + std::vector local_map = lsplt::MapInfo::Scan(); + LOGD("Memory maps scanned."); + + // 5. Find a suitable return address within libc.so for remote calls. + // This address is used to ensure remote calls return to a safe and controlled location. + auto libc_return_addr = find_module_return_addr(remote_map, constants::kLibcModule); + if (!libc_return_addr) { + LOGE("Failed to find a suitable return address for '%s' in target process %d.", constants::kLibcModule, + pid); + return false; + } + LOGD("Found libc return address: %p", reinterpret_cast(libc_return_addr)); + + // 6. Transfer the library's file descriptor to the remote process. + 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)); + + // 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 (!handle_opt) { + 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); + + // 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)); + if (!entry_opt) { + LOGE("Failed to find entry point '%s' in remote library (handle %p).", entry_name, + reinterpret_cast(remote_lib_guard.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(), + 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; +} + +} // namespace inject + +/** + * @brief Main function for the injector tool. + * + * Parses command-line arguments, validates them, and initiates the library injection. + * + * @param argc Number of command-line arguments. + * @param argv Array of command-line argument strings. + * @return EXIT_SUCCESS on successful injection, EXIT_FAILURE otherwise. + */ +int main(int argc, char **argv) { + + // Check for correct number of arguments. + if (argc < 4) { + fprintf(stderr, "Usage: %s \n", argv[0]); + fprintf(stderr, " pid - Target process ID\n"); + fprintf(stderr, " lib_path - Absolute path to the shared library to inject\n"); + fprintf(stderr, " entry_name - Entry point symbol name (e.g., 'entry') in " + "the library\n"); + return EXIT_FAILURE; + } + + // Parse and validate PID. + char *endptr; + long pid_long = strtol(argv[1], &endptr, 10); + if (*endptr != '\0' || pid_long <= 0 || pid_long > INT_MAX) { + fprintf(stderr, "Error: Invalid PID '%s'. PID must be a positive integer.\n", argv[1]); + return EXIT_FAILURE; + } + int pid = static_cast(pid_long); + + // Resolve and validate library path. + char resolved_path[inject::constants::kMaxPathLength]; + if (realpath(argv[2], resolved_path) == nullptr) { + fprintf(stderr, "Error: Failed to resolve library path '%s': %s\n", argv[2], strerror(errno)); + return EXIT_FAILURE; + } + + if (access(resolved_path, R_OK) != 0) { + fprintf(stderr, "Error: Library file '%s' is not readable: %s\n", resolved_path, strerror(errno)); + return EXIT_FAILURE; + } + + // Validate entry name. + const char *entry_name = argv[3]; + if (strlen(entry_name) == 0) { + fprintf(stderr, "Error: Entry name cannot be empty.\n"); + return EXIT_FAILURE; + } + + LOGI("TEESimulator injector starting..."); + bool success = inject::inject_library(pid, resolved_path, entry_name); + + if (success) { + LOGI("Injection completed successfully."); + return EXIT_SUCCESS; + } else { + LOGE("Injection failed."); + return EXIT_FAILURE; + } +} diff --git a/app/src/main/cpp/inject/utils.cpp b/app/src/main/cpp/inject/utils.cpp new file mode 100644 index 0000000..3282ca8 --- /dev/null +++ b/app/src/main/cpp/inject/utils.cpp @@ -0,0 +1,1030 @@ +#include "utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "logging.hpp" + +// Anonymous namespace for file-local constants and helper functions. +namespace { +constexpr size_t kMaxPathLengthInternal = PATH_MAX; // Internal max path length. +constexpr size_t kMsgBufferSize = 64; // Buffer size for generic messages. +constexpr size_t kStatusBufferSize = 128; // Buffer size for wait status parsing. +constexpr int kInvalidFd = -1; // Represents an invalid file descriptor. +constexpr uintptr_t kStackAlignment = 0xf; // Stack alignment requirement (16 bytes for many architectures). +constexpr int kMaxRegisterArgs = 8; // Maximum number of arguments passed via registers (e.g., AArch64). + +// Permission characters used in memory map parsing. +constexpr char kReadPerm = 'r'; +constexpr char kWritePerm = 'w'; +constexpr char kExecPerm = 'x'; +constexpr char kNoPerm = '-'; + +// Characters used for generating random magic strings. +constexpr std::string_view kRandomChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +#if defined(__x86_64__) +// Helper function to set x86_64 specific registers for remote calls. +void setup_x86_64_args(struct user_regs_struct ®s, const std::vector &args) { + // Arguments are passed in RDI, RSI, RDX, RCX, R8, R9. + if (args.size() >= 1) + regs.rdi = args[0]; + if (args.size() >= 2) + regs.rsi = args[1]; + if (args.size() >= 3) + regs.rdx = args[2]; + if (args.size() >= 4) + regs.rcx = args[3]; + if (args.size() >= 5) + regs.r8 = args[4]; + if (args.size() >= 6) + regs.r9 = args[5]; +} +#elif defined(__aarch64__) +// Helper function to set AArch64 specific registers for remote calls. +void setup_aarch64_args(struct user_regs_struct ®s, const std::vector &args) { + // Arguments are passed in x0-x7. + for (size_t i = 0; i < std::min(args.size(), static_cast(kMaxRegisterArgs)); i++) { + regs.regs[i] = args[i]; + } +} +#elif defined(__arm__) +// Helper function to set ARM specific registers for remote calls. +void setup_arm_args(struct user_regs_struct ®s, const std::vector &args) { + // Arguments are passed in R0-R3. + for (size_t i = 0; i < std::min(args.size(), static_cast(4)); i++) { // ARM has 4 register arguments (R0-R3) + regs.uregs[i] = args[i]; + } +} +#endif +} // namespace + +/** + * @brief Switches the mount namespace of the current process to that of the target PID, or restores it. + * + * This is crucial for operations like `open()` on `/proc//mem` or other sensitive files, + * which might be in a different mount namespace than the injector. + * + * @param pid If non-zero, switches to the namespace of `pid`. + * If zero, restores to the namespace stored in `*fd`. + * @param fd On entry (pid != 0), points to an int to store the original namespace FD. + * On entry (pid == 0), points to the FD of the namespace to restore to. + * FD is consumed/set to kInvalidFd on successful restore. + * @return True on success, false on failure. + */ +bool switch_mnt_ns(int pid, int *fd) { + if (pid == 0) { // Restore original namespace + if (!fd || *fd == kInvalidFd) { + LOGE("Invalid file descriptor for namespace switch (restore operation)."); + return false; + } + + UniqueFd nsfd(*fd); // Take ownership of the FD. + *fd = kInvalidFd; // Invalidate original pointer. + + if (setns(nsfd, CLONE_NEWNS) == -1) { + PLOGE("Failed to switch back to original namespace (FD: %d).", nsfd.operator const int &()); + return false; + } + + LOGD("Successfully switched back to original namespace (FD: %d).", nsfd.operator const int &()); + return true; + } else { // Switch to target PID's namespace + int old_nsfd = kInvalidFd; + + if (fd) { // If an FD pointer is provided, save current namespace FD. + old_nsfd = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC); + if (old_nsfd == kInvalidFd) { + PLOGE("Failed to open current mount namespace for backup."); + return false; + } + *fd = old_nsfd; // Store the original namespace FD. + } + + std::string target_path = "/proc/" + std::to_string(pid) + "/ns/mnt"; + UniqueFd target_nsfd = open(target_path.c_str(), O_RDONLY | O_CLOEXEC); + if (target_nsfd == kInvalidFd) { + PLOGE("Failed to open target PID %d's mount namespace: %s", pid, target_path.c_str()); + if (fd) + *fd = kInvalidFd; // Invalidate backup FD if target fails. + return false; + } + + if (setns(target_nsfd, CLONE_NEWNS) == -1) { + PLOGE("Failed to switch to target PID %d's mount namespace: %s", pid, target_path.c_str()); + if (fd) + *fd = kInvalidFd; // Invalidate backup FD if target fails. + return false; + } + + LOGD("Successfully switched to mount namespace for PID %d.", pid); + return true; + } +} + +/** + * @brief Writes data to the remote process's memory using either `process_vm_writev` or `/proc//mem`. + * + * `process_vm_writev` is generally preferred for performance and atomicity but requires kernel support. + * `/proc//mem` is a fallback or for specific use cases. + * + * @param pid The target process ID. + * @param remote_addr The target address in the remote process. + * @param buf A pointer to the local buffer containing data to write. + * @param len The number of bytes to write. + * @param use_proc_mem If true, uses /proc//mem; otherwise, uses process_vm_writev. + * @return The number of bytes written, or -1 on error. + */ +ssize_t write_proc(int pid, uintptr_t remote_addr, const void *buf, size_t len, bool use_proc_mem) { + if (!buf || len == 0) { + LOGE("Invalid parameters for write_proc: buffer is null or length is zero."); + return -1; + } + + LOGV("Writing %zu bytes to PID %d at address %" PRIxPTR " (use_proc_mem=%s).", len, pid, remote_addr, + use_proc_mem ? "true" : "false"); + + ssize_t bytes_written; + + if (use_proc_mem) { + // Fallback or specific use case: writing via /proc//mem. + // This requires opening the mem file and using pwrite to specify the offset. + char proc_path[kMaxPathLengthInternal]; + snprintf(proc_path, sizeof(proc_path), "/proc/%d/mem", pid); + + UniqueFd proc_fd = open(proc_path, O_WRONLY | O_CLOEXEC); + if (proc_fd == kInvalidFd) { + PLOGE("Failed to open %s for writing.", proc_path); + return -1; + } + + bytes_written = pwrite(proc_fd, buf, len, static_cast(remote_addr)); + if (bytes_written == -1) { + PLOGE("pwrite failed for remote address %" PRIxPTR ".", remote_addr); + } + } else { + // Preferred method: process_vm_writev for direct memory access. + // It transfers data between the vector of iovecs from local process to remote process. + struct iovec local_iov = {.iov_base = const_cast(buf), .iov_len = len}; + struct iovec remote_iov = {.iov_base = reinterpret_cast(remote_addr), .iov_len = len}; + + bytes_written = process_vm_writev(pid, &local_iov, 1, &remote_iov, 1, 0); + if (bytes_written == -1) { + PLOGE("process_vm_writev failed for remote address %" PRIxPTR ".", remote_addr); + } + } + + if (bytes_written != -1 && static_cast(bytes_written) != len) { + LOGW("Partial write: %zd bytes written, %zu expected for PID %d at %" PRIxPTR ".", bytes_written, len, pid, + remote_addr); + } + + return bytes_written; +} + +/** + * @brief Reads data from the remote process's memory using `process_vm_readv`. + * + * `process_vm_readv` is generally the most efficient and robust way to read from another process's memory. + * + * @param pid The target process ID. + * @param remote_addr The source address in the remote process. + * @param buf A pointer to the local buffer to store the read data. + * @param len The number of bytes to read. + * @return The number of bytes read, or -1 on error. + */ +ssize_t read_proc(int pid, uintptr_t remote_addr, void *buf, size_t len) { + if (!buf || len == 0) { + LOGE("Invalid parameters for read_proc: buffer is null or length is zero."); + return -1; + } + + LOGV("Reading %zu bytes from PID %d at address %" PRIxPTR ".", len, pid, remote_addr); + + // Setup iovec structures for local and remote memory. + struct iovec local_iov = {.iov_base = buf, .iov_len = len}; + struct iovec remote_iov = {.iov_base = reinterpret_cast(remote_addr), .iov_len = len}; + + ssize_t bytes_read = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0); + if (bytes_read == -1) { + PLOGE("process_vm_readv failed for remote address %" PRIxPTR ".", remote_addr); + } else if (static_cast(bytes_read) != len) { + LOGW("Partial read: %zd bytes read, %zu expected for PID %d at %" PRIxPTR ".", bytes_read, len, pid, + remote_addr); + } + + return bytes_read; +} + +/** + * @brief Retrieves the current CPU registers of the target process using ptrace. + * + * This function handles architecture-specific differences in `ptrace` calls for registers. + * + * @param pid The target process ID. + * @param regs A reference to a `user_regs_struct` to store the registers. + * @return True on success, false on failure. + */ +bool get_regs(int pid, struct user_regs_struct ®s) { + LOGV("Retrieving registers for PID %d.", pid); + +#if defined(__x86_64__) || defined(__i386__) + // For x86/x86_64, PTRACE_GETREGS is used directly with `struct + // user_regs_struct`. + if (ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1) { + PLOGE("Failed to get registers for PID %d.", pid); + return false; + } +#elif defined(__aarch64__) || defined(__arm__) + // For ARM/AArch64, PTRACE_GETREGSET is used with `NT_PRSTATUS` and an iovec. + struct iovec reg_iov = {.iov_base = ®s, .iov_len = sizeof(struct user_regs_struct)}; + if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, ®_iov) == -1) { + PLOGE("Failed to get register set for PID %d.", pid); + return false; + } +#else +# error "Unsupported architecture for register access in get_regs." +#endif + + LOGV("Successfully retrieved registers for PID %d.", pid); + return true; +} + +/** + * @brief Sets the CPU registers of the target process using ptrace. + * + * This function handles architecture-specific differences in `ptrace` calls for registers. + * + * @param pid The target process ID. + * @param regs A reference to a `user_regs_struct` containing the registers to set. + * @return True on success, false on failure. + */ +bool set_regs(int pid, struct user_regs_struct ®s) { + LOGV("Setting registers for PID %d.", pid); + +#if defined(__x86_64__) || defined(__i386__) + // For x86/x86_64, PTRACE_SETREGS is used directly. + if (ptrace(PTRACE_SETREGS, pid, 0, ®s) == -1) { + PLOGE("Failed to set registers for PID %d.", pid); + return false; + } +#elif defined(__aarch64__) || defined(__arm__) + // For ARM/AArch64, PTRACE_SETREGSET is used. + struct iovec reg_iov = {.iov_base = ®s, .iov_len = sizeof(struct user_regs_struct)}; + if (ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, ®_iov) == -1) { + PLOGE("Failed to set register set for PID %d.", pid); + return false; + } +#else +# error "Unsupported architecture for register access in set_regs." +#endif + + LOGV("Successfully set registers for PID %d.", pid); + return true; +} + +/** + * @brief Gets a descriptive string of the memory region containing a given address. + * @param map_info A vector of `lsplt::MapInfo` for the process. + * @param addr The address to look up. + * @return A string representing the memory region (e.g., "path perms"), or + * "". + */ +std::string get_addr_mem_region(const std::vector &map_info, uintptr_t addr) { + for (const auto &map : map_info) { + if (map.start <= addr && map.end > addr) { + std::string perms_str; + perms_str.reserve(4); // "rwx" + null or '-' + + perms_str += (map.perms & PROT_READ) ? kReadPerm : kNoPerm; + perms_str += (map.perms & PROT_WRITE) ? kWritePerm : kNoPerm; + perms_str += (map.perms & PROT_EXEC) ? kExecPerm : kNoPerm; + + return map.path + ' ' + perms_str; + } + } + return ""; +} + +/** + * @brief Finds the base address of a module in a process's memory map. + * + * This function iterates through memory maps and identifies the entry + * that corresponds to the start of a shared library (offset 0) with a matching suffix. + * + * @param map_info A vector of `lsplt::MapInfo` for the process. + * @param module_suffix The suffix of the module path (e.g., "libc.so"). + * @return The base address of the module, or nullptr if not found. + */ +void *find_module_base(const std::vector &map_info, std::string_view module_suffix) { + for (const auto &map : map_info) { + // A module's base is typically its first segment with offset 0. + if (map.offset == 0 && map.path.ends_with(module_suffix)) { + LOGV("Found module base for '%.*s' at %p.", static_cast(module_suffix.length()), module_suffix.data(), + reinterpret_cast(map.start)); + return reinterpret_cast(map.start); + } + } + + LOGV("Module base not found for suffix '%.*s'.", static_cast(module_suffix.length()), module_suffix.data()); + return nullptr; +} + +/** + * @brief Finds the address of a function in a remote process by resolving it locally and calculating the offset. + * + * This is a common technique for remote code injection: + * 1. Load the target module locally (e.g., `libc.so`). + * 2. Find the function's address in the *local* module. + * 3. Calculate the offset of the function from the *local* module's base. + * 4. Find the module's base address in the *remote* process. + * 5. Add the calculated offset to the *remote* module's base to get the remote function address. + * + * @param local_map_info Memory map of the local (injector) process. + * @param remote_map_info Memory map of the remote (target) process. + * @param module_name The name of the module (e.g., "libc.so"). + * @param function_name The name of the function (e.g., "open"). + * @return The remote address of the function, or nullptr if not found. + */ +void *find_func_addr(const std::vector &local_map_info, + const std::vector &remote_map_info, std::string_view module_name, + std::string_view function_name) { + LOGV("Resolving function '%.*s' in module '%.*s'.", static_cast(function_name.length()), function_name.data(), + static_cast(module_name.length()), module_name.data()); + + // 1. Open the module locally to find the symbol. + // RTLD_NOW ensures all undefined symbols are resolved immediately. + void *lib_handle = dlopen(module_name.data(), RTLD_NOW); + if (!lib_handle) { + LOGE("Failed to open local library '%.*s': %s.", static_cast(module_name.length()), module_name.data(), + dlerror()); + return nullptr; + } + + // Use a lambda for RAII-like dlclose to ensure handle is closed. + auto lib_closer = [lib_handle]() { + dlclose(lib_handle); + }; + + // 2. Find the function's address in the *local* module. + auto *symbol_addr = reinterpret_cast(dlsym(lib_handle, function_name.data())); + if (!symbol_addr) { + LOGE("Failed to find local symbol '%.*s' in library '%.*s': %s.", static_cast(function_name.length()), + function_name.data(), static_cast(module_name.length()), module_name.data(), dlerror()); + lib_closer(); // Ensure local handle is closed before returning. + return nullptr; + } + LOGV("Found local symbol '%.*s' at address %p.", static_cast(function_name.length()), function_name.data(), + symbol_addr); + + // 3. Find the module's base address in the *local* process. + auto *local_base = reinterpret_cast(find_module_base(local_map_info, module_name)); + if (!local_base) { + LOGE("Failed to find local base address for module '%.*s'.", static_cast(module_name.length()), + module_name.data()); + lib_closer(); + return nullptr; + } + + // 4. Find the module's base address in the *remote* process. + auto *remote_base = reinterpret_cast(find_module_base(remote_map_info, module_name)); + if (!remote_base) { + LOGE("Failed to find remote base address for module '%.*s'.", static_cast(module_name.length()), + module_name.data()); + lib_closer(); + return nullptr; + } + + // 5. Calculate the offset and derive the remote function address. + ptrdiff_t symbol_offset = symbol_addr - local_base; + auto *remote_symbol_addr = remote_base + symbol_offset; + + LOGV("Address translation: local_base=%p, remote_base=%p, offset=%td -> remote_addr=%p", local_base, remote_base, + symbol_offset, remote_symbol_addr); + + lib_closer(); // Close local handle. + return remote_symbol_addr; +} + +/** + * @brief Aligns the stack pointer (`REG_SP`) to ensure proper stack frame setup. + * + * Stack alignment (typically 16-bytes on modern architectures) is crucial for correct function calls, + * especially for variadic functions or those using SIMD registers. + * + * @param regs A reference to the `user_regs_struct` to modify. + * @param preserve_bytes Number of bytes to preserve below the new stack pointer. + * This is useful if some data needs to be kept on the stack just before the new alignment. + */ +void align_stack(struct user_regs_struct ®s, uintptr_t preserve_bytes) { + // Decrement stack pointer by preserve_bytes, then align it down to the nearest multiple of (kStackAlignment + 1). + regs.REG_SP = (regs.REG_SP - preserve_bytes) & ~kStackAlignment; + LOGV("Stack aligned to %" PRIxPTR " (preserved %zu bytes).", static_cast(regs.REG_SP), preserve_bytes); +} + +/** + * @brief Pushes a block of memory onto the remote process's stack. + * + * This function decrements the stack pointer, aligns it + * (if necessary, by a subsequent call to align_stack after multiple pushes), and then writes the data. + * + * @param pid The target process ID. + * @param regs A reference to the `user_regs_struct` (its stack pointer will be updated). + * @param data A pointer to the local data to push. + * @param length The number of bytes to push. + * @return The remote address where the data was pushed, or 0 on error. + */ +uintptr_t push_memory(int pid, struct user_regs_struct ®s, const void *data, size_t length) { + if (!data || length == 0) { + LOGE("Invalid parameters for push_memory: data=%p, length=%zu.", data, length); + return 0; + } + + // Decrement stack pointer to make space for the data. + regs.REG_SP -= length; + // Align the stack. + // This might shift REG_SP further down if not already aligned. + // If multiple small pushes happen, it's better to align once at the end or before a call. + // For a single block, aligning after decrement is fine. + align_stack(regs); + + auto stack_addr = static_cast(regs.REG_SP); + + // Write the data to the remote stack. + if (write_proc(pid, stack_addr, data, length) != static_cast(length)) { + LOGE("Failed to push %zu bytes to remote stack at %" PRIxPTR ".", length, stack_addr); + return 0; + } + + LOGV("Pushed %zu bytes to remote stack at %" PRIxPTR ".", length, stack_addr); + return stack_addr; +} + +/** + * @brief Pushes a null-terminated string onto the remote process's stack. + * + * This function calculates the string length (including null terminator), + * decrements the stack pointer, aligns it, and then writes the string. + * + * @param pid The target process ID. + * @param regs A reference to the `user_regs_struct` (its stack pointer will be updated). + * @param str The null-terminated C-style string to push. + * @return The remote address where the string was pushed, or 0 on error. + */ +uintptr_t push_string(int pid, struct user_regs_struct ®s, const char *str) { + if (!str) { + LOGE("Null string pointer passed to push_string."); + return 0; + } + + size_t str_length = strlen(str) + 1; // Include null terminator. + + // Decrement stack pointer and align it. + regs.REG_SP -= str_length; + align_stack(regs); // Align the stack after making space. + + auto stack_addr = static_cast(regs.REG_SP); + + // Write the string to the remote stack. + if (write_proc(pid, stack_addr, str, str_length) != static_cast(str_length)) { + LOGE("Failed to push string '%s' (%zu bytes) to remote stack at %" PRIxPTR ".", str, str_length, stack_addr); + return 0; + } + + LOGV("Pushed string '%s' (%zu bytes) to remote stack at %" PRIxPTR ".", str, str_length, stack_addr); + return stack_addr; +} + +/** + * @brief Prepares and initiates a remote function call in the target process. + * + * This function sets up the target process's registers according to the calling convention of the architecture: + * + * - Arguments are placed in registers or on the stack. + * - The return address is pushed onto the stack (x86) or placed in a link register (ARM/AArch64). + * - The instruction pointer is set to the target function's address. + * + * Finally, `PTRACE_CONT` is used to resume the target process. + * + * @param pid The target process ID. + * @param regs A reference to the `user_regs_struct` (will be modified with call context). + * @param func_addr The remote address of the function to call. + * @param return_addr The address in the remote process where execution should resume after the call. + * @param args A vector of `uintptr_t` representing the function arguments. + * @return True if the remote call was successfully initiated, false otherwise. + */ +bool remote_pre_call(int pid, struct user_regs_struct ®s, uintptr_t func_addr, uintptr_t return_addr, + std::vector &args) { + // Ensure stack is aligned before modifying it for function arguments. + align_stack(regs); + + LOGV("Setting up remote function call to %p (func_addr=%" PRIxPTR ") with %zu arguments. Return to %p.", + reinterpret_cast(func_addr), func_addr, args.size(), reinterpret_cast(return_addr)); + for (size_t i = 0; i < args.size(); i++) { + LOGV(" arg[%zu] = %p (%" PRIuPTR ")", i, reinterpret_cast(args[i]), args[i]); + } + +#if defined(__x86_64__) + // x86_64 Calling Convention (System V AMD64 ABI): + // Arguments: RDI, RSI, RDX, RCX, R8, R9. + // Additional arguments on stack (right-to-left). + // Return Address: Pushed onto stack by CALL instruction. + // Return Value: RAX. + setup_x86_64_args(regs, args); + + if (args.size() > kMaxRegisterArgs) { // kMaxRegisterArgs for x86_64 is 6 + // Push excess arguments onto the stack. + size_t stack_args_size = (args.size() - kMaxRegisterArgs) * sizeof(uintptr_t); + align_stack(regs, stack_args_size); + // Align stack while reserving space for args. + + // Write stack arguments from `args.data() + kMaxRegisterArgs` (the elements beyond registers). + if (write_proc(pid, static_cast(regs.REG_SP), args.data() + kMaxRegisterArgs, stack_args_size) != + static_cast(stack_args_size)) { + LOGE("Failed to push stack arguments for x86_64 remote call."); + return false; + } + } + + // Push the return address onto the stack. This simulates what a `call` instruction would do. + regs.REG_SP -= sizeof(uintptr_t); + if (write_proc(pid, static_cast(regs.REG_SP), &return_addr, sizeof(return_addr)) != + sizeof(return_addr)) { + LOGE("Failed to write return address for x86_64 remote call."); + return false; + } + + regs.REG_IP = func_addr; // Set instruction pointer to the target function. + +#elif defined(__i386__) + // i386 Calling Convention (cdecl): + // Arguments: All pushed onto stack (right-to-left). + // Return Address: Pushed onto stack by CALL instruction. + // Return Value: EAX. + if (args.size() > 0) { + size_t stack_args_size = args.size() * sizeof(uintptr_t); + align_stack(regs, stack_args_size); + + // Push all arguments onto the stack (order is important if ABI is right-to-left push). + // The current implementation writes args.data() directly, + // assuming it's already in the correct order for push. + // For cdecl, arguments are pushed right-to-left. + // A vector `args = {A, B, C}` means A is arg1, B is arg2 etc. + // So, `C` should be pushed first, then `B`, then `A`. + // `write_proc` copies linearly. + // This implies `args` should be pre-reversed for cdecl. + // For simplicity, we assume the remote function is compatible with how it's pushed, + // or that it's variadic where order doesn't matter for first args. + // A robust i386 implementation would need to push args in reverse order. + if (write_proc(pid, static_cast(regs.REG_SP), args.data(), stack_args_size) != + static_cast(stack_args_size)) { + LOGE("Failed to push arguments for i386 remote call."); + return false; + } + } + + // Push the return address onto the stack. + regs.REG_SP -= sizeof(uintptr_t); + if (write_proc(pid, static_cast(regs.REG_SP), &return_addr, sizeof(return_addr)) != + sizeof(return_addr)) { + LOGE("Failed to write return address for i386 remote call."); + return false; + } + + regs.REG_IP = func_addr; // Set instruction pointer. + +#elif defined(__aarch64__) + // AArch64 Calling Convention (Procedure Call Standard for the ARM 64-bit Architecture): + // Arguments: x0-x7. + // Additional arguments on stack. + // Return Address: x30 (Link Register, LR). + // Return Value: x0. + setup_aarch64_args(regs, args); + + if (args.size() > kMaxRegisterArgs) { // kMaxRegisterArgs for AArch64 is 8 + size_t stack_args_size = (args.size() - kMaxRegisterArgs) * sizeof(uintptr_t); + align_stack(regs, stack_args_size); + + if (write_proc(pid, static_cast(regs.REG_SP), args.data() + kMaxRegisterArgs, stack_args_size) != + static_cast(stack_args_size)) { + LOGE("Failed to push stack arguments for AArch64 remote call."); + return false; + } + } + + regs.regs[30] = return_addr; // Set Link Register (LR) to return address. + regs.REG_IP = func_addr; // Set Program Counter (PC) to target function. + +#elif defined(__arm__) + // ARM Calling Convention (ARM Procedure Call Standard - AAPCS): + // Arguments: R0-R3. Additional arguments on stack. + // Return Address: R14 (Link Register, LR). + // Return Value: R0. + setup_arm_args(regs, args); + + if (args.size() > 4) { // ARM has 4 register arguments (R0-R3). + size_t stack_args_size = (args.size() - 4) * sizeof(uintptr_t); + align_stack(regs, stack_args_size); + + if (write_proc(pid, static_cast(regs.REG_SP), args.data() + 4, stack_args_size) != + static_cast(stack_args_size)) { + LOGE("Failed to push stack arguments for ARM remote call."); + return false; + } + } + + regs.uregs[14] = return_addr; // Set Link Register (R14) to return address. + regs.REG_IP = func_addr; // Set Program Counter (R15) to target function. + + // Handle Thumb mode for ARM. + // If func_addr is odd, it indicates Thumb instruction set. + // Clear the LSB of PC and set the T bit in CPSR (R16). + constexpr auto CPSR_T_MASK = 1lu << 5; + if ((regs.REG_IP & 1) != 0) { + regs.REG_IP = regs.REG_IP & ~1; // Clear LSB for actual address. + regs.uregs[16] = regs.uregs[16] | CPSR_T_MASK; // Set T bit. + } else { + regs.uregs[16] = regs.uregs[16] & ~CPSR_T_MASK; // Clear T bit for ARM mode. + } + +#else +# error "Unsupported architecture for remote function calls in remote_pre_call." +#endif + + // Set the modified registers in the target process. + if (!set_regs(pid, regs)) { + LOGE("Failed to set registers for remote function call in PID %d.", pid); + return false; + } + + // Continue the target process execution. + if (ptrace(PTRACE_CONT, pid, 0, 0) == -1) { + PLOGE("Failed to continue remote process %d execution.", pid); + return false; + } + + LOGV("Remote function call initiated successfully for PID %d to %p.", pid, reinterpret_cast(func_addr)); + return true; +} + +/** + * @brief Waits for and finalizes a remote function call, retrieving its return value. + * + * After `remote_pre_call` resumes the process, this function waits for the process to stop + * (ideally at the `return_addr` previously set). + * It then retrieves the registers to extract the function's return value. + * + * @param pid The target process ID. + * @param regs A reference to the `user_regs_struct` (will be updated with post-call registers). + * @param expected_return_addr The address where the remote call was expected to return to. + * Used for error checking (e.g., if a crash occurs elsewhere). + * @return The return value of the remote function (from REG_RET), or 0 on error. + */ +uintptr_t remote_post_call(int pid, struct user_regs_struct ®s, uintptr_t expected_return_addr) { + LOGV("Waiting for remote function call completion in PID %d.", pid); + + int status; + // Wait for the target process to stop. + if (!wait_for_trace(pid, &status, __WALL)) { + LOGE("Failed to wait for remote function completion in PID %d.", pid); + return 0; + } + + // Retrieve the registers after the call. + if (!get_regs(pid, regs)) { + LOGE("Failed to get registers after remote call completion in PID %d.", pid); + return 0; + } + + int stop_signal = WSTOPSIG(status); + LOGV("Remote function in PID %d stopped with signal: %s(%d) at address %p.", pid, sigabbrev_np(stop_signal), + stop_signal, reinterpret_cast(regs.REG_IP)); + + // Check if the process stopped at the expected return address. + // SIGTRAP is often received if a breakpoint was set at return_addr, or if single-stepping. + // A SIGSEGV here indicates a crash during the remote function execution. + if (static_cast(regs.REG_IP) != expected_return_addr) { + // Log unexpected return, potentially indicating a crash or unexpected flow. + LOGE("Remote function in PID %d returned to unexpected address %p (expected %p).", pid, + reinterpret_cast(regs.REG_IP), reinterpret_cast(expected_return_addr)); + + // Attempt to get more detailed crash info if it was a SIGSEGV or similar. + if (stop_signal == SIGSEGV || stop_signal == SIGBUS || stop_signal == SIGILL) { + siginfo_t crash_info; + if (ptrace(PTRACE_GETSIGINFO, pid, 0, &crash_info) == 0) { + LOGE("Crash details for PID %d: si_code=%d si_addr=%p.", pid, crash_info.si_code, crash_info.si_addr); + } else { + PLOGE("Failed to get crash signal info for PID %d.", pid); + } + } + return 0; // Indicate failure. + } + + uintptr_t return_value = regs.REG_RET; // Extract the return value from the appropriate register. + LOGV("Remote function in PID %d completed with return value: %p (%" PRIxPTR ").", pid, + reinterpret_cast(return_value), return_value); + return return_value; +} + +/** + * @brief Executes a complete remote function call (pre-call, continue, post-call). + * + * This is a convenience wrapper combining `remote_pre_call` and `remote_post_call`. + * + * @param pid The target process ID. + * @param regs A reference to the `user_regs_struct` (will be modified). + * @param func_addr The remote address of the function to call. + * @param return_addr The address in the remote process where execution should + * resume after the call. + * @param args A vector of `uintptr_t` representing the function arguments. + * @return The return value of the remote function, or 0 on error. + */ +uintptr_t remote_call(int pid, struct user_regs_struct ®s, uintptr_t func_addr, uintptr_t return_addr, + std::vector &args) { + if (!remote_pre_call(pid, regs, func_addr, return_addr, args)) { + LOGE("Failed to prepare remote function call in PID %d.", pid); + return 0; + } + return remote_post_call(pid, regs, return_addr); +} + +/** + * @brief Forks twice to create a daemon process. + * + * The first `fork` creates a child. + * The parent waits for this child to exit and then returns its PID. + * The child then `forks` again. + * The second child becomes the daemon, and the first child exits, + * ensuring the daemon is not a session leader and is re-parented to `init`. + * + * @return 0 in the grand-child (daemon), PID of first child in parent, or -1 on error. + */ +int fork_dont_care() { + // First fork: Parent returns, child continues to fork again. + int first_pid = fork(); + if (first_pid < 0) { + PLOGE("Failed first fork for daemon process."); + return first_pid; + } + + if (first_pid == 0) { // This is the first child process. + // Second fork: First child exits, grand-child becomes daemon. + int second_pid = fork(); + if (second_pid < 0) { + PLOGE("Failed second fork for daemon process."); + exit(EXIT_FAILURE); // Grand-child creation failed, exit first child. + } else if (second_pid > 0) { + exit(EXIT_SUCCESS); // First child exits. + } + // This is the grand-child process, now a daemon. + return 0; + } else { // This is the original parent process. + int status; + // Wait for the first child to terminate (it will exit after the second + // fork). + waitpid(first_pid, &status, __WALL); + return first_pid; // Return PID of the first child. + } +} + +/** + * @brief Waits for the target process to stop due to ptrace. + * + * This function continuously calls `waitpid` until the process stops. + * It handles `EINTR` (interrupted system call) by retrying. + * + * @param pid The target process ID. + * @param status A pointer to an integer to store the wait status. + * @param flags Flags for `waitpid` (e.g., `__WALL`). + * @return True if the process successfully stopped, false otherwise. + */ +bool wait_for_trace(int pid, int *status, int flags) { + if (!status) { + LOGE("Null status pointer passed to wait_for_trace."); + return false; + } + + while (true) { + pid_t result = waitpid(pid, status, flags); + if (result == -1) { + if (errno == EINTR) { + LOGV("waitpid for PID %d interrupted, retrying.", pid); + continue; // Retry on EINTR. + } else { + PLOGE("waitpid failed for PID %d.", pid); + return false; + } + } + + // If waitpid returns a valid PID, check if the process actually stopped. + if (!WIFSTOPPED(*status)) { + LOGE("Process %d not stopped for trace: %s.", pid, parse_status(*status).c_str()); + return false; + } + + LOGV("Process %d stopped for trace with status: %s.", pid, parse_status(*status).c_str()); + return true; + } +} + +/** + * @brief Parses the wait status integer into a human-readable string. + * @param status The status integer returned by `waitpid`. + * @return A string describing the wait status (e.g., "exited with code 0", "stopped by signal SIGSTOP"). + */ +std::string parse_status(int status) { + char status_buf[kStatusBufferSize]; + + if (WIFEXITED(status)) { + // Process exited normally. + snprintf(status_buf, sizeof(status_buf), "0x%x exited with code %d", status, WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + // Process terminated by a signal. + snprintf(status_buf, sizeof(status_buf), "0x%x terminated by signal %s(%d)", status, + sigabbrev_np(WTERMSIG(status)), WTERMSIG(status)); + } else if (WIFSTOPPED(status)) { + // Process stopped by a signal (e.g., ptrace, job control). + int stop_signal = WSTOPSIG(status); + snprintf(status_buf, sizeof(status_buf), "0x%x stopped by signal=%s(%d), event=%s", status, + sigabbrev_np(stop_signal), stop_signal, parse_ptrace_event(status)); + } else { + // Unknown status. + snprintf(status_buf, sizeof(status_buf), "0x%x unknown status", status); + } + + return std::string(status_buf); +} + +/** + * @brief Retrieves the executable path of a process by reading its `/proc//exe` symlink. + * @param pid The target process ID. + * @return The absolute path to the executable, or an empty string on error. + */ +std::string get_program(int pid) { + std::string exe_path = "/proc/" + std::to_string(pid) + "/exe"; + char resolved_path[kMaxPathLengthInternal + 1]; // +1 for null terminator. + + ssize_t link_size = readlink(exe_path.c_str(), resolved_path, kMaxPathLengthInternal); + if (link_size == -1) { + PLOGE("Failed to read executable path for PID %d: %s", pid, exe_path.c_str()); + return ""; + } + + resolved_path[link_size] = '\0'; // Null-terminate the string. + return std::string(resolved_path); +} + +/** + * @brief Finds a suitable return address within a specific module in the remote process. + * + * For remote code injection, after a remote function call completes, + * the instruction pointer needs to be set to a safe and controlled location. + * A non-executable segment within a common library like `libc.so` is often chosen + * because it's guaranteed to exist and typically won't trigger unwanted execution. + * + * @param map_info A vector of `lsplt::MapInfo` for the remote process. + * @param module_suffix The suffix of the module path (e.g., "libc.so"). + * @return A pointer to a suitable return address, or nullptr if not found. + */ +void *find_module_return_addr(const std::vector &map_info, std::string_view module_suffix) { + for (const auto &map : map_info) { + // Look for a readable, non-executable segment of the module. + // This is a common heuristic for finding a safe return address for ptrace. + if (!(map.perms & PROT_EXEC) && (map.perms & PROT_READ) && map.path.ends_with(module_suffix)) { + LOGV("Found return address region for '%.*s' at %p.", static_cast(module_suffix.length()), + module_suffix.data(), reinterpret_cast(map.start)); + return reinterpret_cast(map.start); + } + } + + LOGV("No suitable return address region found for module suffix '%.*s'.", static_cast(module_suffix.length()), + module_suffix.data()); + return nullptr; +} + +/** + * @brief Generates a random alphanumeric string of a specified length. + * + * Uses `std::random_device` for seeding and `std::mt19937` for pseudo-random number generation. + * + * @param length The desired length of the magic string. + * @return The generated magic string. + */ +std::string generateMagic(size_t length) { + if (length == 0) { + LOGW("Zero length requested for magic string, returning empty string."); + return ""; + } + + // Seed the random number generator using a hardware-entropy source if available. + std::mt19937 random_generator{std::random_device{}()}; + // Distribution to pick a random character from kRandomChars. + std::uniform_int_distribution char_distribution(0, kRandomChars.length() - 1); + + std::string magic_string; + magic_string.reserve(length); // Reserve memory to avoid reallocations. + + for (size_t i = 0; i < length; i++) { + magic_string += kRandomChars[char_distribution(random_generator)]; + } + + LOGV("Generated magic string of length %zu.", length); + return magic_string; +} + +/** + * @brief Sets the SELinux security context of a file using the `setxattr` syscall. + * + * This is relevant for Android systems where SELinux policies often + * restrict processes from accessing files with certain contexts. + * + * @param file_path The path to the file. + * @param security_context The new security context string (e.g., "u:object_r:system_file:s0"). + * @return 0 on success, -1 on failure. + */ +int setfilecon(const char *file_path, const char *security_context) { + if (!file_path || !security_context) { + LOGE("Invalid parameters for setfilecon: file_path=%p, security_context=%p.", file_path, security_context); + return -1; + } + + size_t context_len = strlen(security_context) + 1; // Include null terminator. + // Call the setxattr syscall directly. + // `XATTR_NAME_SELINUX` specifies the SELinux extended attribute. + int result = syscall(__NR_setxattr, file_path, XATTR_NAME_SELINUX, security_context, context_len, 0); + + if (result == 0) { + LOGV("Successfully set SELinux context '%s' for file '%s'.", security_context, file_path); + } else { + PLOGE("Failed to set SELinux context '%s' for file '%s'.", security_context, file_path); + } + + return result; +} + +/** + * @brief Sets the SELinux context for newly created sockets in the current thread/process. + * + * This function attempts to write the security context to `/proc/thread-self/attr/sockcreate`. + * If that fails (e.g., permission issues, or old kernel), it falls back to a process-specific path. + * + * @param security_context The SELinux context string to set. + * @return True on success, false on failure. + */ +bool set_sockcreate_con(const char *security_context) { + if (!security_context) { + LOGE("Null security context passed to set_sockcreate_con."); + return false; + } + + size_t context_size = strlen(security_context) + 1; // Include null terminator. + + // Try setting via `/proc/thread-self/attr/sockcreate`, which is the most specific. + UniqueFd sockcreate_fd = open("/proc/thread-self/attr/sockcreate", O_WRONLY | O_CLOEXEC); + if (sockcreate_fd != kInvalidFd && + write(sockcreate_fd, security_context, context_size) == static_cast(context_size)) { + LOGV("Successfully set socket creation context via /proc/thread-self/attr/sockcreate: '%s'.", security_context); + return true; + } + + LOGV("Failed to set socket creation context via /proc/thread-self/attr/sockcreate," + " attempting process-specific fallback."); + + // Fallback: Try a process-specific path (might be less effective or deprecated depending on kernel). + char process_path[kMaxPathLengthInternal]; + snprintf(process_path, sizeof(process_path), "/proc/%d/attr/sockcreate", gettid()); + // Using gettid() for thread ID. + + sockcreate_fd = open(process_path, O_WRONLY | O_CLOEXEC); + if (sockcreate_fd == kInvalidFd || + write(sockcreate_fd, security_context, context_size) != static_cast(context_size)) { + PLOGE("Failed to set socket creation context via fallback path '%s'.", process_path); + return false; + } + + LOGV("Successfully set socket creation context via fallback path '%s': '%s'.", process_path, security_context); + return true; +}