Building a True Per-App Android Device Spoofer

In the realm of Android applications, the concept of “device spoofing” has gained traction, particularly among developers seeking to manipulate device information for various purposes. Most existing device spoofer applications operate within a limited framework, primarily altering the android.os.Build attributes. However, these modifications often fall short when it comes to native system API calls, which can still retrieve the genuine device information. Enter Tweaks, a sophisticated per-app spoofer integrated into the Android Open Source Project (AOSP) and LineageOS, designed to elevate the art of spoofing to a new level.

Why the property area, not Build.*?

At the heart of Android’s architecture lies a set of memory-mapped files located under /dev/properties, where every system property is stored. These files represent a serialized bionic prop_area, forming a trie of properties that share a common SELinux context. Fields such as Build.MODEL and Build.FINGERPRINT are merely constants derived from properties like ro.product.model and ro.build.fingerprint at process initiation. Both native code and executed children access these mapped files directly.

By altering what a process perceives under /dev/properties, one can effectively change all derived properties. The brilliance of Tweaks lies in its ability to apply these changes on a per-app basis. Utilizing Zygote’s private mount namespace, Tweaks can bind-mount a modified copy of a property-context file over the original, ensuring that only the targeted application receives the spoofed values while the rest of the system remains unaffected.

Shape of the system

Tweaks app (platform-signed)
  | binder: ITweaksManager.setConfig(packageName, config)
  v
TweaksManagerService (system_server)
  | writes /data/system/tweaks/packageName.json
  v
(next launch of packageName)
ProcessList.startProcess                            (system_server)
  | TweaksLocalService.getOverrideDirForPkg(packageName)
  |  -> resolves /dev/properties/pa.      (generate if missing)
  | passes --sysprop-override-dir= to Zygote
  v
Zygote child
  | OverlaySyspropOverrideFiles                     (bind-mount each patched file over its live counterpart)
  | systempropertieszygote_reload()             (re-map the prop area)
  | ReloadBuildJavaConstants()                      (re-read Build.* and re-derive Build.FINGERPRINT)
  v
app reads spoofed values; global namespace untouched

The control mechanism operates through the binder, with only the platform-signed Tweaks app permitted to initiate calls. The read mechanism is embedded within the spawn path, avoiding inter-process communication. Since ProcessList runs within system_server, it accesses the configuration via an in-process facade, TweaksLocalService. The patched property area files are generated by propgen, ensuring a streamlined process.

Patching a prop_info in place

The core functionality resides in propgen.cpp, which takes a configuration’s property overrides and produces the necessary property context files carrying spoofed values. The initial task involves identifying the location of a property, facilitated by a bionic property_info trie that maps property names to their SELinux context strings. By categorizing properties by context, propgen minimizes the number of context files it needs to access, thereby enhancing efficiency.

// GetPropertyInfo(char* prop, char context, char type)
info->GetPropertyInfo(o.first.c_str(), &context, &type);
bycontext[context].pushback(o);

Once the properties are organized by context, the patching process begins. A bionic prop_info has a defined structure, allowing propgen to locate the property, overwrite its value, and update the associated serial number accordingly. This in-place editing approach eliminates the need for complex trie surgery, as the Build.* properties are consistently present.

The TweaksManagerService: config, content-addressing, and gating

The TweaksManagerService within system_server governs the control side of the operation. The generated property areas are content-addressed, meaning their directory names are derived from a hash of the override set:

File areaDirFor(TweaksOverrideConfig config) {
  return new File("/dev/properties", "pa." + hashOverrides(config.properties));
}

This design allows multiple apps with identical overrides to share a single area in tmpfs, which is ephemeral and regenerated upon the next launch. The service ensures that all interactions with TweaksManagerService are securely gated, permitting only the Tweaks app to read or write configurations, thus maintaining the integrity of the spoofing process.

The spawn path

Bridging the control and read planes, the spawn path in ProcessList.startProcess determines whether to apply the spoofing:

final TweaksLocalService tweaks = LocalServices.getService(TweaksLocalService.class);
String syspropOverrideDir =
        (tweaks != null) tweaks.getOverrideDirForPackage(app.info.packageName) : null;
// ... passed through Zygote.start(..., syspropOverrideDir);

The getOverrideDirForPackage function evaluates several conditions to decide whether to enable spoofing for a specific app. If the conditions are met, it returns the appropriate area directory, which is then passed to Zygote during the app’s initialization.

sepolicy

Security is paramount in this architecture, hinging on SELinux’s permissiveness aligning with the design’s requirements. Each property eligible for spoofing is attributed to tweaksspoofableprop, with rules crafted specifically for this attribute:

// GetPropertyInfo(char* prop, char context, char type)
info->GetPropertyInfo(o.first.c_str(), &context, &type);
bycontext[context].pushback(o);

This meticulous approach ensures that propgen operates within a constrained environment, capable only of reading and writing designated properties, thereby minimizing potential security risks.

Membership is generated, not hand-written

The process of attributing properties is automated through a Python script that queries the build for SELinux policy directories, ensuring that only properties not restricted by neverallow rules are included.

The ordering issue

In the context of AOSP builds, the order of declarations in SELinux policies is critical. Generated attribution files must be positioned correctly to avoid build errors, necessitating careful management of the build configuration.

What this does NOT hide

Despite its capabilities, there are limitations to what Tweaks can conceal. The following elements still reveal the authentic device information:

  • /proc/self/mountinfo displays the bind mounts over /dev/properties, which are absent in stock configurations.
  • Using getfilecon on an overlaid file returns tweaksproparea, rather than the genuine property context.
  • A direct read of /system/build.prop continues to yield real values, as the overlay only affects the property area.

The final result

This endeavor has proven to be an enriching experiment, showcasing the potential of Tweaks in the landscape of Android development. The patches for frameworks/base are accessible at itsaky/androidframeworksbase, while the source code for the Tweaks app and propgen, including SELinux policies, can be found at itsaky/androidvendoritsaky. A demonstration video illustrates the seamless integration of system property spoofing, even at the getprop layer, within a terminal emulator app.

AppWizard
Building a True Per-App Android Device Spoofer