Assessing the Forensic Viability of Android Memory Analysis Across Production Builds: A Cross-Version Study of Security Hardening and Structure Preservation


Abstract

Android memory forensics recovers evidence that never touches disk: decrypted messages, session credentials, and the live internal state of a running application. The tools that perform this recovery depend on debug symbols embedded in libart.so, the Android Runtime library, to locate data structures and interpret their layout. Across recent releases Google has stripped most of that information from the binaries that ship on consumer phones as part of a broader security hardening effort, yet no prior work has measured how far the stripping has progressed on the devices examiners actually encounter, or whether the memory architecture beneath the stripped surface still resembles what the forensics literature describes. This paper measures the gap between the unstripped development builds researchers use and the production builds that ship on real hardware, using binaries extracted from Pixel factory images across Android 8 through Android 15. Static symbols fell from 20,495 entries to zero, dynamic symbols dropped by roughly 60 percent, and source file references disappeared entirely. A compressed fallback section in the Android 15 binary restores thousands of function names but carries no structure layouts. Source code review and memory map comparisons across Android 8 and 15 show that the heap spaces, garbage collector infrastructure, and allocation bitmaps remain structurally intact, with visible changes limited to naming and arithmetic optimization. Live validation on a rooted, fully stripped Pixel 7 confirms that the runtime entry point is still locatable through the dynamic symbol table, and that structural offsets pulled from a version-matched development build resolve to valid pointers inside production memory. The results suggest that forensic tool compatibility with modern Android remains achievable, though it now takes considerably more manual effort than before.

Android forensics, memory forensics, ART runtime, symbol stripping, libart.so, digital forensics

1 Introduction↩︎

Roughly three out of four smartphones in active use run Android [1]. That share is what makes memory forensics, the analysis of a device’s volatile memory rather than its persistent storage, central to mobile investigations. A memory capture can hold artifacts that never reach disk at all: decrypted chat content, session keys, and the live internal state of whatever application was running when the phone was seized. A disk image cannot recover any of it; that data exists only briefly, in RAM.

Android does not hold still, though. Google ships a new major version every year, and each release can change how the runtime executes applications, manages memory, and, most relevant here, how much debugging information survives into the binaries that reach consumer devices. Over the last several releases Google has steadily stripped debug symbols from production binaries as part of a broader hardening strategy: those symbols tell a forensic tool what a function does and where a data structure lives, and removing them also raises the cost of reverse engineering the binary, since a full symbol table doubles as a blueprint for return-oriented and jump-oriented exploitation [2].

At the center of this problem is libart.so, the shared library implementing the Android Runtime (ART). Engineering and userdebug builds ship it with a complete symbol table, full DWARF debug information, and source file references, the version researchers typically work with. Consumer devices ship something else: a production build that has been through a stripping process designed to remove nearly all of it.

Most published Android memory forensics research was built and tested against that unstripped version, on binaries with full debug information and development builds where each structure still carries a name. No phone that reaches an examiner’s desk runs that build, though. The most directly relevant prior work, Babangida et al. [3], tracked 34 internal ART structures across Android 9 through 14 and found that 73.2 percent of structure member offsets changed between versions, concluding that offset-based forensic methods face a mounting maintenance burden. That result is well supported by their data, but the entire analysis relied on engineering builds with full DWARF symbols: it measures how far structures drift from version to version while assuming the debug information needed to find those structures in the first place will always be there. That assumption does not hold on a real device.

That leaves two open questions, which this paper addresses directly.

RQ1: To what extent does Google’s security hardening of production Android binaries limit the debug symbols available for memory forensics, and what forensic capability remains despite that hardening?

RQ2: Are the core memory management structures in ART architecturally preserved across Android versions despite this hardening, and does that preservation allow same-version forensic tool compatibility across build types?

Answering them required four things: quantifying symbol removal in production libart.so binaries across versions, examining the AOSP source and git history for evidence of structural change, comparing an unstripped and a production build of the same release to isolate the effect of stripping on its own, and validating the findings live on a rooted, fully stripped Pixel 7. What follows is the first concrete measurement of the forensic information gap on a real production device, evidence that the memory architecture beneath the stripped surface is intact, and a working procedure for recovering the ART Runtime object on a stripped Android 15 build from offsets carried over from a version-matched development build.

2 Background and Related Work↩︎

2.1 ART and Android Memory Management↩︎

Since Android 5.0, applications run on the Android Runtime rather than the older Dalvik VM [4], compiling DEX bytecode into native code ahead of time, just in time, or through a mix of both depending on the release [5]. ART gives each process its own heap, split into specialized regions, typically a main space, zygote space, and non-moving space, plus a separate large object space for allocations above roughly 12KB [6], [7]. Live and dead objects are tracked through mark and live bitmaps, modified pages through a card table, and collection coordination through allocation and live stacks.

The collector itself has changed more than once. Android 8 introduced the Concurrent Copying (CC) collector as the default, replacing the Concurrent Mark-Sweep collector that ran through Android 7 [6], allocating within fixed-size regions on a structure called RegionSpace and evacuating regions once their live object count drops low enough. Android 13 later introduced a userfaultfd-based Concurrent Mark-Compact collector that removes CC’s read barrier [8], [9], which is the default by Android 15 on supported devices, with CC retained as a fallback.

Small object allocation under CC does not go through the older Runs-of-Slots Allocator (RosAlloc), an assumption in some earlier forensics literature, but through a bump-pointer allocator called RegionTLAB that gives each thread its own allocation buffer inside a RegionSpace region. RosAlloc remains in the codebase, but since Android 8 its role has narrowed to backing the non-moving space, where JNI-pinned objects live [10]. This matters for forensic technique: methods built around RosAlloc’s deterministic first-available-slot behavior [11] cannot assume that behavior describes the allocator actually handling most allocations on a modern device. All of this, for a forensic tool, is reached through libart.so: whatever information that binary retains determines whether the tool can locate the runtime instance, walk the heap, and enumerate live objects.

2.2 Android Userland Memory Forensics Tools↩︎

Acquisition of Android memory is itself non-trivial. LiME [12], a loadable kernel module, remains the standard for kernel-level acquisition, but it requires root and a module compiled against the exact kernel on the target, often impractical on a locked-bootloader device with a recent kernel update [13], [14]. Userland alternatives, reading memory through /proc/[pid]/mem on rooted devices or through dynamic instrumentation with Frida [15], sacrifice full physical coverage but are far easier to deploy, and for ART-focused work they capture what actually matters.

Several tools built on this userland approach recover ART-managed objects specifically. DroidScraper [6], presented at RAID 2019, locates the Runtime instance through a static symbol lookup in libart.so and traverses the heap to recover roughly 90 percent of live objects; it targeted Android 8, 8.1, and 9, and depends on the nm utility finding the mangled symbol _ZN3art7Runtime9instance_E in the static symbol table, an assumption that held for those versions but not for a stripped Android 15 binary. Timeliner [11] reconstructs the order in which a user opened different app screens from the spatial ordering that first-available-slot allocators tend to produce, though it still needs to locate Activity-related structures through the same kind of symbol information. AmpleDroid [7] extends DroidScraper to the Large Object Space, and RetroScope [16] reconstructs prior app screens from recovered GUI elements. Sudhakaran et al. [17] separately found that garbage collection timing and process foreground state at acquisition significantly affect how complete the recovered data turns out to be. Each of these tools was built and tested on Android 8 or 9 binaries with full or near-full symbol tables, and none of them addressed what happens once that information is stripped away.

2.3 Security Hardening and Symbol Stripping↩︎

Google’s hardening measures span kernel-level protections such as ASLR, stack canaries, and Control Flow Integrity [18][20], but the change most relevant here operates at the binary level. Development builds ship libart.so with a full .symtab, DWARF debug sections [21], a .strtab, and source file references, and production builds strip nearly all of that away. Two things survive: the .dynsym section, since the runtime linker needs it, and a compressed .gnu_debugdata section [22], also called MiniDebugInfo, which embeds a minimal ELF containing function-level symbols for crash reporting.

The APEX module system introduced in Android 10 [23] adds a further wrinkle: ART is now packaged as an independently updatable module, and libart.so moved from /system/lib64/ to /apex/com.android.art/lib64/, meaning it can be updated on its own schedule, separately from the base operating system.

2.4 Cross-Version Runtime Evolution↩︎

The study most directly relevant to this paper is Babangida et al. [3], which tracked 34 ART data structures across Android 9 through 14 on four architectures, extracting DWARF symbols with a custom parser built on pyelftools [24]. Their DWARF symbol counts grew from roughly 15,700 on Android 9 to about 19,100 on Android 14, and across the 34 structures tracked, 73.2 percent of members had their offsets change in at least two versions, alongside a DWARF version bump at Android 13 and a compression shift from zlib to zstd. Their conclusion, that symbol-based forensics faces a growing maintenance burden, is well supported by their data, but every binary in that study carried full DWARF information: the reported counts describe what a developer compiling AOSP with debug flags sees, not what ships to a customer. Once the symbols are gone, the relevant question is no longer whether an offset shifted between versions, but whether the structure can be located at all. That is a different problem, and, this paper argues, a prior one to the problem they measured.

Android’s build documentation defines three variants [25]: eng, for day-to-day development with full symbols and relaxed security; userdebug, a middle configuration for pre-release evaluation; and user, what ships to retail devices, hardened and stripped. The eng or userdebug binaries used to validate all of the tools discussed above are not what ships on a retail device, and none of that prior work was tested against the user variant, the one an investigator actually finds on a seized phone. This paper targets that gap directly, quantifying what production stripping removes and testing whether the architecture underneath remains navigable once it has.

3 Methodology↩︎

The investigation proceeded in four phases: quantifying symbol availability in production binaries, examining the AOSP source and git history for structural change, isolating the effect of stripping by comparing an unstripped and a production build of the same release, and validating the findings empirically on live device memory.

3.1 Equipment and Environment↩︎

The primary device was a Google Pixel 7 ("Panther") running Android 15, build BP1A.250505.005.B1, rooted with Magisk [26] by patching the init_boot partition rather than boot, a partition-layout change Android 13 introduced. Root access was needed to read process memory maps through /proc and to pull binaries out of APEX modules. The earlier endpoint used an Android 8.0 (Oreo, API 26) emulator configured in Android Studio on a Pixel 2 XL, x86_64 system image; although the architecture differs from the ARM64 Pixel 7, ART’s memory management code and naming conventions are architecture-independent, so heap layout does not vary by target. All binary analysis ran on a Debian workstation using standard ELF utilities, nm, readelf, objcopy (the aarch64-linux-gnu cross variant), file, and objdump, with no custom tooling required.

3.2 Phase 1: Production Binary Symbol Analysis↩︎

Two versions bracket the stripping timeline: Android 8.0, Pixel 2 XL ("Taimen"), build OPD1.170816.010, the earliest version running ART’s Concurrent Copying collector and DroidScraper’s original target, and Android 15, Pixel 7 ("Panther"), build BP1A.250505.005.B1, the current production build on the test device and the version at which Android begins its transition to 16KB memory pages. The Android 15 binary was pulled directly from the device with adb pull /apex/com.android.art/lib64/libart.so; the Android 8 binary came from Google’s public factory image archive [27], extracted by converting the sparse system.img to a raw image with simg2img, mounting it, and, since ART ships inside an APEX module from Android 10 onward, unzipping com.android.art.apex and mounting the payload image before copying the library out of lib64/.

Each extracted binary went through four checks: file to classify it as stripped or not, nm to count static symbol table entries (zero on a stripped binary, since .symtab is gone), nm -D to count the dynamic symbols that survive stripping because the runtime linker needs them, and readelf -S to enumerate every ELF section present, including whether .symtab, .strtab, any .debug_* section, or .gnu_debugdata exists. The Android 15 binary also carried a .gnu_debugdata section, extracted and decompressed separately with objcopy --dump-section followed by xz -d, then inspected with readelf -s to characterize exactly what that fallback mechanism provides.

3.3 Phase 2: Source Code and Git History Analysis↩︎

This phase moved from compiled binaries to the AOSP source tree itself [28], examining art/runtime/gc/heap.h and heap.cc, the RosAlloc allocator’s rosalloc.h and rosalloc.cc, the page-size handling code in bionic, and the Runtime class definition in runtime.h. Git history for the RosAlloc allocator specifically was traced to identify what had changed between Android 8 and the current codebase, and each modified call site was checked for whether the change touched a data structure or allocation algorithm, as opposed to a purely arithmetic optimization.

3.4 Phase 3: Unstripped Build vs. Production Build Comparison↩︎

Compiling AOSP from source with the eng flag was attempted first but failed for lack of memory on the available hardware. Instead, an unstripped symbols package was pulled from Google’s Continuous Integration infrastructure at ci.android.com [29]: branch aosp-android-latest-release, target aosp_cf_arm64_only_phone-userdebug, build ID 14981173. This is a userdebug build, not an eng target, but the distinction does not matter here, since both retain full DWARF information, complete static symbol tables, and source file references, exactly what this phase measures. The package targets a Cuttlefish virtual device instead of a Pixel 7, but libart.so compiles from the same device-independent source tree regardless of target, a claim the high dynamic symbol overlap reported in Section 4.3 supports directly.

The production libart.so from the Pixel 7 and the CI build’s libart.so were compared with the same tools used in Phase 1, plus a dynamic symbol overlap check: extracting the sorted, unique dynamic symbol names from each binary with nm -D and comparing them with comm to count symbols shared between the two builds versus symbols unique to each. A high overlap would confirm that the two binaries share a common codebase, and that the difference between them comes down to stripping, not source divergence.

3.5 Phase 4: Empirical Cross-Version Validation↩︎

The final phase tested whether the architectural preservation suggested by Phases 2 and 3 held up under direct observation, through three activities: a memory map comparison, a baseline replication of the original DroidScraper recovery on Android 8, and a live recovery attempt on the stripped Pixel 7.

For the memory map comparison, process memory maps for system_server (chosen because it is a long-running process exercising the full range of ART’s memory management) were pulled from /proc/[pid]/maps on both the Android 8 emulator and the Pixel 7, filtered for dalvik- and rosalloc-related entries, and compared by hand against each other. An earlier attempt to acquire full physical memory with LiME on the emulator failed because the available goldfish kernel source corresponded to a newer branch (4.4-dev) than the one the API 26 emulator actually ran, so /proc-based extraction was used instead.

Before attempting anything on Android 15, the original DroidScraper procedure [6] was replicated on Android 8 using GDB to locate the Runtime instance symbol and read the Runtime pointer from live memory, confirming the baseline recovery chain functions as documented when full symbols are present.

The core validation used Frida [15] to repeat that recovery on the Pixel 7. Because _ZN3art7Runtime9instance_E is a GLOBAL PROTECTED export required for inter-library linking, it survives stripping and remains in the dynamic symbol table: Frida’s Process.getModuleByName() supplied the base address of libart.so at runtime, the known symbol offset located the instance pointer, and reading that address (process ID 32359) yielded the Runtime object itself. With the object located, DWARF field offsets for heap_ and thread_list_ were pulled from the unstripped CI build with readelf -wi and applied directly to it; walking one level further, the offset for bump_pointer_space_ was applied to the recovered Heap object. A matching internal layout would resolve every one of these reads to a valid pointer; a diverged layout would return garbage or unmapped addresses instead.

Three further complications specific to modern Android were documented along the way: pointers on the Pixel 7 carry Memory Tagging Extension (MTE) tags in their upper bits [30][32] that must be stripped before an address can be used for traversal; SELinux policy blocks direct /proc/pid/mem reads even with root [33], [34], which is why the recovery used Frida’s in-process injection instead of an out-of-process read; and the APEX relocation of libart.so changes where a tool needs to look for the binary on disk. The full command history for all four phases, including the Frida and GDB scripts, is maintained in a public repository for reproducibility.

4 Results↩︎

4.1 Symbol Availability Across Production Builds↩︎

Table 1 summarizes the symbol counts extracted from production libart.so binaries at each endpoint.

Table 1: Symbol Counts in Production libart.so Binaries
Metric Android 8 Android 15
Static symbols (nm) 20,495 0
Dynamic symbols (nm -D) 6,577 2,614
readelf --syms total 27,395 2,618
Binary status (file) not stripped stripped
Source file references 3,594 0
.symtab present yes no
.strtab present yes no
.debug_* sections 0 0
.gnu_debugdata present no yes

Static symbols collapsed from 20,495 on Android 8 to zero on Android 15. Android 8’s libart.so is classified by file as not stripped; Android 15’s is classified as stripped. Dynamic symbols, which cannot be removed without breaking runtime linking, still dropped by roughly 60 percent, from 6,577 (nm -D) to 2,614 (2,618 by readelf’s count; the four-entry difference comes from how each tool handles null and special entries). Source file references disappeared entirely, from 3,594 down to zero, and neither production build examined carried any DWARF debug section. Because only these two endpoints were analyzed at the factory-image level, the exact version at which complete static stripping began cannot be pinned down from this data alone; other manufacturers may follow a different timeline.

Android 15’s binary does retain a .gnu_debugdata section. Decompressed, it yields 19,287 symbol entries, about seven times the 2,618 dynamic symbols in the main table, but every entry is a bare FUNC symbol: a name and an address, nothing else. There are no type definitions, no structure member offsets, and no source mappings. That is enough to label which function occupies a given address range for a crash report, but not enough to tell a forensic tool where the Heap or Thread fields sit inside a recovered Runtime object.

4.2 Source Code Evolution↩︎

Git history for rosalloc.cc traces to a single commit, dd98d26e9b ("Optimize division by / modulo of gPageSize," authored by Richard Neill at ARM), as the primary change to that file between Android 8 and the current tree. The commit replaced direct division and modulo operations against the page size with bitwise equivalents, DivideByPageSize() and ModuloPageSize(), across 47 call sites, explicitly to support "legacy 4K, page size agnostic 4K and 16K" configurations. Every one of the 47 modified sites involved only the calculation method; no data structure was added, removed, or reorganized, and no allocation algorithm changed. The larger structural shift in this period was not to RosAlloc itself but to what sits above it: Android 8 introduced the Concurrent Copying collector and its RegionSpace/RegionTLAB allocator as the new default for the main heap, which is why RosAlloc’s own file needed so little internal change: its role narrowed, but the allocator itself was largely left alone.

4.3 Unstripped Build vs. Production Build↩︎

Table 2 lists the ELF sections present in each build.

Table 2: ELF Sections Present in Unstripped (CI) versus Production (Pixel 7) libart.so, Android 15
ELF Section Unstripped Production
.dynsym present present
.gnu.version present present
.symtab present absent
.strtab present absent
.debug_info present absent
.debug_line present absent
.debug_abbrev present absent
.debug_str present absent
.debug_addr present absent
.debug_rnglists present absent
.debug_loclists present absent
.debug_aranges present absent
.debug_str_offsets present absent
.debug_line_str present absent
.gnu_debugdata absent present

The CI symbols build carries ten separate .debug_* sections along with a full .symtab and .strtab. The production build from the Pixel 7 has none of these, and instead carries the single .gnu_debugdata section the CI build lacks. In raw symbol counts, the unstripped build shows 63,027 static symbols via nm against zero for production: a complete removal.

To confirm that the two binaries actually share a codebase, and not just a matching version number, their dynamic symbol tables were compared directly. Of the unique dynamic symbols in each, 2,385 were shared, a 90.4 percent overlap relative to the CI build’s unique dynamic symbol count. The symbols that did not overlap were traceable to device-specific differences between the Cuttlefish virtual target and the physical Pixel 7, not to any divergence in source code, which supports treating stripping as the only meaningful difference between the two binaries for the purposes of this comparison.

4.4 Empirical Validation on the Pixel 7↩︎

Table 3 maps memory structure naming between the two versions.

Table 3: Dalvik/RosAlloc Memory Structure Naming, Android 8 versus Android 15
Android 8 Android 15
/dev/ashmem/dalvik-main space [anon:dalvik-main space]
/dev/ashmem/dalvik-zygote space [anon:dalvik-zygote space]
/dev/ashmem/dalvik-non moving space [anon:dalvik-non moving space]
dalvik-card table dalvik-card table
dalvik-live stack dalvik-live stack
dalvik-allocation stack dalvik-allocation stack
dalvik-rosalloc page map dalvik-linear-alloc page-status map
dalvik-allocspace main rosalloc dalvik-allocspace non moving

Filtered memory maps for system_server contained 284 dalvik- and rosalloc-related entries on the Android 8 emulator against 121 on the Pixel 7, a 57 percent reduction that looks more severe than it is: Android 15 consolidates several numbered sub-structures, several separate mark-bitmap entries, for instance, into fewer combined entries; the underlying tracking mechanism is not removed, just consolidated. Each core concept present on Android 8 (main space, zygote space, non-moving space, card table, allocation and live stacks, bitmap tracking) is still present on Android 15. The visible changes are a naming shift from explicit /dev/ashmem/ prefixes to anonymous [anon:] mappings, and the reassignment already noted in Section 4.2: dalvik-allocspace main rosalloc on Android 8 becomes dalvik-allocspace non moving on Android 15, with dalvik-rosalloc page map correspondingly renamed to dalvik-linear-alloc page-status map. Structures such as the card table, live stack, and allocation stack kept identical names across both versions.

The Runtime instance symbol itself, _ZN3art7Runtime9instance_E, appears in the dynamic symbol table of both the unstripped Android 8 build and the fully stripped Android 15 production build, marked GLOBAL PROTECTED in both, meaning Google cannot remove it without breaking the runtime’s own ability to link at startup. Replicating the original DroidScraper procedure on the Android 8 emulator located this symbol at offset 0x0070a980 through GDB, matching the original paper, and successfully read the Runtime pointer from live memory. Repeating the same recovery on the Pixel 7 with Frida found the libart.so base address at 0x7125386000; adding the known offset, 0xc0d238, located the instance pointer at 0x7125f93238, and reading that address returned the Runtime object at 0xb400007227ddcc40 (the leading b4 byte is an MTE tag; the untagged address is 0x7227ddcc40). The same symbol, seven major Android versions later and under full static stripping, still resolves.

Locating the Runtime object is only the entry point, though. It occupies 3,072 bytes containing pointers to the Heap, the ThreadList, the ClassLinker, and dozens of other structures, and without DWARF information there is no way to tell which bytes correspond to which field just by looking at a hex dump. To locate individual fields inside it, DWARF offsets extracted from the unstripped CI build, heap_ at offset 0x200 within Runtime and thread_list_ at 0x248, were applied directly to the live Runtime object recovered above, and the offset for bump_pointer_space_, at 0x338 within the Heap object, was applied one level further in. Every one of these reads resolved to a valid ARM64 pointer: heap_ to 0xb4000071e7ddecf0, thread_list_ to 0xb400007257de2010, and bump_pointer_space_ to 0xb400007297de6210. The production binary’s internal layout matches the unstripped build’s layout exactly. Stripping removes the labels, not the structure. This result is specific to matched versions, however. Given Babangida et al.’s finding that most structure offsets shift across versions, there is no reason to expect offsets from a different Android release to resolve correctly against an Android 15 binary, and that cross-version transfer was not attempted here.

5 Discussion↩︎

5.1 Answering the Research Questions↩︎

RQ1 asked how far security hardening limits symbol availability and what capability survives it. The impact is severe but not absolute: production libart.so lost every static symbol and most source references between Android 8 and 15, yet what remains is enough to start from, not enough to finish with. The Runtime instance symbol survives because linking requires it, the remaining dynamic symbols provide a handful of additional landmarks, and MiniDebugInfo supplies thousands of function names with addresses but no structural information. An examiner working only from what the production binary provides can locate the Runtime object itself, but nothing past it: the binary contains no map of which byte range inside that object corresponds to which field.

RQ2 asked whether the underlying memory architecture survives that hardening, and whether the answer enables tool compatibility across build types. Yes to both, with one qualification. The heap spaces, bitmap tracking, card table, and allocation stacks present on Android 8 all carry over to Android 15, changed in name and in the RosAlloc reassignment but not in kind. The Frida-based validation is the strongest evidence here: DWARF offsets from an Android 15 unstripped build resolved correctly against a production Pixel 7’s live memory for every field tested, meaning a production binary’s internal layout is identical to the unstripped build of that same version. The qualification is that this was shown within a single version, not across versions. Babangida et al.’s finding that most structure offsets shift between releases stands; this paper adds that, within a given release, an examiner who obtains a version-matched unstripped build can use its DWARF data directly as an offset map for the corresponding production device.

5.2 Implications↩︎

For practitioners, the workflow this points to is straightforward even if more effortful than the tools built for Android 8 assumed: identify the device’s exact Android version and build number, pull a matching unstripped build from ci.android.com or by compiling AOSP, extract DWARF offsets from its libart.so, and apply them to the seized device’s memory image. That is more manual work than DroidScraper or Timeliner required, but it is a defined, repeatable procedure.

For tool developers, a tool can no longer assume nm returns anything useful against a production Android 15 binary. Recovery logic needs to fall back to the dynamic symbol table for the Runtime instance and accept externally supplied DWARF offsets for everything past it. MTE tagging is a separate concern: any tool doing pointer arithmetic on a modern device needs to strip the tag bits first, or every subsequent read fails.

For the research community, validating a technique only against unstripped or engineering builds is no longer sufficient. The gap between what a researcher compiles and what an examiner is handed, negligible before Android 15’s stripping regime, is now wide enough that an untested technique cannot be assumed to work on a production binary. Babangida et al.’s cross-version offset instability is relevant here too: the DWARF cross-application technique demonstrated in this paper needs a version-matched build for every release it is applied to, a maintenance burden, though a tractable one given that Google publishes CI builds for each release.

5.3 Limitations↩︎

Several constraints bound how far these findings generalize. All production binary analysis used Pixel devices; manufacturers that ship modified ART builds, Samsung, OnePlus, and Xiaomi among them, were not examined, so whether the same stripping and structural preservation hold on non-Pixel hardware remains untested. The factory-image analysis covered only the two endpoints, Android 8 and 15, so the exact progression between them, including the precise point at which static stripping became total, is not characterized. The Android 8 memory map came from an x86_64 emulator while the Android 15 map came from an ARM64 device; ART’s memory management code is architecture-independent, but this mismatch means the comparison reflects both a version and a potential architecture difference, though none was observed. Only system_server was examined; other processes could show different layouts, though system_server was chosen because it exercises the full range of ART’s memory features. The DWARF cross-application was validated only within a single version, not across versions, and while the Frida validation confirmed the Runtime, Heap, and BumpPointerSpace pointers are reachable, full object enumeration on the production device was not attempted end to end. Finally, LiME could not be deployed on the Android 8 emulator due to a kernel mismatch, so all analysis relied on userland access through /proc and Frida rather than a full physical acquisition; there is no specific reason to expect different results from a kernel-level capture, since the relevant structures live in userland memory regardless of method, but that assumption was not tested.

6 Conclusion and Future Work↩︎

Android 15 marks a real turning point for production binary stripping. Static symbols, debug information, and source file references are gone entirely from the binaries examiners actually receive, and any forensic tool built or tested before this shift now works in a fundamentally different environment. What has not changed is the architecture underneath. The structures present on Android 8 are all still there on Android 15, carrying new names but the same layout, and the Runtime instance symbol that any recovery chain starts from survives because the system cannot function without it. The measured gap, over 63,000 symbols in a development build against zero in production, is real, but nothing about it required changing how the runtime is built or laid out in memory. The structures are still where the source code says they should be, and a version-matched unstripped build can supply the map the device itself no longer does.

Several directions follow from this work. The DWARF cross-application demonstrated here was done entirely by hand. Automating the retrieval of a matching CI build and the extraction of its offsets would move the technique out of the research setting and into routine casework. Testing whether the same stripping and structural preservation hold on Samsung, OnePlus, Xiaomi, and other manufacturers’ builds is an open question, since vendor modifications to ART could cut either way. Extending the Frida-based recovery beyond Runtime traversal to full object enumeration on a production device would close the gap between a structure being reachable and evidence actually being recovered. A more ambitious direction is version-independent structure discovery: if ART structures carry byte patterns or pointer relationships that hold steady across releases, scanning for those patterns might locate them without symbols or precomputed offsets at all, removing the need for a version-matched build. Finally, longitudinal tracking of future releases would show whether Google continues down this path, whether the .gnu_debugdata fallback eventually disappears too, and whether the Runtime instance symbol remains exported or is made internal, since that symbol is the one reliable way into the runtime that this study identified.

Acknowledgment↩︎

This work is based, in part, on research conducted as part of the first author’s Master’s thesis in the Department of Electrical Engineering and Computer Science at Florida Institute of Technology. A substantially expanded study building on this work is currently in preparation for journal submission.

References↩︎

[1]
StatCounter. (2025). Mobile operating system market share worldwide. https://gs.statcounter.com/os-market-share/mobile/worldwide.
[2]
Shacham, H. (2007). The geometry of innocent flesh on the bone: Return-into-libc without function calls (on the x86). In Proceedings of the ACM Conference on Computer and Communications Security (CCS) (pp. 552–561).
[3]
Bappah, B., Bristol, L. G., Noureddine, L., & Ali-Gombe, A. (2025). Exploring runtime evolution in Android: A cross-version analysis and its implications for memory forensics. arXiv preprint arXiv:2512.18517.
[4]
Google. (2017). ART and Dalvik. Android Open Source Project. https://source.android.com/docs/core/runtime.
[5]
Chartier, M. (2017). Debugging ART garbage collection. Android Open Source Project. https://source.android.com/docs/core/runtime/gc-debug.
[6]
Ali-Gombe, A., Sudhakaran, S., Case, A., & Richard III, G. G. (2019). DroidScraper: A tool for Android in-memory object recovery and reconstruction. In Proceedings of the 22nd International Symposium on Research in Attacks, Intrusions and Defenses (RAID) (pp. 547–559).
[7]
Sudhakaran, S., Ali-Gombe, A., Orgah, A., Case, A., & Richard III, G. G. (2020). AmpleDroid: Recovering large object files from Android application memory. In Proceedings of the IEEE International Workshop on Information Forensics and Security (WIFS) (pp. 1–6).
[8]
Android Developers Blog. (2022, August). Android 13 is in AOSP! https://android-developers.googleblog.com/2022/08/android-13-is-in-aosp.html.
[9]
Android Open Source Project. (2023). Enable userfaultfd-based CMC GC by default on Android S and above [AOSP commit 854cb7d]. https://android.googlesource.com/platform/art/+/854cb7d94594f027cf0f056d6cd023e7a00df0cd.
[10]
Android Open Source Project. (2024). Debug ART garbage collection. https://source.android.com/docs/core/runtime/gc-debug.
[11]
Bhatia, R., Saltaformaggio, B., Yang, S. J., Ali-Gombe, A. I., Zhang, X., Xu, D., & Richard III, G. G. (2018). Tipped off by your memory allocator: Device-wide user activity sequencing from Android memory images. In Proceedings of the Network and Distributed System Security Symposium (NDSS).
[12]
Sylve, J., Case, A., Marziale, L., & Richard III, G. G. (2012). Acquisition and analysis of volatile memory from Android devices. Digital Investigation, 8(3–4), 175–184.
[13]
Stuttgen, J., & Cohen, M. (2014). Robust Linux memory acquisition with minimal target impact. Digital Investigation, 11, S112–S119.
[14]
Spreitzenbarth, M., Schmitt, S., & Freiling, F. C. (2015). Practicability study of Android volatile memory forensic research. In Proceedings of the IEEE International Workshop on Information Forensics and Security (WIFS).
[15]
Ravnas, O. A. V. (2024). Frida: A dynamic instrumentation toolkit. https://frida.re.
[16]
Saltaformaggio, B., Bhatia, R., Zhang, X., Xu, D., & Richard III, G. G. (2016). Screen after previous screens: Spatial-temporal recreation of Android app displays from memory images. In Proceedings of the 25th USENIX Security Symposium (pp. 1137–1151).
[17]
Sudhakaran, S., Ali-Gombe, A., Case, A., & Richard III, G. G. (2022). Evaluating the reliability of Android userland memory forensics. In Proceedings of the 17th International Conference on Cyber Warfare and Security (ICCWS).
[18]
Google. (2017). Hardening the kernel in Android Oreo. Android Developers Blog. https://android-developers.googleblog.com/2017/08/hardening-kernel-in-android-oreo.html.
[19]
Abadi, M., Budiu, M., Erlingsson, U., & Ligatti, J. (2009). Control-flow integrity: Principles, implementations, and applications. ACM Transactions on Information and System Security, 13(1).
[20]
Google. (2018). Control flow integrity in the Android kernel. Android Developers Blog. https://android-developers.googleblog.com/2018/10/control-flow-integrity-in-android-kernel.html.
[21]
DWARF Debugging Information Format Committee. (2017). DWARF debugging information format, version 5. https://dwarfstd.org/dwarf5std.html.
[22]
Red Hat. (2012). MiniDebugInfo: the .gnu_debugdata section. https://sourceware.org/gdb/onlinedocs/gdb/MiniDebugInfo.html.
[23]
Google. (2019). APEX file format. Android Open Source Project. https://source.android.com/docs/core/ota/apex.
[24]
Bendersky, E. (2024). pyelftools: Parsing ELF and DWARF in Python. https://github.com/eliben/pyelftools.
[25]
Android Open Source Project. (2026). Building Android. https://source.android.com/docs/setup/build/building.
[26]
Wu, J. (2024). Magisk: The magic mask for Android. https://github.com/topjohnwu/Magisk.
[27]
Google. (2025). Factory images for Nexus and Pixel devices. https://developers.google.com/android/images.
[28]
Google. (2025). Android Open Source Project. https://source.android.com/.
[29]
Google. (2025). Android Continuous Integration. https://ci.android.com/.
[30]
Arm Ltd. (2023). Memory Tagging Extension. https://developer.arm.com/documentation/102925/latest/.
[31]
Google. (2023). ARM Memory Tagging Extension in Android. https://source.android.com/docs/security/test/memory-safety/arm-mte.
[32]
Serebryany, K., Stepanov, E., Tarasov, A., & Vyukov, D. (2018). Memory tagging and how it improves C/C++ memory safety. arXiv preprint arXiv:1802.09517.
[33]
Google. (2024). Security-Enhanced Linux in Android. https://source.android.com/docs/security/features/selinux.
[34]
Smalley, S., & Craig, R. (2013). Security Enhanced (SE) Android: Bringing flexible MAC to Android. In Proceedings of the Network and Distributed System Security Symposium (NDSS).