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