~/posts/mobile/android-emulator-kernel-structure-location.md

Android emulators: task lists and kernel relocation anchors

Follow task discovery, SELinux state differences, and cold-boot relocation in an Android 11 x86_64 AVD. Separate configuration-dependent layouts, structural evidence, arithmetic models, and runtime validation.

date[31:24]
read[23:16]
8 min
cat[15:8]
Mobile
Contents
  1. 0x00Debug access and kernel objects are separate layers
  2. 0x01Validate an embedded list from a comm candidate
  3. 0x02Choose a reference object rather than memorize an address
  4. 0x03Express SELinux state changes as set constraints
  5. 0x04Evaluate relocation anchors across cold boots
  6. 0x05Separate arithmetic checks from runtime validation
  7. 0x06References

Adapting Emuroot to an emulator is first a structure-discovery problem. The host already has access to guest memory through QEMU's GDB stub; the remaining work is to identify tasks, credentials, and SELinux state correctly. That premise differs from finding a kernel vulnerability reachable by an ordinary Android app, and does not establish a general privilege-escalation path on physical phones.

The case focuses on a 2021 Google Play AVD running Android 11, API 30, x86_64, with a Linux 5.4-series kernel. It connects task-name searches, doubly linked lists, state differences, and relationships across cold boots. The case's numeric values are retained for checking those relationships, not as fixed offsets for other system images.

Debug access and kernel objects are separate layers

The historical host-side entry used the emulator's -qemu -s option and a GDB connection to local port 1234. -qemu forwards arguments to the underlying engine. The stub grants debugging access outside the guest kernel's policy boundary. QEMU's documentation explains that access and the responsibility to protect the connection; its reachability should be constrained in a test environment.

After establishing debug access, at least these object relationships still need verification:

Structure relationships to verify independently4 rows
Object or field Purpose Cross-check
task_struct.tasks Traverse the task list Reciprocal links and bounded traversal back to the start
task_struct.pid, comm Identify the target task PID matches the observed process; name is supporting evidence
task_struct.cred, real_cred Locate credentials Pointer targets, identity fields, and version-specific layout
selinux_state.enforcing Observe global enforcement mode Configuration, field width, and a round-trip state change

The Linux credential documentation distinguishes UID/GID, filesystem identity, capability sets, and LSM information. Changing one UID does not describe the complete permission result. Credentials also have reference-counting and copy-and-replace rules; a debugger's direct write is not the kernel's normal credential-update interface.

Field layouts depend on the ABI, configuration, and compiled image. Even familiar structures such as cred require configuration-aware layout checks before assigning offsets.

Validate an embedded list from a comm candidate

A short, controlled task name can provide a starting point, such as the illustrative name probe_shell. Linux 5.4 defines TASK_COMM_LEN as 16, including space for the terminating null. A string match can still come from a log or userspace buffer. Structural relationships turn it into a credible task candidate.

The tasks member is a list_head embedded in task_struct. Its next and prev point to neighboring list nodes, not the starts of neighboring task structures. This diagram shows the relationship between two nodes, not the full task population or its actual insertion order.

flowchart LR
  A["task A / tasks"] -->|next| B["task B / tasks"]
  B -->|prev| A

The case places a candidate comm at C = 0xffffa30441257310 and candidate tasks at T = 0xffffa30441257048. Their separation is 0x2c8. After reading the node pointer in T.next, adding that same separation gives a candidate comm in the neighboring task:

Δ=C−T,Cnext=next⁡(T)+Δ\Delta=C-T,\qquad C_{\mathrm{next}}=\operatorname{next}(T)+\Delta

Plausible names such as kworker/3:0 and sh support the hypothesis more strongly than a pointer beginning with 0xffff. Reciprocal next->prev and prev->next links, PID correspondence, and list closure still need checking. Alignment, high addresses, and printable strings are filters, not proofs.

Tasks may exit and links may change while the kernel runs. An inconsistent observation can mean either a bad offset or an inconsistent sample. Distinguish them using a controlled snapshot or paused state, and impose a node limit on traversal.

Choose a reference object rather than memorize an address

PID 1 has a stable identity, not a stable task-structure address. The initial task associated with swapper/0 is a more useful image-resident reference. It avoids some variability from ordinary task allocation, but still moves with kernel address-space layout randomization (KASLR).

Handle three kinds of change separately3 rows
Source of change Symptom Evidence to retain
Dynamic task allocation Similar processes occupy different addresses PID, task fields, and list relationships
Image randomization for one build Multiple image-resident objects move together Difference between an anchor value and an object
Kernel or configuration change Relative layouts can also change Image identity, ABI, configuration, and revalidated offsets

Select the target primarily by PID and use comm as corroboration. Multiple sh processes make the name ambiguous. Do not hardcode one list direction as always finding the newest task first; establish the actual insertion and linkage behavior.

The historical adaptation covered x86 and x86_64 Google Play AVDs for Android 9, 10, and 11. That is evidence of work across multiple images, not shared layouts. Each image needs its own location parameters and validation results.

Express SELinux state changes as set constraints

Check that the field exists before selecting a search width. Linux 5.4's selinux_state includes bool enforcing under CONFIG_SECURITY_SELINUX_DEVELOP. With that option absent, the accessor returns enforcement enabled directly. A switchable byte is therefore not a valid assumption for every 5.4 kernel.

On a reference AVD with matching build and configuration that supports normal mode changes, define these sets over the same address range using one-byte reads:

Address sets from three searches3 rows
Set State and matching value Count in the case
A Value 1 while Enforcing 45,624
B Still 1 while Permissive 45,518
C Value 0 in the same Permissive state 2,002,519

First exclude addresses that remain one, then require a confirmed zero:

A′=A∖BA'=A\setminus B
candidates=A′∩C\mathrm{candidates}=A'\cap C

The two operations serve different purposes. Difference removes unchanged entries; intersection excludes failed reads, missing coverage, or values that changed to something else. If memory remains active, derive B and C from the same paused state where possible so that changes between searches do not contaminate the result.

The case yielded about 50 candidates. Returning to Enforcing and retaining entries that became one again reduced that to two. A recorded one-byte intervention followed by a semantic check with getenforce identified 0xffffffffb4718b19. This is stronger causal evidence than a single correlated transition, but it remains specific to that build and boot. Restore the previous value after intervention.

The address contains 16 hexadecimal digits. Address width also belongs in debugger-command validation: duplicating a leading ffff produces a value wider than 64 bits.

Evaluate relocation anchors across cold boots

Disabling KASLR changes the experiment. To retain randomization, recover the current load position rather than reuse an address from the previous boot.

The case searches near x86_64 cpu_entry_area for values that correlate with the randomized kernel image, reading eight-byte words at four-byte intervals. Overlapping reads can combine split descriptor fields into values that resemble pointers. This is an empirical anchor search, not a demonstrated type interpretation for every hit.

Let P be a fixed read location. On the first boot it yields V1, while the relevant swapper/0 field has independently been located at S1. Save D = S1 - V1. After a cold boot, read V2 at the same location and predict S2 = V2 + D.

Relocation relationship in the case5 rows
Quantity Value
V1 0xffffffffb2448e00
S1 0xffffffffb3028310
D 0xbdf510, or 12,449,040
V2 0xffffffffa2448e00
Predicted S2 0xffffffffa3028310

Correct arithmetic is only the first check. The predicted location must still pass task-name, PID, and list validation. Repeated agreement across cold boots can justify retaining the difference for that image. The exact anchor location P and image hash are not supplied in this case, so these numbers do not form a directly reusable location profile.

The SELinux state object can be related to a verified kernel reference in the same way. An image update, configuration change, or different entry-area layout requires revalidation rather than inherited offsets.

Separate arithmetic checks from runtime validation

The model below checks set operations, the reverse state transition, the comm separation, and the relocation formula. The small set addresses are explicitly illustrative. The script neither connects to an emulator nor reads or writes kernel memory.

emuroot_layout_model.pypython
def changed_to_zero(a, b, c):
    return (a - b) & c

# Illustrative address sets; not guest-memory observations.
a = {0x1000, 0x1001, 0x1002, 0x1003}
b = {0x1000, 0x2000}
c = {0x1001, 0x1002}
assert changed_to_zero(a, b, c) == {0x1001, 0x1002}
assert len(a - b) != len(a) - len(b)
back_to_one = {0x1002}
assert changed_to_zero(a, b, c) & back_to_one == {0x1002}

task_link = 0xffffa30441257048
comm = 0xffffa30441257310
assert comm - task_link == 0x2c8

v1 = 0xffffffffb2448e00
s1 = 0xffffffffb3028310
v2 = 0xffffffffa2448e00
delta = s1 - v1
assert delta == 12449040 == 0xbdf510
assert v2 + delta == 0xffffffffa3028310
assert len("probe_shell".encode("ascii")) < 16
print("PASS: candidate sets; reverse transition; comm delta=0x2c8")
print("PASS: relocation delta=0xbdf510; predicted=0xffffffffa3028310")
Actual location-model output
$ python emuroot_layout_model.py
PASS: candidate sets; reverse transition; comm delta=0x2c8
PASS: relocation delta=0xbdf510; predicted=0xffffffffa3028310

Runtime validation must additionally establish that the right objects were selected, that permission behavior matches expectations, and that the experiment's state was restored.

Checks to retain from a runtime test3 steps
  1. 1

    Structural consistency

    Record the image version, configuration, task PID, list closure, and credential relationships. A single string or pointer shape does not replace them.

  2. 2

    Semantic consistency

    Record process identity, capabilities, controlled-operation results, and SELinux observations separately. A successful GDB write establishes only that the write completed.

  3. 3

    Restoration and retesting

    Restore the previous state and validate location relationships after a cold boot. Distinguish snapshot restoration from a cold boot that actually rerandomizes the image.

The reusable result is a conditional discovery process: names suggest candidates, list invariants establish object identity, state differences narrow variable sets, and cold boots test relocation relationships. Fixed addresses are outputs of one observation, not the interface to depend on.

References

NORMAL~/posts/mobile/android-emulator-kernel-structure-location.md§--
0%en