← All posts
·13 min read ·#android #internals #mobile

Can you spoof getCallingUid()?

It's the question I get every time Binder comes up, and for years my answer was basically 'nah, the kernel handles it', which is what you say when you don't actually know. So I followed one call down to /dev/binder to find out where that UID really comes from.

Every time Android security comes up with someone newer to it, I get some version of the same question:

If I write a malicious app, what stops it from lying about its UID? getCallingUid() just returns an int. Who says that int is telling the truth?

And for an embarrassingly long time my answer was some version of “the kernel handles it, don’t worry about it.” If you’ve ever said that, you know it’s the thing you say when you don’t actually know. You know the vibe of the answer. You couldn’t point at the line.

I got tired of that, so one evening I actually followed a call down to see who writes that number. This is that walk. It ends in the kernel and it starts with the single most boring line of Android code I can come up with:

val cm = getSystemService(ConnectivityManager::class.java)
cm.activeNetwork

Looks like a method call. It’s not. ConnectivityService, the thing that actually knows whether you’re online, lives in system_server. Different process, different UID, memory your app is never going to touch. You read activeNetwork, and a few microseconds later an answer shows up like nothing happened. Something carried your request across a process boundary and brought a reply back, fast enough that you never think about it.

That something is /dev/binder. And once I started reading it, the UID question mostly answered itself, but you have to walk the whole thing to see why. So bear with me.1

It’s one ioctl. That’s the whole API.

I went in expecting a pile of syscalls. There’s basically one.

A process opens /dev/binder exactly once (libbinder’s ProcessState does it lazily, the first time you touch anything remote), and after that, every conversation it will ever have is the same ioctl, forever:

struct binder_write_read {
    binder_size_t     write_size;
    binder_size_t     write_consumed;
    binder_uintptr_t  write_buffer;
    binder_size_t     read_size;
    binder_size_t     read_consumed;
    binder_uintptr_t  read_buffer;
};

#define BINDER_WRITE_READ _IOWR('b', 1, struct binder_write_read)

You give it a write buffer full of commands (BC_*, each with a payload) and a read buffer for the driver to drop returns into (BR_*). One syscall, both directions. You push whatever you’re sending and pick up whatever came in for you on the same trip. A method call going out is BC_TRANSACTION. The reply coming back is BR_REPLY. Everything else is bookkeeping around those two.

client · userspace kernel server · userspace binder driver /dev/binder cm.activeNetwork parcel.writeInt(…) IPCThreadState::transact ioctl(fd, WRITE_READ) onTransact(code, …) parcel.readInt(…) executeCommand ioctl(fd, WRITE_READ) BC_TRANSACTION → BR_TRANSACTION BC_REPLY → BR_REPLY
Fig 1 · one request goes down the client stack, across as BC_TRANSACTION, gets delivered as BR_TRANSACTION, and the reply comes back the same way.

What’s actually on the wire

One BC_TRANSACTION carries one struct. Here it is, and I want you to look at two specific lines:

struct binder_transaction_data {
    union {
        __u32             handle;
        binder_uintptr_t  ptr;
    } target;
    binder_uintptr_t  cookie;
    __u32             code;
    __u32             flags;
    __kernel_pid_t    sender_pid;
    __kernel_uid32_t  sender_euid;
    binder_size_t     data_size;
    binder_size_t     offsets_size;
    union {
        struct {
            binder_uintptr_t  buffer;
            binder_uintptr_t  offsets;
        } ptr;
        __u8 buf[8];
    } data;
};

The handle is who you’re calling, just an integer meaning “the third thing I’m allowed to talk to.” The code field is which method: write an AIDL interface and each method gets a number, and that number is the exact int your generated onTransact switches on. The buffer pointer holds your marshalled bytes (a Parcel, if you’re up in Java-land), and the offsets pointer is a little array I’m going to ignore for now and come back to, because it’s the sneaky part.

Now the two fields in the middle, sender_pid and sender_euid. You get struct slots for your own PID and UID, and you never write to them. You can’t. Something else fills them in. Remember this. It turns out to be the whole answer to the spoofing question, and we’ll get there.

One copy, into a page you’re not allowed to write

Okay, the part I actually nerd out about.

Normal Unix IPC (pipes, sockets) copies your data twice. Once from your buffer into a kernel buffer, then again from that kernel buffer out to the receiver. Send a 200 KB bitmap to system_server over a socket and you’ve just done 400 KB of memcpy to move 200 KB of actual data. Every single time.

a stack of money burning
your CPU cycles, copying the same bytes twice, for the crime of using a socket

Binder just doesn’t do that. When a process first talks to the driver, libbinder maps a chunk of the driver’s memory into it, read-only. Here’s the size constant and the mmap that uses it, both from ProcessState.cpp:

#define BINDER_VM_SIZE (((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE)) * 2)

mmap(nullptr, BINDER_VM_SIZE, PROT_READ,
     MAP_PRIVATE | MAP_NORESERVE, opened.get(), 0);

PROT_READ. The receiver can look at this buffer but never write it. That threw me at first. How do you receive data into a buffer you can’t write? Then it clicked: the receiver isn’t the one writing. The kernel is. When you fire a transaction, the driver grabs a free slice of the receiver’s mapped buffer and copies your bytes straight in. Once, from your pages into theirs, inside the syscall. The receiver reads its Parcel right out of that slice and, when it’s done, hands it back with BC_FREE_BUFFER. Sender to receiver, one copy, no bounce through a middle buffer.

pipe / socket · two copies binder · one copy sender buf kernel buf(bounce) receiver buf copy #1 copy #2 sender buf receiver'smmap (PROT_READ) one copy read in place, then BC_FREE_BUFFER
Fig 2 · the kernel writes once into the receiver's read-only mapping; the receiver reads its Parcel straight out of that slice.

That buffer is a fixed size, and it’s shared by every transaction the process has in flight at the same moment. Which is the real reason TransactionTooLargeException exists, and why the “1 MB limit” everyone repeats is kind of a lie.2 The exception doesn’t fire at some fixed payload size. It fires when the driver can’t find a free slice big enough right then, because other transactions are also squatting in the buffer. So your 500 KB call works fine all through testing and then blows up in prod during a traffic burst, on a background thread, three frames deep, with a stack trace that tells you nothing useful.

the This Is Fine dog sitting in a burning room
your 800 KB Parcel, meeting three other in-flight transactions during peak load

Moving real data over Binder is a mistake, not a feature. If it’s bigger than a few KB, put it in shared memory (ashmem, AHardwareBuffer) and pass the file descriptor. Binder is for “hey, go do the thing,” not for carrying the thing.

The offsets array, and why handles can’t be forged

Remember the offsets pointer, the thing I told you to ignore? This is the sneaky part.

A Parcel isn’t always dumb bytes. You can write an actual Binder object into one, and you do it all the time. Every time you hand a service a callback interface so it can call you back later, that’s an object riding along in the Parcel. But you can’t just copy an object across a boundary; a pointer into your address space is garbage in someone else’s. So the Parcel keeps a list of offsets (“there’s an object at byte 16, another at byte 48”) and the driver walks that list, rewriting each one on the way over.

Each of those is a flat_binder_object:

struct flat_binder_object {
    struct binder_object_header hdr;
    __u32 flags;
    union {
        binder_uintptr_t binder;
        __u32            handle;
    };
    binder_uintptr_t cookie;
};

The hdr is a type tag and the union underneath is the payload: either a raw pointer or an integer handle. Which one it holds is the entire trick. In your process, that object is a real node, type BINDER_TYPE_BINDER, holding a pointer into your memory. The driver copies the parcel over and rewrites that entry to BINDER_TYPE_HANDLE, swapping your pointer for a plain integer that only means something to the receiver. It keeps two internal tables, nodes (things a process owns) and refs (handles a process holds), and refcounts them together so nobody frees an object that someone else is still holding.

process A (sender) driver process B (receiver) BINDER_TYPE_BINDER binder = 0x7fa3…c0 node ⇄ ref tables + refcounts BINDER_TYPE_HANDLE handle = 12 the driver rewrites the object mid-flight; a pointer becomes an integer B cannot invent handle 12; only the kernel hands it out
Fig 3 · a node in A becomes an opaque handle in B. The same fix-up runs for file descriptors (BINDER_TYPE_FD): the driver dup()s the fd into the receiver.

And here’s the first place the security model shows up, almost by accident. A handle isn’t an address you can guess and it isn’t a global name. It’s an index into a table the kernel keeps for your process, specifically. You can’t make up handle = 12 and reach a service nobody ever gave you, because the only way a row lands in your ref table is the kernel putting it there, and it only does that when a node genuinely got sent to you. Handles work like capabilities: you get one because someone who had it passed it to you. There’s no side door.3

But where does the first handle come from?

Chicken and egg: you can only call a handle you already have, so how do you get the first one? There’s exactly one hardcoded exception. Handle 0 is always the ServiceManager, and every process can reach handle 0 without being handed anything. It’s the phone book. Services register themselves (addService("connectivity", …)), you look one up (getService("connectivity")) and get a handle back, and from then on you talk to the service directly. ServiceManager drops out of the conversation.

Service ServiceManagerhandle 0 Client (your app) addService("connectivity", node) getService("connectivity") → handle 12 transact(handle 12, …) · SM out of the loop
Fig 4 · ServiceManager is the naming service. adb shell service list dumps everything registered with it.

If you’ve ever run adb shell service list (or dumpsys -l), congrats, you were reading this phone book. Every line is something registered under handle 0, and every line is a door you might be able to knock on. It’s genuinely the first command I run on any target. It’s the map.

So, can you spoof it?

No. And now I can actually say why instead of vibing at you.

Back to those two fields you were never allowed to fill in, sender_pid and sender_euid. When your BC_TRANSACTION lands in the driver, the driver writes them itself, from your task’s real credentials, in kernel space, where your app has exactly zero reach. There is no byte anywhere on the wire that says “I’m UID 1000, pinky promise.” The identity gets stamped by the one party in the whole exchange that actually knows who you are, and that party isn’t taking suggestions.

So when system_server does the boring classic:

if (Binder.getCallingUid() != Process.SYSTEM_UID)
    throw new SecurityException("nope");

…that int came from the kernel, not from you. getCallingUid() just reads back the sender_euid the driver stamped on your transaction. You can craft any Parcel you like, set any method code, throw around every handle you’ve got. None of it lets you lie about your UID, because you never got to write your UID down in the first place. Compare that to a “from” field an app sticks in its own payload: worth exactly nothing, believe it at your own peril.

Gandalf shouting You Shall Not Pass
the binder driver, to your app's carefully forged UID field

If you audit Android IPC, this is the one line to keep in your head: the caller UID is the only thing in the transaction you can trust. Treat every other byte as attacker-controlled. A service is exactly as safe as its getCallingUid() / permission check is early and honest.

There’s a catch, though, and it’s a real bug class, so let me ruin it for you. When a service needs to do privileged work on your behalf, it calls clearCallingIdentity(), which swaps your stamped identity for the service’s own so the downstream checks pass. It’s supposed to call restoreCallingIdentity() right after, in a finally block. Miss that restore, or clear it a line too early, and later checks in the same call see the service’s beefy UID instead of yours. Confused deputy, gift-wrapped. A clear with no matching restore is the first thing my eyes go to when I read a system service now. More often than it should be, it’s sitting right there.

If you break things for fun

Zoom out and it’s almost embarrassingly small. Every cross-process call on the device is a BC_TRANSACTION through one character device. The payload is a struct, a blob of bytes, and an offsets array the driver patches on the way through. The receiver reads it out of a page the kernel wrote once and won’t let it scribble on. And the single field you can’t forge is the one the whole permission model is built on.

Which tells you exactly where the bugs aren’t: the driver. It does its little job well. The bugs live in what the other end assumes. The Parcel it trusts too far. The getCallingUid() check that runs one line too late, or, my favorite, not at all. That gap between what the driver guarantees and what the service assumes is basically the entire game.

Want to watch it happen? strace -e ioctl any app and watch BINDER_WRITE_READ scroll past, or cat /sys/kernel/debug/binder/transactions on a rooted device to see the live ones mid-flight. Then go open drivers/android/binder.c in the kernel tree. Yeah, it’s ~6k lines. It’s not scary though, it’s just long. After this it reads like a place you’ve already been.4

And if you find a way to spoof that UID that I missed: seriously, I want to be wrong about this. Come argue with me in the Discord.

Footnotes

  1. Fun aside: Binder isn’t Google’s idea. It comes from OpenBinder, which came from Be and Palm. Dianne Hackborn wrote the OpenBinder docs and a lot of Android’s version, so if you go read the ancient OpenBinder whitepaper the vocabulary is identical: nodes, references, the “context manager.” Old tech never dies, it just gets a new /dev node. ↩

  2. The old value genuinely was 1MB - 8K, and half the answers on Stack Overflow still quote it like scripture. The current one, ((1MB - PAGE_SIZE) * 2), is bigger, but please don’t design against any of these numbers. A Binder call is for control, not cargo. ↩

  3. The same fix-up path handles BINDER_TYPE_FD: drop a file descriptor into a Parcel and the driver dup()s it into the receiver’s fd table. Super convenient, and a lovely source of bugs. A service that accepts an fd and mmaps or reads it without checking what it actually points at has been the root of real sandbox escapes. Every readFileDescriptor() sitting on a trust boundary is worth a second, suspicious look. ↩

  4. On modern Android /dev/binder isn’t one static node anymore. It’s binderfs, a tiny filesystem so each container/instance can have its own binder devices. Doesn’t change anything above; the ioctl interface is identical. It just means every time I said “one device,” it’s really “one device per binder domain.” Pedants, consider yourselves addressed. ↩

AA
Azka Ainul Maarij @lawbyte

Senior Security Consultant at Vantage Point Singapore and HTB challenge author. Organizes HackTheBox Indonesia.

Comments