Context & introduction
Disclosure notice: Throughout this article, all vendor-specific identifiers (vendor name, binary names, service names, package names) have been replaced with generic placeholders. The vendor has been anonymized as
[VENDOR], and affected names follow the convention[VENDOR]tvservice,com.[VENDOR].REDACTED, etc.
I decided to take a closer look at my Android TV after noticing it was unusually slow. Beyond the performance issue, the device came with a significant amount of pre-installed vendor software that could not be removed without root access. This sparked an interest in the security of the vendor layer running on the device.
This article documents the investigation of vendor-specific native services and their IPC interfaces. The goal is to explain what Android Binders are, how vendors extend them, and what the attack surface looks like from an offensive perspective.
Initial reconnaissance
The first step when assessing an Android IoT device is to enumerate processes running as root. Vendor binaries are a particularly interesting target: they often implement complex functionality with less security scrutiny than the Android framework itself, and they run with the highest privilege level on the system.
| |
Several non-standard vendor binaries are running as root. The main target here is [VENDOR]tvservice, a 25 MB stripped ARM ELF binary.
| |

| |
Opening the binary in Ghidra and searching for interesting imports, system immediately stands out.


system is used in multiple places, but one occurrence is particularly interesting:
| |
This handler reads a command string directly from the caller-supplied Parcel and passes it to tos_system_execute_cmd, which calls system() β a direct OS shell execution. Since [VENDOR]tvservice runs as root, any caller that reaches this code path with an arbitrary command achieves root command execution. The only protection in place is the tos_system_verify check at the top. Understanding what that means requires some background on Android IPC.
Background: Android IPC
System services
Android separates application code from privileged system functionality through system services β long-running background processes that expose well-defined interfaces to clients. Unlike Android application services (declared in a manifest), system services run outside the application lifecycle and are managed by the platform itself.
Examples include WindowManagerService, ActivityManagerService, or hardware abstraction layers for audio and sensors. On vendor-customized devices, additional proprietary system services are layered on top to expose device-specific functionality.
Binder: the kernel IPC driver
All processes on Android run in isolated sandboxes. Direct memory sharing between processes is not allowed. The Binder [1] is the kernel-level IPC mechanism that bridges this isolation. It is exposed as a character device, /dev/binder, which every participating process opens and mmap()s.
When a client sends a call to a service, the kernel driver copies the data from the client’s address space directly into the service’s mapped region, without an intermediate copy through a kernel buffer. This zero-copy design is the main performance advantage of Binder over traditional Unix IPC.

Android ships with four standard binder devices:
| Device | Usage |
|---|---|
/dev/binder | Standard Android framework IPC |
/dev/hwbinder | Hardware HAL communication |
/dev/vndbinder | Vendor services (AOSP-standard) |
On the device under analysis, a fourth device is present:
| |
/dev/sbinder is not part of the AOSP standard. We will come back to it.

ServiceManager
Since multiple services can coexist on the same binder device, there needs to be a name registry. The ServiceManager [2] fulfills this role: it runs at handle 0 (the reserved context manager handle) and maintains a mapping between service names and their binder handles. Services register themselves at startup; clients query by name to obtain a handle before sending calls.
| |
The object model
The Binder framework exposes three core abstractions [3]:
IBinder: The common interface implemented by all binder objects.BBinder: The server-side base class. Services subclass it and overrideonTransact()to handle incoming calls.BpBinder(Binder Proxy): The client-side stub. It holds a remote handle and forwards calls through the kernel driver.
When a client calls a method on a remote service, it invokes transact() on a BpBinder. The kernel delivers a BR_TRANSACTION event to the server thread, which dispatches it to onTransact() on the corresponding BBinder.
Parcels
Data is exchanged through Parcel objects [4] β flat byte buffers with typed read/write accessors (readInt32(), readString16(), readBlob(), etc.) and bookkeeping for embedded binder references.
The first field written into any Parcel is the interface token β a UTF-16LE string that identifies the target interface. The server calls enforceInterface() on receipt to verify the token matches, preventing a client from sending a call meant for interface A to a service implementing interface B.
Going back to the Ghidra output above: onTransact() is the server-side handler. param_2 is the transaction code (which method is being called), param_3 is the input Parcel (caller-supplied data), and param_4 is the reply Parcel. param_1 is the implicit this pointer, typed as uint by Ghidra since the class type was not resolved.
The vendor sbinder ecosystem
Going back to the binary, [VENDOR]tvservice does not register its services on the standard /dev/binder. Instead, it opens a completely separate device: /dev/sbinder.
| |
This function is used to bootstrap a full ServiceManager loop embedded inside the binary itself:
| |
[VENDOR]tvservice is both a service host and a ServiceManager for /dev/sbinder. It establishes a fully parallel IPC universe β one that is completely invisible to the standard service list command, which only queries /dev/binder.
Registered services
The function proxy_add_service() registers services on sbinder at startup, driven by a bitmask:
| |
Among others, remote_property_module is registered (bit 5 of 0x126 is set). sbinder_system_service β the handler containing the system() call β is also registered, separately from this function.
Additionally, the application_manager binary exposes remote_application_manager_sbinder, and ntvi_server exposes over 40 distinct sbinder services covering audio, video, CEC, Bluetooth, network settings, EPG, PVR, and more.
SELinux and sbinder access
The device file has world-readable/writable permissions but carries a SELinux label:
| |
Inspection of the firmware’s SELinux policy (vendor/etc/selinux/vendor_sepolicy.cil) reveals which domains can access it:
| |
Notably, both untrusted_app (any third-party application) and shell (ADB) appear to be allowed to open and map the device. In practice, whether this is sufficient to communicate with the services depends on a second security layer.
The security layer: tos_system_verify
Allowing unprivileged processes to open /dev/sbinder does not directly expose the methods offered by the services registered on it. Every onTransact() handler in [VENDOR]tvservice begins with an authorization check:
| |
tos_system_verify maintains an in-memory whitelist of authorized PIDs. The calling PID is obtained through IPCThreadState::getCallingPid(), which reads the value set by the kernel at the time of the transaction β it cannot be forged by the caller. If the PID is not on the whitelist, the call is rejected before any service logic runs. Any process whose PID is absent from this list will receive PERMISSION_DENIED regardless of the method it is trying to invoke.
How does a process get authorized?
The whitelist is populated through a dedicated bootstrap service: remote_property_module. This service exposes a method β TRANSACTION_tos_system_authorized (transaction code 6) β whose sole purpose is to add a PID to the whitelist. The call requires a signed authorization key:
| |
The authorized_key field follows the format:
| |
This key is verified by fpi_system_security_authorized, a function within [VENDOR]tvservice that is invoked by tos_system_authorized when processing this transaction. It performs the following steps:
- Base64-decode the signature
- RSA-decrypt it using a public key embedded in the binary
- Verify that the SHA-1 of the
cmdlinefield matches the decrypted hash - Verify that
/proc/<pid>/cmdlinematches the declaredcmdline - On success, add the PID to the licensed app list
IMAGE
To authorize any process, a valid RSA signature from the vendor’s private key is required.
The bypass: method codes 1β7 skip verification
A closer look at the onTransact() header in remote_property_module reveals an unusual condition:
| |
The mask 0xfffffffb equals ~0x4, clearing bit 2. Evaluating this condition for each method code:
| Code | & 0xfffffffb | - 2 | > 1 | Verification? |
|---|---|---|---|---|
| 1 | 1 | -1 | No | Bypassed |
| 2 | 2 | 0 | No | Bypassed |
| 3 | 3 | 1 | No | Bypassed |
| 4 | 0 | -2 | No | Bypassed |
| 5 | 1 | -1 | No | Bypassed |
| 6 | 2 | 0 | No | Bypassed |
| 7 | 3 | 1 | No | Bypassed |
| 8 | 8 | 6 | Yes | Verified |
| 9+ | … | >1 | Yes | Verified |
Method codes 1 through 7 bypass tos_system_verify. The intent is clear: the authorization method (code 6) must be reachable by any process before it is whitelisted, so verification is intentionally skipped for the bootstrap range. The security is supposed to come from the RSA signature, not from the access control check.
PID spoofing vulnerability
Inside the handler for code 6, the PID to be authorized is read from the client-supplied Parcel:
| |
The PID is not taken from IPCThreadState::getCallingPid(). It is an arbitrary integer supplied by the caller. This means an attacker can request authorization for any PID of their choice β for instance, the PID of [VENDOR]tvservice itself β without needing to run code in that process.
The exploit flow would be:
- Open
/dev/sbinderfrom ADB - Send method code
6toremote_property_module - Supply any PID in the Parcel
- If the signature check passes, that PID is whitelisted for all privileged operations
Step 4 is where the attack stalls.
The RSA wall
fpi_system_security_authorized validates the authorization key against a 1024-bit RSA public key embedded in [VENDOR]tvservice:
| |
The debug strings in the binary confirm the verification steps:
| |
A search across the entire firmware for keys matching this public key’s modulus, and for any pre-signed authorization tokens, returned no results. The private key is held on the vendor’s build infrastructure. No application in the firmware embeds a hardcoded signed token.

This blocks exploitation of sbinder_system_service via the authorization path: even with the PID spoofing primitive and the bypass on method code 6, generating a valid signature requires the private key.
application_manager: a different beast
While [VENDOR]tvservice enforces tos_system_verify on all its services, the application_manager binary does not implement this check at all.
| |
Inspection of application_manager’s onTransact() in Ghidra confirms: there is no call to tos_system_verify. The service accepts calls from any process without any authorization check.
The service descriptor is remote_application_manager_sbinder. Its interface, derived from libtaf_client.so (the vendor’s Application Framework client library), exposes the following operations among others:
| Code | Method | Notes |
|---|---|---|
| 22 | PushKeyevent | Inject key events |
| 34 | InstallApp | Install a .tpk application |
| 35 | UninstallApp | Uninstall an application |
| 38 | GetInstalledApps | Enumerate installed apps |
Method 34 β InstallApp β is the most significant. The installation path leads to AppRunner::StartApp:
| |
The binary at the caller-supplied path is executed via fork + execve. Since application_manager runs as root, the spawned process inherits root privileges. Any caller that can reach this method can execute arbitrary binaries as root, with no RSA signature required.

The Parcel format for InstallApp (method code 34) is:
| |
I wrote a native proof-of-concept that implements the raw sbinder protocol to send this transaction:
| |
Result:
| |
/dev/sbinder opened successfully. mmap() failed. SELinux intervened.

SELinux: the real gatekeeper
Despite the SELinux policy granting shell_30_0 the map permission on sbinder_device, the kernel AVC log shows a denial:
| |
The ADB shell process runs in the u:r:shell:s0 domain, not u:r:shell_30_0:s0. These are distinct SELinux types, and shell (the context assigned to processes spawned via ADB) does not have map permission on sbinder_device.
Without mmap, the Binder protocol cannot function: the kernel driver requires a memory-mapped receive buffer to deliver reply data to the caller. The exploitation attempt is blocked before it can even resolve the service handle.
The situation across all binder devices:
| Device | ADB shell access | Reason |
|---|---|---|
/dev/binder | Full | Standard Android, shell:s0 allowed |
/dev/hwbinder | Full | HAL, shell:s0 allowed |
/dev/vndbinder | Full | Vendor, shell:s0 allowed |
/dev/sbinder | No mmap | SELinux denies map for shell:s0 |
What would have worked
The following summarizes each attempted attack path and the control that blocked it:
| |
Identified vulnerabilities:
- PID spoofing in
remote_property_module(method code 6): the PID to whitelist is read from caller-controlled data rather than from the kernel. An attacker can request whitelisting of any arbitrary PID. application_managerhas notos_system_verify: any process able to communicate via sbinder can install an application that will be executed as root.sbinder_system_serviceexposes arbitrary command execution: method code0x18passes a caller-controlled Parcel blob directly tosystem()as root. Any process that clearstos_system_verifycan achieve root RCE by supplying an arbitrary command in the Parcel.
The defense-in-depth combination β SELinux blocking the mmap at the kernel level, and RSA signatures gating the authorization path β prevented full exploitation from the ADB shell context.

To go further, one of the following would be needed:
- Code execution in a process context that SELinux permits to
mmap/dev/sbinder(e.g., through a vulnerability in an already-authorized process such ascom.[VENDOR].systemserver) - Access to the RSA private key, or recovery of a signed token from the memory of a running authorized process
- Exploitation of the TOCTOU window on
/proc/<pid>/cmdlineduring PID spoofing
Conclusion
This investigation illustrates how vendor customizations can introduce significant complexity into the Android security model. The vendor here built a fully parallel IPC infrastructure on /dev/sbinder β with its own ServiceManager, its own service registry, and its own authorization scheme β largely outside the visibility of standard Android tooling.
Two design weaknesses stand out. First, using an attacker-controlled value as a security-sensitive identifier (the PID in the authorization call) violates a basic principle: security decisions must rely on kernel-verified data, not caller-supplied data. Second, exposing a service that executes arbitrary shell commands as root (application_manager) without any authorization check creates a critical primitive that is one SELinux policy gap away from full compromise.
References
[1] Android Binder IPC β https://source.android.com/docs/core/architecture/hidl/binder-ipc
[2] Android ServiceManager β https://cs.android.com/android/platform/superproject/+/main:frameworks/native/cmds/servicemanager/
[3] Binder object model β https://cs.android.com/android/platform/superproject/+/main:frameworks/native/libs/binder/
[4] Android Parcel β https://developer.android.com/reference/android/os/Parcel
[5] Binder transactions in the Linux kernel β https://www.synacktiv.com/en/publications/binder-transactions-in-the-bowels-of-the-linux-kernel
Further reading
- Fuzzing Samsung system services over Binder β Thalium, 2023
- Android Binder: from basics to exploitation β Duo Security
- Android IPC mechanisms overview β Android documentation
