Add binder transaction interception framework

This commit introduces a comprehensive framework for intercepting and manipulating binder transactions on Android at the `ioctl` level. It provides a man-in-the-middle layer between the binder driver and user-space `libbinder`, enabling detailed analysis and control over IPC.

The core mechanism works by hooking the `ioctl` system call within the context of a target process. It specifically intercepts the `BINDER_WRITE_READ` command's return buffer from the kernel.

Key components of the framework:

- IOCTL Hook: Intercepts `BR_TRANSACTION` commands delivered by the binder driver to the process.
- Transaction Rewriting: If a transaction is intended for a monitored service, its destination is rewritten in-memory to a local `BinderStub`. The original transaction details are saved in a thread-local context.
- BinderStub: A fake binder service that receives the hijacked transaction. It retrieves the original context and delegates processing to the `BinderInterceptor`.
- BinderInterceptor: The central management class. It maintains a registry of monitored binders and their associated callback interfaces. It orchestrates the pre-transact and post-transact hooks.
- Callback Protocol: Defines a clear protocol for a remote tool to:
    - Register and unregister binders for interception.
    - Receive pre-transaction notifications and choose to: continue, modify data, skip the transaction, or provide an immediate fake reply.
    - Receive post-transaction notifications with the final result and modify the reply.
This commit is contained in:
JingMatrix
2025-11-25 19:21:05 +01:00
parent cc52307ca8
commit 9bd75d15f7
+695 -2
View File
@@ -1,6 +1,699 @@
#include <android/binder.h>
#include <binder/Binder.h>
#include <binder/Common.h>
#include <binder/IPCThreadState.h>
#include <binder/IServiceManager.h>
#include <binder/Parcel.h>
#include <sys/ioctl.h>
#include <utils/StrongPointer.h>
#include <atomic>
#include <cinttypes>
#include <map>
#include <mutex>
#include <queue>
#include <shared_mutex>
#include <string_view>
#include <thread>
#include <utility>
#include "logging.hpp"
#include "lsplt.hpp"
extern "C" [[gnu::visibility("default")]] [[gnu::used]]
bool entry(void *library_handle) {
/**
* =========================================================================================
* BINDER INTERCEPTION LOGIC
* =========================================================================================
*
* [ Application / libbinder.so ] [ Android System / Service ]
* | ^
* | (1. Calls ioctl(BINDER_WRITE_READ) to wait for work) |
* v |
* [ Kernel Driver ] <------------------------------------------------------+
* |
* | (2. Kernel has an incoming transaction for this process,
* | prepares a BR_TRANSACTION command in the read_buffer)
* |
* v
* [ return from ioctl() is HOOKED ]
* |
* +---(3. Hook inspects the read_buffer from the Kernel)
* |
* +--- If a BR_TRANSACTION targets a monitored Binder:
* | (4) Rewrites the transaction's target to our BinderStub
* |
* v
* [ libbinder.so ]
* |
* | (5. libbinder processes the (modified) buffer and
* | dispatches the command to the BinderStub)
* |
* v
* [ BinderStub::onTransact ]
* |
* v
* [ BinderInterceptor ]
* |
* +---(6. Pre-Process / Modify / Log)
* |
* +---(7. Forward to Real Target) ----> [ Real Target BBinder ]
* |
* +---(8. Post-Process Reply)
* |
* v
* [ (9) Return Result to libbinder ]
*
* --- Explanation of the Flow ---
*
* This diagram illustrates a "man-in-the-middle" attack on the Binder framework, achieved
* by hooking the ioctl system call within the application's process.
*
* 1. Waiting for Work:
* An application's binder thread calls `ioctl()` with the `BINDER_WRITE_READ` command.
* This call typically blocks in the kernel, waiting for incoming transactions or other commands.
*
* 2. Kernel Prepares Command:
* When an external process sends a transaction to a service hosted in this application,
* the kernel driver prepares a `BR_TRANSACTION` command and places it in the `read_buffer`
* associated with the waiting `ioctl` call.
*
* 3. Interception on Return:
* The `ioctl()` call returns to userspace.
* Our hook intercepts this return. It now has access to the `read_buffer`
* populated by the kernel *before* `libbinder` gets to see it.
*
* 4. Hijacking:
* The hook parses the `read_buffer`. If it finds a `BR_TRANSACTION` command destined
* for a service that is registered with our `BinderInterceptor`, it rewrites the transaction data in-place.
* Specifically, it changes the target binder handle to that of our `BinderStub`
* and saves the original transaction details in a thread-local map.
*
* 5. Dispatch to Stub:
* The hook then returns control to the original caller, `libbinder`.
* `libbinder` proceeds to parse the now-modified buffer.
* Seeing a transaction for `BinderStub`, it invokes its `onTransact` method.
*
* 6. Pre-Processing:
* The `BinderStub` retrieves the original, unmodified transaction details from the thread-local map.
* It then passes control to the `BinderInterceptor`, which can log, modify,
* or block the transaction before it reaches its real destination.
*
* 7. Forwarding:
* The `BinderInterceptor` forwards the (potentially modified) transaction to the original,
* intended `BBinder` service.
*
* 8. Post-Processing:
* After the real service processes the transaction and generates a reply,
* the reply is returned to the `BinderInterceptor`,
* which gets a final chance to inspect or modify the result.
*
* 9. Return Result:
* The final result is returned up the call stack to `libbinder`,
* which sends the reply back to the kernel driver to be delivered to the original caller.
*
*
* =========================================================================================
**/
using namespace android;
// =============================================================================================
// Constants and Protocols
// =============================================================================================
namespace {
namespace intercept {
// Interceptor protocol codes (User space agreement between App and Interceptor Service)
constexpr uint32_t kRegisterInterceptor = 1;
constexpr uint32_t kUnregisterInterceptor = 2;
constexpr uint32_t kPreTransact = 1;
constexpr uint32_t kPostTransact = 2;
constexpr uint32_t kActionSkipTransaction = 1;
constexpr uint32_t kActionContinue = 2;
constexpr uint32_t kActionOverrideReply = 3;
constexpr uint32_t kActionOverrideData = 4;
constexpr uint32_t kActionContinueAndSkipPost = 5;
constexpr uint32_t kBackdoorCode = 0xdeadbeef;
// Strings for LibBinder hooks
constexpr std::string_view kBinderLibName = "/libbinder.so";
constexpr std::string_view kIoctlSymbol = "ioctl";
} // namespace intercept
// =============================================================================================
// Binder Driver Protocol Definitions (Ref: Android Kernel Header)
// =============================================================================================
// Use an X-Macro to define a list of all binder return protocols. This allows us
// to generate a string conversion function without a massive, hard-to-maintain switch statement.
#define BINDER_RETURN_COMMAND_LIST(X) \
X(BR_ERROR) \
X(BR_OK) \
X(BR_TRANSACTION_SEC_CTX) \
X(BR_TRANSACTION) \
X(BR_REPLY) \
X(BR_ACQUIRE_RESULT) \
X(BR_DEAD_REPLY) \
X(BR_TRANSACTION_COMPLETE) \
X(BR_INCREFS) \
X(BR_ACQUIRE) \
X(BR_RELEASE) \
X(BR_DECREFS) \
X(BR_ATTEMPT_ACQUIRE) \
X(BR_NOOP) \
X(BR_SPAWN_LOOPER) \
X(BR_FINISHED) \
X(BR_DEAD_BINDER) \
X(BR_CLEAR_DEATH_NOTIFICATION_DONE) \
X(BR_FAILED_REPLY) \
X(BR_FROZEN_REPLY) \
X(BR_ONEWAY_SPAM_SUSPECT) \
X(BR_TRANSACTION_PENDING_FROZEN) \
X(BR_FROZEN_BINDER) \
X(BR_CLEAR_FREEZE_NOTIFICATION_DONE)
// Helper macro to generate a 'case CMD: return "CMD";' line.
#define GENERATE_CASE_STRING(CMD) \
case CMD: \
return #CMD;
/**
* @brief Converts a binder driver return command code into its string representation.
* @param cmd The command code (e.g., BR_TRANSACTION).
* @return A string literal of the command name or "UNKNOWN_BR_COMMAND".
*/
const char *getBinderReturnCommandName(uint32_t cmd) {
switch (cmd) {
BINDER_RETURN_COMMAND_LIST(GENERATE_CASE_STRING)
default:
return "UNKNOWN_BR_COMMAND";
}
}
} // namespace
// =============================================================================================
// Global State & Forward Declarations
// =============================================================================================
// Original ioctl function pointer
int (*g_original_ioctl)(int fd, int request, ...) = nullptr;
// Unique ID generator for transactions
static std::atomic<uint64_t> g_transaction_id_counter = 0;
// Context info to pass from the ioctl hook (processBinderWriteRead) to the BinderStub.
struct ThreadTransactionInfo {
uint64_t transaction_id;
uint32_t transaction_code;
wp<BBinder> target_binder;
// Default constructor
ThreadTransactionInfo() : transaction_id(0), transaction_code(0) {}
ThreadTransactionInfo(uint64_t id, uint32_t code, wp<BBinder> target)
: transaction_id(id), transaction_code(code), target_binder(std::move(target)) {}
};
// A map keyed by thread ID. When ioctl intercepts a transaction intended for us,
// it pushes the info here. When the runtime calls our Stub, it pops the info.
static std::mutex g_thread_context_mutex;
static std::map<std::thread::id, std::queue<ThreadTransactionInfo>> g_thread_context_map;
// =============================================================================================
// Class: BinderInterceptor
// Logic: Manages the registry of intercepted Binders and handles the protocol (Pre/Post calls).
// =============================================================================================
class BinderInterceptor : public BBinder {
struct RegistrationEntry {
wp<IBinder> target;
sp<IBinder> callback_interface;
};
// Reader-Writer lock for the registry to allow concurrent reads (lookups)
mutable std::shared_mutex registry_mutex_;
std::map<wp<IBinder>, RegistrationEntry> registry_;
public:
BinderInterceptor() = default;
// Checks if a specific Binder instance is currently registered for interception
bool isBinderIntercepted(const wp<BBinder> &target) const {
std::shared_lock lock(registry_mutex_);
return registry_.find(target) != registry_.end();
}
// Main entry point for processing the "Man-in-the-Middle" logic
bool processInterceptedTransaction(uint64_t tx_id, sp<BBinder> target, uint32_t code, const Parcel &data,
Parcel *reply, uint32_t flags, status_t &result);
protected:
// Handle configuration commands sent to the Interceptor itself
status_t onTransact(uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags) override;
private:
status_t handleRegister(const Parcel &data);
status_t handleUnregister(const Parcel &data);
// Helpers to serialize data for the remote callback interface
status_t writeTransactionData(Parcel &out, uint64_t tx_id, sp<BBinder> target, uint32_t code, uint32_t flags,
const Parcel &in_data) const;
};
static sp<BinderInterceptor> g_interceptor_instance = nullptr;
// =============================================================================================
// Class: BinderStub
// Logic: The "Dummy" binder that acts as the destination for intercepted calls.
// It retrieves context from the global map and delegates to BinderInterceptor.
// =============================================================================================
class BinderStub : public BBinder {
protected:
status_t onTransact(uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags) override {
if (code != intercept::kBackdoorCode) {
LOGE("BinderStub received an unexpected direct call with code %u! This is a bug or misuse.", code);
return UNKNOWN_TRANSACTION;
}
ThreadTransactionInfo info;
bool found_context = false;
// 1. Retrieve the context for this thread (set previously by inspectAndRewriteTransaction)
{
std::lock_guard<std::mutex> lock(g_thread_context_mutex);
auto it = g_thread_context_map.find(std::this_thread::get_id());
if (it != g_thread_context_map.end() && !it->second.empty()) {
info = std::move(it->second.front());
it->second.pop();
if (it->second.empty()) {
g_thread_context_map.erase(it); // Cleanup to prevent memory leak
}
found_context = true;
}
}
if (!found_context) {
LOGW("BinderStub received transaction but no context found for thread");
return UNKNOWN_TRANSACTION;
}
// 2. Handle special "Backdoor" to get the Interceptor reference
if (info.transaction_code == intercept::kBackdoorCode && info.target_binder == nullptr && reply) {
LOGD("Backdoor handshake received.");
reply->writeStrongBinder(g_interceptor_instance);
return OK;
}
// 3. Promote the weak reference to the real target
sp<BBinder> real_target = info.target_binder.promote();
if (!real_target) {
LOGE("[TX_ID: %" PRIu64 "] Target binder is dead.", info.transaction_id);
return DEAD_OBJECT;
}
// 4. Delegate to the Interceptor logic
status_t status = OK;
bool interceptorManagedFlow = g_interceptor_instance->processInterceptedTransaction(
info.transaction_id, real_target, info.transaction_code, data, reply, flags, status);
// 5. If Interceptor logic says "Forward it", we call the original binder
if (!interceptorManagedFlow) {
LOGV("[TX_ID: %" PRIu64 "] Forwarding to original implementation.", info.transaction_id);
status = real_target->transact(info.transaction_code, data, reply, flags);
}
return status;
}
};
static sp<BinderStub> g_stub_instance = nullptr;
// =============================================================================================
// Hook Logic: IOCTL & Buffer Parsing
// =============================================================================================
namespace {
/**
* @brief Analyses a binder transaction. If the target is monitored,
* hijacks the transaction by rewriting its destination to our BinderStub.
* @param txn_data Pointer to the transaction data within the ioctl buffer.
*/
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
if (!txn_data || txn_data->target.ptr == 0)
return;
bool hijack = false;
ThreadTransactionInfo info;
// Check 1: Root user backdoor for retrieving the interceptor service binder
if (txn_data->code == intercept::kBackdoorCode && txn_data->sender_euid == 0) {
info.transaction_code = intercept::kBackdoorCode;
info.target_binder = nullptr;
hijack = true;
}
// Check 2: Normal interception based on registry of monitored binders
else {
// Safe casting based on Binder driver ABI
RefBase::weakref_type *weak_ref = reinterpret_cast<RefBase::weakref_type *>(txn_data->target.ptr);
// Try to acquire a temporary strong reference to check the object safely
if (weak_ref && weak_ref->attemptIncStrong(nullptr)) {
// The raw pointer to the binder object itself is stored in the cookie
BBinder *target_binder_ptr = reinterpret_cast<BBinder *>(txn_data->cookie);
// This is safe ONLY because we successfully called attemptIncStrong().
// The sp<> constructor will not increment the ref count again, it just adopts the one we have.
// When sp_target goes out of scope, it will call decStrong(), releasing our temporary reference.
sp<BBinder> sp_target = sp<BBinder>::fromExisting(target_binder_ptr);
// Now we can safely use sp_target (which implicitly converts to a wp) for the lookup.
if (g_interceptor_instance->isBinderIntercepted(sp_target)) {
info.transaction_code = txn_data->code;
info.target_binder = sp_target; // Assign the valid weak pointer
hijack = true;
}
// No need to manually call decStrong(); the sp destructor handles it.
}
}
if (hijack) {
uint64_t tx_id = ++g_transaction_id_counter;
info.transaction_id = tx_id;
LOGV("[Hook] Hijacking Transaction %" PRIu64 " (Code: %u)", tx_id, txn_data->code);
// Rewrite the destination to our Stub
txn_data->target.ptr = reinterpret_cast<uintptr_t>(g_stub_instance->getWeakRefs());
txn_data->cookie = reinterpret_cast<uintptr_t>(g_stub_instance.get());
txn_data->code = intercept::kBackdoorCode;
// Store context for the stub to retrieve later in its onTransact
std::lock_guard<std::mutex> lock(g_thread_context_mutex);
g_thread_context_map[std::this_thread::get_id()].push(std::move(info));
}
}
/**
* @brief Parses the read buffer from a BINDER_WRITE_READ ioctl call, which contains
* commands sent from the kernel driver to userspace.
* @param bwr The binder_write_read struct containing buffer pointers and sizes.
*/
void processBinderReadBuffer(const binder_write_read &bwr) {
if (bwr.read_size == 0 || bwr.read_consumed == 0 || bwr.read_buffer == 0)
return;
uintptr_t ptr = bwr.read_buffer;
uintptr_t end = ptr + bwr.read_consumed;
LOGV("[Hook] Processing Read Buffer: Size=%llu, Consumed=%llu", bwr.read_size, bwr.read_consumed);
while (ptr < end) {
// Ensure we can read at least the command header
if (end - ptr < sizeof(uint32_t))
break;
uint32_t cmd = *reinterpret_cast<const uint32_t *>(ptr);
ptr += sizeof(uint32_t);
// Calculate payload size from the ioctl command code
size_t cmd_size = _IOC_SIZE(cmd);
// Log the command using our generated to-string function
LOGV("[Driver -> User] Command: %s (0x%x), DataSize: %zu", getBinderReturnCommandName(cmd), cmd, cmd_size);
// Safety check: ensure the command's data does not exceed the buffer
if (ptr + cmd_size > end) {
LOGE("[Hook] Buffer overflow detected while parsing command %s", getBinderReturnCommandName(cmd));
break;
}
// We are primarily interested in BR_TRANSACTION commands to intercept
if (cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX) {
binder_transaction_data *txn = nullptr;
if (cmd == BR_TRANSACTION_SEC_CTX) {
// The data is wrapped in a secctx struct
auto *wrapper = reinterpret_cast<binder_transaction_data_secctx *>(ptr);
txn = &wrapper->transaction_data;
} else {
txn = reinterpret_cast<binder_transaction_data *>(ptr);
}
inspectAndRewriteTransaction(txn);
}
// Advance pointer to the next command
ptr += cmd_size;
}
}
} // namespace
// =============================================================================================
// The Actual Hook Function
// =============================================================================================
int intercepted_ioctl(int fd, int request, ...) {
va_list ap;
va_start(ap, request);
void *arg = va_arg(ap, void *);
va_end(ap);
// 1. Call original kernel ioctl to let the driver do its work
int result = g_original_ioctl(fd, request, arg);
// 2. After the call returns, check if it was a BINDER_WRITE_READ and if it succeeded
if (result >= 0 && request == BINDER_WRITE_READ && arg != nullptr) {
const auto *bwr = static_cast<const binder_write_read *>(arg);
// We only care about data read FROM the driver (i.e., incoming commands)
if (bwr->read_consumed > 0) {
processBinderReadBuffer(*bwr);
}
}
return result;
}
// =============================================================================================
// BinderInterceptor Implementation
// =============================================================================================
// Placed at the top of the .cpp file, inside the BinderInterceptor implementation section.
#define VALIDATE_STATUS(tx_id, expr) \
do { \
status_t __result = (expr); \
if (__result != OK) { \
LOGE("[TX_ID: %" PRIu64 "] Parcel operation failed in %s: '%s' returned %d", (tx_id), __func__, #expr, \
__result); \
return __result; \
} \
} while (0)
status_t BinderInterceptor::onTransact(uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags) {
switch (code) {
case intercept::kRegisterInterceptor:
return handleRegister(data);
case intercept::kUnregisterInterceptor:
return handleUnregister(data);
default:
return BBinder::onTransact(code, data, reply, flags);
}
}
status_t BinderInterceptor::handleRegister(const Parcel &data) {
sp<IBinder> target;
sp<IBinder> callback;
if (data.readStrongBinder(&target) != OK || !target)
return BAD_VALUE;
if (data.readStrongBinder(&callback) != OK || !callback)
return BAD_VALUE;
// We can only intercept local Binders (BBinder), not remote proxies (BpBinder)
if (target->localBinder() == nullptr) {
LOGE("Cannot intercept remote binder proxies.");
return BAD_TYPE;
}
wp<IBinder> weak_target = target;
std::unique_lock lock(registry_mutex_);
registry_[weak_target] = {weak_target, callback};
LOGI("Interceptor registered for binder %p", target.get());
return OK;
}
status_t BinderInterceptor::handleUnregister(const Parcel &data) {
sp<IBinder> target;
if (data.readStrongBinder(&target) != OK || !target)
return BAD_VALUE;
wp<IBinder> weak_target = target;
std::unique_lock lock(registry_mutex_);
if (registry_.erase(weak_target) > 0) {
LOGI("Interceptor unregistered for binder %p", target.get());
return OK;
}
LOGW("Attempted to unregister a non-existent interceptor for binder %p", target.get());
return NAME_NOT_FOUND;
}
status_t BinderInterceptor::writeTransactionData(Parcel &out, uint64_t tx_id, sp<BBinder> target, uint32_t code,
uint32_t flags, const Parcel &in_data) const {
// This is the data contract for communicating with the remote analysis/control tool
VALIDATE_STATUS(tx_id, out.writeInt64(tx_id));
VALIDATE_STATUS(tx_id, out.writeStrongBinder(target));
VALIDATE_STATUS(tx_id, out.writeUint32(code));
VALIDATE_STATUS(tx_id, out.writeUint32(flags));
VALIDATE_STATUS(tx_id, out.writeInt32(IPCThreadState::self()->getCallingUid()));
VALIDATE_STATUS(tx_id, out.writeInt32(IPCThreadState::self()->getCallingPid()));
VALIDATE_STATUS(tx_id, out.writeUint64(in_data.dataSize()));
VALIDATE_STATUS(tx_id, out.appendFrom(&in_data, 0, in_data.dataSize()));
return OK;
}
bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder> target, uint32_t code,
const Parcel &request, Parcel *reply, uint32_t flags,
status_t &result) {
sp<IBinder> callback;
{
std::shared_lock lock(registry_mutex_);
auto it = registry_.find(target);
if (it == registry_.end())
return false; // Should not happen given logic in hook, but safe
callback = it->second.callback_interface;
}
// --- Phase 1: Pre-Transaction Callback ---
Parcel pre_req, pre_resp;
writeTransactionData(pre_req, tx_id, target, code, flags, request);
if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) {
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id);
return false; // Callback failed, proceed as if not intercepted
}
int32_t action = pre_resp.readInt32();
// ACTION: Override Reply immediately and skip the real transaction
if (action == intercept::kActionOverrideReply) {
if (reply) {
result = pre_resp.readInt32(); // Read status code from response
size_t size = pre_resp.readUint64();
reply->setDataSize(0);
reply->appendFrom(&pre_resp, pre_resp.dataPosition(), size);
}
return true; // Handled
}
// ACTION: Silently skip/drop the transaction
if (action == intercept::kActionSkipTransaction) {
result = OK; // Return OK to caller, but do nothing
return true; // Handled
}
// ACTION: Skip the post-transaction hook
if (action == intercept::kActionContinueAndSkipPost) {
result = OK; // Return OK to caller, but do nothing
return false; // Forward it
}
// ACTION: Modify the transaction's request data before forwarding
Parcel final_request;
if (action == intercept::kActionOverrideData) {
size_t size = pre_resp.readUint64();
final_request.appendFrom(&pre_resp, pre_resp.dataPosition(), size);
} else {
// Default (kActionContinue): Use original data
final_request.appendFrom(&request, 0, request.dataSize());
}
// --- Phase 2: Execute Original Transaction ---
result = target->transact(code, final_request, reply, flags);
// --- Phase 3: Post-Transaction Callback ---
Parcel post_req, post_resp;
writeTransactionData(post_req, tx_id, target, code, flags, final_request);
// Append the result of the execution for the callback to see
VALIDATE_STATUS(tx_id, post_req.writeInt32(result));
size_t reply_size = (reply) ? reply->dataSize() : 0;
VALIDATE_STATUS(tx_id, post_req.writeUint64(reply_size));
if (reply && reply_size > 0) {
VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size));
}
if (callback->transact(intercept::kPostTransact, post_req, &post_resp) == OK) {
int32_t post_action = post_resp.readInt32();
if (post_action == intercept::kActionOverrideReply && reply) {
result = post_resp.readInt32(); // Read new status
size_t new_size = post_resp.readUint64();
reply->setDataSize(0); // Clear original reply
VALIDATE_STATUS(tx_id, reply->appendFrom(&post_resp, post_resp.dataPosition(), new_size));
}
}
return true; // We handled the flow, even if we just forwarded it
}
// =============================================================================================
// Initialization and Entry Point
// =============================================================================================
bool initialize_hooks() {
auto maps = lsplt::MapInfo::Scan();
dev_t binder_dev = 0;
ino_t binder_ino = 0;
bool found = false;
for (const auto &map : maps) {
if (map.path.ends_with(intercept::kBinderLibName)) {
binder_dev = map.dev;
binder_ino = map.inode;
found = true;
LOGD("Found libbinder at: %s", map.path.c_str());
break;
}
}
if (!found) {
LOGE("Could not find libbinder.so in memory maps.");
return false;
}
// Instantiate Singleton components
g_interceptor_instance = sp<BinderInterceptor>::make();
g_stub_instance = sp<BinderStub>::make();
// Register the ioctl hook with LSPLT
lsplt::RegisterHook(binder_dev, binder_ino, intercept::kIoctlSymbol.data(),
reinterpret_cast<void *>(intercepted_ioctl), reinterpret_cast<void **>(&g_original_ioctl));
if (!lsplt::CommitHook()) {
LOGE("lsplt::CommitHook failed.");
return false;
}
LOGI("Binder interception initialized successfully.");
return true;
}
extern "C" [[gnu::visibility("default")]] [[gnu::used]]
bool entry(void *handle) {
LOGI("Binder Interceptor library loaded (handle: %p)", handle);
return initialize_hooks();
}