Android ARM64 eBPF uprobe DEX dumper.
Use run_dexdump_full.sh on the device. Put these files in the same directory:
/data/local/tmp/dex_dump_bin
/data/local/tmp/dex_dump.bpf.o
/data/local/tmp/run_dexdump_full.sh
Deploy:
adb push dex_dump.bpf.o /data/local/tmp/
adb push dex_dump_bin /data/local/tmp/
adb push run_dexdump_full.sh /data/local/tmp/
adb shell chmod +x /data/local/tmp/dex_dump_bin /data/local/tmp/run_dexdump_full.shRun from a device-side root shell with full capabilities:
adb shell
su
cd /data/local/tmp
./run_dexdump_full.sh runAfter the script is running and waiting for events, open the target protected app and interact with it until its unpacking or DEX loading logic is triggered.
Background mode:
cd /data/local/tmp
./run_dexdump_full.sh start
./run_dexdump_full.sh logs
./run_dexdump_full.sh stopThe script automatically:
- checks
dex_dump_binanddex_dump.bpf.o - writes diagnostics to
dexdump.log - cleans old
dump_pid_*.dexfiles - runs with
-scan - dumps full DEX files by default with
MAX_DUMP=0
Useful script environment variables:
EXTRA_ARGS: extra arguments passed todex_dump_bin, for example-symbol ...or-offset 0x...MAX_DUMP: max bytes to dump per DEX region. Default is0, meaning full dumpLIB: ART DEX library path. Default is/apex/com.android.art/lib64/libdexfile.soOUT: dump output directory. Default is the script directoryWORKDIR: directory containingdex_dump_binanddex_dump.bpf.o. Default is the script directory
Examples:
./run_dexdump_full.sh run
MAX_DUMP=65536 ./run_dexdump_full.sh run
EXTRA_ARGS="-symbol '_ZNK3art16ArtDexFileLoader4OpenEPKhmRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEE'" ./run_dexdump_full.sh run
EXTRA_ARGS="-offset 0x19a90" ./run_dexdump_full.sh runPull results:
adb pull /data/local/tmp .
adb pull /data/local/tmp/dexdump.log .GitHub Actions builds two artifacts:
dex_dump.bpf.o: eBPF bytecodedex_dump_bin: Android ARM64 user-space loader/dumper
Run the Build Android eBPF Dex Dumper workflow manually from Actions, or push to main.
adb push dex_dump.bpf.o /data/local/tmp/
adb push dex_dump_bin /data/local/tmp/
adb shell chmod +x /data/local/tmp/dex_dump_binRun from a device-side root shell with full capabilities. On some devices, adb shell su -c inherits adbd's limited capability bounding set and cannot load eBPF programs.
Basic run:
adb shell
su
cd /data/local/tmp
./dex_dump_bin -lib /apex/com.android.art/lib64/libdexfile.so -package com.example.target -max-dump 65536If the package is already running, -package resolves the current PID and filters events to that PID. If the app starts after the dumper, either start the dumper without -package, or start the app first and then run the dumper with -package.
Without -package, all DEX load events are observed:
./dex_dump_bin -lib /apex/com.android.art/lib64/libdexfile.so -max-dump 65536Use an explicit symbol when testing a symbol found with readelf:
./dex_dump_bin \
-lib /apex/com.android.art/lib64/libdexfile.so \
-symbol '_ZNK3art16ArtDexFileLoader4OpenEPKhmRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEE' \
-package com.example.target \
-max-dump 65536Use a file offset if symbol lookup fails or symbols are stripped:
./dex_dump_bin \
-lib /apex/com.android.art/lib64/libdexfile.so \
-offset 0x19a90 \
-package com.example.target \
-max-dump 65536Enable header scanning as a fallback. This scans readable memory ranges for DEX headers after startup and after DEX events:
./dex_dump_bin -lib /apex/com.android.art/lib64/libdexfile.so -package com.example.target -scan -max-dump 65536If eBPF loading fails from ADB root, create and run a device-side script from a real root context, for example through a Magisk service or local terminal root shell:
cd /data/local/tmp
./dex_dump_bin -lib /apex/com.android.art/lib64/libdexfile.so -package com.example.target -scan -max-dump 65536Dumped files are written under /data/local/tmp/<package>/.
File name format:
dump_pid_<pid>_0x<base>_<read_size>_<source>.dex
Pull results:
adb pull /data/local/tmp .
adb pull /data/local/tmp/dexdump.log .Useful flags:
-obj: path todex_dump.bpf.o-lib: path to the ART DEX library, default/apex/com.android.art/lib64/libdexfile.so-symbol: optional symbol name to uprobe. If not set, the program tries the built-in symbol list inmain.go-offset: optional file offset to uprobe. If non-zero, this overrides symbol lookup-out: dump output directory, default/data/local/tmp-max-dump: max bytes to dump per DEX region, default65536. Use0to dump the full reported size-package: optional Android package name filter-scan: scan target process memory for DEX headers after events. Requires-packagefor initial scan
Different Android versions and ROMs put ART DEX loading code in different libraries. Do not assume libart.so always contains the function you need.
This project currently hooks DEX memory-open functions where the first useful arguments are:
x0 = this
x1 = const uint8_t* base
x2 = size_t size
That matches the current eBPF program, which reads the DEX base from register x1 and size from x2.
Start with libdexfile.so, then check libart.so if needed:
adb shell "readelf -Ws /apex/com.android.art/lib64/libdexfile.so | grep -iE 'ArtDexFileLoader|DexFileLoader|DexFileC|OpenCommon|OpenWithDataSection'"
adb shell "readelf -Ws /apex/com.android.art/lib64/libart.so | grep -iE 'ArtDexFileLoader|DexFileLoader|DexFileC|OpenCommon|OpenWithDataSection'"Useful symbols usually look like one of these:
art::ArtDexFileLoader::Open(const uint8_t*, size_t, ...)
art::DexFileLoader::Open(const uint8_t*, size_t, ...)
art::DexFileLoader::OpenWithDataSection(const uint8_t*, size_t, ...)
art::DexFile::DexFile(const uint8_t*, size_t, ...)
In mangled form, examples from one tested device were:
_ZNK3art16ArtDexFileLoader4OpenEPKhmRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEE
_ZNK3art13DexFileLoader4OpenEPKhmRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEE
_ZN3art7DexFileC2EPKhmS2_mRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileENS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISG_EEEEb
The important part is EPKhm in the mangled name. It means the function has const uint8_t* and size_t arguments, which are the DEX base and DEX size.
readelf may show UND entries. Those are imports, not implementations. Hook the library that contains a real address.
Good:
0000000000019a90 FUNC GLOBAL PROTECTED ... _ZNK3art16ArtDexFileLoader4OpenEPKhm...
Bad:
0000000000000000 FUNC GLOBAL DEFAULT UND _ZNK3art16ArtDexFileLoader4OpenEPKhm...
If the symbol is UND in libart.so, check libdexfile.so.
If you found a symbol in libdexfile.so, pass it with -symbol:
./dex_dump_bin \
-lib /apex/com.android.art/lib64/libdexfile.so \
-symbol '_ZNK3art16ArtDexFileLoader4OpenEPKhmRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEE' \
-package com.example.target \
-max-dump 65536If symbol names are stripped or link.Uprobe cannot resolve them, use the symbol value from readelf as a file offset:
./dex_dump_bin \
-lib /apex/com.android.art/lib64/libdexfile.so \
-offset 0x19a90 \
-package com.example.target \
-max-dump 65536Most of the time, you only need to change the default library or the built-in symbol list.
Default library path is in main.go:
libPath := flag.String("lib", "/apex/com.android.art/lib64/libdexfile.so", "path to target ART dex library")Built-in symbols are in attachDexOpen():
symbols := []string{
"_ZNK3art16ArtDexFileLoader4OpenEPKhm...",
"_ZNK3art13DexFileLoader4OpenEPKhm...",
"_ZN3art7DexFileC2EPKhmS2_m...",
}Add the symbol you found to this list, preferably near the top if it is the best match for your ROM.
You do not need to edit main.go if you use -symbol or -offset from the command line.
Only change the eBPF argument registers if the function signature is different.
Current expected layout:
x1 = DEX base
x2 = DEX size
If you hook a function where the DEX pointer and size are in different arguments, update dex_dump.bpf.c accordingly. For example:
event.base = ctx->regs[1];
event.size = ctx->regs[2];Change regs[1] and regs[2] to the correct ARM64 argument registers for the function you selected.
- Run
readelf -Wsonlibdexfile.soandlibart.so. - Pick a defined symbol containing
EPKhmor an equivalentconst uint8_t*, size_tsignature. - Test it with
-symbolfirst. - If symbol lookup fails, test with
-offsetusing the address fromreadelf. - If it works, optionally add the symbol to
attachDexOpen()inmain.go. - Rebuild and redeploy.
On some Magisk/rooted devices, adb shell su -c still lacks the capabilities required to load eBPF programs because it inherits adbd's restricted capability bounding set.
If loading fails with operation not permitted or memlock/capability errors, run the dumper from a real device-side root context, such as a Magisk service script or local root shell with full capabilities.