#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "logging.hpp" #include "lsplt.hpp" /** * ========================================================================================= * 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 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 target_binder; // Default constructor ThreadTransactionInfo() : transaction_id(0), transaction_code(0) {} ThreadTransactionInfo(uint64_t id, uint32_t code, wp 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> 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 target; sp callback_interface; std::vector filtered_codes; }; mutable std::shared_mutex registry_mutex_; std::map, RegistrationEntry> registry_; public: BinderInterceptor() = default; bool shouldIntercept(const wp &target, uint32_t code) const { std::shared_lock lock(registry_mutex_); auto it = registry_.find(target); if (it == registry_.end()) return false; const auto &codes = it->second.filtered_codes; return codes.empty() || std::find(codes.begin(), codes.end(), code) != codes.end(); } // Main entry point for processing the "Man-in-the-Middle" logic bool processInterceptedTransaction(uint64_t tx_id, sp 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 target, uint32_t code, uint32_t flags, const Parcel &in_data) const; }; static sp 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 { public: const String16& getInterfaceDescriptor() const override { static const String16 kDescriptor("org.matrix.TEESimulator.BinderStub"); return kDescriptor; } 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 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 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 g_stub_instance = nullptr; // ============================================================================================= // Hook Logic: IOCTL & Buffer Parsing // ============================================================================================= namespace { void inspectAndRewriteTransaction(binder_transaction_data *txn_data) { if (!txn_data || txn_data->target.ptr == 0) return; // AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff). // System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range. // Skip those — intercepting a ping adds measurable latency that timing detectors flag. if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode) 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: Spoof uid of KeyStore requests from the daemon to bypass permission check } else if (txn_data->sender_euid == 0) { // The kernel driver fills sender_euid. // libbinder.so trusts this value to populate IPCThreadState. txn_data->sender_euid = 1000; LOGV("[Hook] Spoofing UID for transaction: 0 -> %d", txn_data->sender_euid); hijack = false; // Never hijack to avoid recursion // Check 3: Normal interception based on registry of monitored binders } else { // Safe casting based on Binder driver ABI RefBase::weakref_type *weak_ref = reinterpret_cast(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(txn_data->cookie); // Create a weak pointer for the lookup and to store in our context map. // This is safe because we are holding a strong reference. wp wp_target = target_binder_ptr; if (g_interceptor_instance->shouldIntercept(wp_target, txn_data->code)) { info.transaction_code = txn_data->code; info.target_binder = wp_target; // Assign the valid weak pointer hijack = true; } // Manually release the temporary strong reference we acquired at the start. target_binder_ptr->decStrong(nullptr); } } 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(g_stub_instance->getWeakRefs()); txn_data->cookie = reinterpret_cast(g_stub_instance.get()); txn_data->code = intercept::kBackdoorCode; // Store context for the stub to retrieve later in its onTransact std::lock_guard 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; while (ptr < end) { if (end - ptr < sizeof(uint32_t)) break; uint32_t cmd = *reinterpret_cast(ptr); ptr += sizeof(uint32_t); size_t cmd_size = _IOC_SIZE(cmd); if (ptr + cmd_size > end) { LOGE("[Hook] Buffer overrun parsing command 0x%x", cmd); break; } if (__builtin_expect(cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX, 0)) { binder_transaction_data *txn; if (cmd == BR_TRANSACTION_SEC_CTX) { txn = &reinterpret_cast(ptr)->transaction_data; } else { txn = reinterpret_cast(ptr); } inspectAndRewriteTransaction(txn); } 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); if (result >= 0 && request == BINDER_WRITE_READ && arg != nullptr) { const auto *bwr = static_cast(arg); // Fast reject: only enter the parser if the buffer could contain a BR_TRANSACTION. // Pings, ref ops, and looper management never produce BR_TRANSACTION, so scanning // their buffers is pure overhead (~2-5us per ioctl in debug builds). if (bwr->read_consumed >= sizeof(uint32_t)) { uint32_t first_cmd = *reinterpret_cast(bwr->read_buffer); if (first_cmd == BR_TRANSACTION || first_cmd == BR_TRANSACTION_SEC_CTX || bwr->read_consumed > sizeof(uint32_t) + _IOC_SIZE(first_cmd)) { 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 target; sp callback; if (data.readStrongBinder(&target) != OK || !target) return BAD_VALUE; if (data.readStrongBinder(&callback) != OK || !callback) return BAD_VALUE; if (target->localBinder() == nullptr) { LOGE("Cannot intercept remote binder proxies."); return BAD_TYPE; } std::vector codes; int32_t code_count = 0; if (data.dataAvail() >= sizeof(int32_t) && data.readInt32(&code_count) == OK && code_count > 0) { codes.reserve(code_count); for (int32_t i = 0; i < code_count; i++) { uint32_t c = 0; if (data.readUint32(&c) == OK) codes.push_back(c); } LOGI("Interceptor registered for binder %p with %zu filtered codes", target.get(), codes.size()); } else { LOGI("Interceptor registered for binder %p (all codes)", target.get()); } wp weak_target = target; std::unique_lock lock(registry_mutex_); registry_[weak_target] = {weak_target, callback, std::move(codes)}; return OK; } status_t BinderInterceptor::handleUnregister(const Parcel &data) { sp target; if (data.readStrongBinder(&target) != OK || !target) return BAD_VALUE; wp 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 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 target, uint32_t code, const Parcel &request, Parcel *reply, uint32_t flags, status_t &result) { sp 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); status_t pre_status = callback->transact(intercept::kPreTransact, pre_req, &pre_resp); if (pre_status != OK) { // Block when interceptor is dead to prevent privacy leak to third-party apps if (callback->pingBinder() != OK) { LOGE("[TX_ID: %" PRIu64 "] Interceptor DEAD. Blocking to prevent attestation leak.", tx_id); result = DEAD_OBJECT; return true; } LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed (not dead). Forwarding.", tx_id); return false; } 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)); } status_t post_status = callback->transact(intercept::kPostTransact, post_req, &post_resp); if (post_status == 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::make(); g_stub_instance = sp::make(); // Register the ioctl hook with LSPLT lsplt::RegisterHook(binder_dev, binder_ino, intercept::kIoctlSymbol.data(), reinterpret_cast(intercepted_ioctl), reinterpret_cast(&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(); }