DEV Community

engnzdl
engnzdl

Posted on

How apps spot an Android emulator (part 1): build props and kernel tells

How apps spot an Android emulator (part 1): build props and kernel tells

I spent a few days this month poking at a stock Android emulator to answer one question: what actually gives it away? Not the obvious stuff everyone lists, but the signals an app reads when it decides "this is not a phone". This is part one of what I found. It covers the two layers you can touch from userspace and the one you can't. Part two covers hardware and behavior.

Quick context on why this matters at all. Emulators are used for legit test automation and for bot farms that install an app thousands of times to farm bonuses. The app's defense side wants to separate a real user on a phone from a VM in a rack. Everything below is that detection, looked at from the inside.

The props layer: a file you can edit

Android exposes device identity through Build.* and the ro.* system properties. Most of them live in one file, build.prop. A stock emulator reports this:

$ adb shell getprop ro.build.product
emulator64_arm64

$ adb shell getprop ro.product.model
sdk_gphone64_arm64

$ adb shell getprop ro.build.fingerprint
google/sdk_gphone64_arm64/emu64xa:12/SGR1.210910.001/7793123:userdebug/dev-keys
Enter fullscreen mode Exit fullscreen mode

Two things jump out. The model name sdk_gphone64_arm64 is not a device anyone has ever bought. And the fingerprint ends in userdebug/dev-keys, which is a build signing that does not ship to consumers. Every real phone in the wild runs user builds with release-keys. Those two strings alone are enough for a cheap first filter.

Here's what this layer actually is. An app reads these values with SystemProperties.get(), which is a string lookup into a shared memory region that init fills at boot from build.prop. It is a file, loaded into memory, read back as strings. With root you can rewrite any of it at runtime and it will happily lie until the next reboot.

I changed it the persistent way: boot the emulator with -writable-system, remount the system partition read-write, edit build.prop, reboot. The new values stuck. ro.build.product went from emulator64_arm64 to a real device's product name and stayed that way.

One flag is worth knowing about. The emulator documents -prop as a way to set properties at boot, and it worked on older versions. On recent versions it's dead: it sets nothing. The reliable path is editing build.prop directly, which is what I used for the rest of this.

So the props layer is fakeable, and everyone knows it. An app that checks only Build.MODEL is catching the VMs whose owners did not bother. That is the point of this layer in a detection stack: cheap, catches the sloppy cases, nothing more.

The kernel layer: the part that does not lie

The second layer is where it gets harder. These values do not come from a file you can edit. They come from the kernel and its boot command line.

$ adb shell getprop ro.kernel.qemu
1

$ adb shell cat /proc/cmdline
... androidboot.hardware=ranchu androidboot.hardware.platform=ranchu ... qemu=1 ...

$ adb shell grep -m1 "Hardware" /proc/cpuinfo
Hardware : goldfish
Enter fullscreen mode Exit fullscreen mode

Three signals pointing the same direction. ro.kernel.qemu is set from a qemu=1 flag on the kernel command line when QEMU boots the guest. /proc/cpuinfo reports goldfish, the codename of the virtual platform the Android emulator has used for years. The kernel itself is a ranchu build, a fork of goldfish compiled for QEMU's virtual hardware, not for any shipping SoC.

I wanted to know whether these could be faked the way props can, so I tried. First I patched build.prop to change ro.hardware from ranchu to a real device's hardware name, rebooted, and checked. Still ranchu. Then I passed androidboot.hardware=... on the QEMU command line to try to override it at boot. Still ranchu.

The reason is that ro.hardware is not a build.prop key. init derives it at boot from androidboot.hardware on the kernel command line, and QEMU sets that when it starts the guest. My edit was ignored because the key I edited was not the key that set the value. To change it you need a custom kernel or a custom ROM, one recompiled with a different hardware identity or one that does not advertise QEMU at all. That is a build system problem, not a config tweak.

ro.board.platform behaved the same way, but for a different reason. I edited it in build.prop and it stayed empty after boot. That one is restricted by SELinux property contexts, so init refuses to accept the value from that file.

The kernel layer is the line. A spoofed build.prop gets caught here because the kernel keeps telling the truth. /proc/cpuinfo is the kernel reporting its own identity, and a userspace process asking for it gets the kernel's answer. This is what a serious detector checks after the props pass, and it is why the props layer alone was never going to be enough.

Where that leaves the first two layers

Props are cosmetic. They are rewritable and any detector worth the name treats them as such. The kernel layer is the real first filter, and it is where a VM leaves marks it cannot edit away from userspace without a custom ROM.

That split matters more than it sounds like it should. These signals look like the same kind of thing from the outside, just strings you could rewrite. They are not. Some of them come from a file, some of them come from the kernel, and the difference determines what you have to build if you want them to say something else.

Next part: the hardware and behavior layer. Sensors, telephony, the GPU renderer string, and why faking those convincingly means reimplementing drivers instead of editing files.

I have been measuring all of this while building enmulator, an open source Android emulator manager in Tauri 2 + Rust.

Educational security research. No working bypass, keys, or implementation details published.

If you build device detection, or you run automation that keeps getting flagged, this is the kind of problem I work on. DM me on X or reach me at enginozdal.com.

Top comments (0)