FART: Automated Unpacking on ART via Active Invocation (Android 9.0 Source Adapted)
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
FART: Automated Unpacking on ART via Active Invocation (Android 9.0 Source Adapted)
This writeup is for security research and technical study only.
Background
FART is an automated unpacking framework for the Android Runtime (ART). The original author published the idea and a system image, but not the modified Android source — which makes it hard to understand the technique deeply or customize it for your own targets. This post documents the process of reverse engineering the published system.img, reconstructing the approach, and adapting it to android-9.0.0_r36, with the modified source released alongside.
Reverse engineering system.img
- Convert the sparse image to an ext4 image:
simg2img system.img system.img.ext4
- Create a mount point:
sudo mkdir sysmain
- Mount it:
sudo mount -t ext4 -o loop system.img.ext4 sysmain
Inside sysmain, the relevant files are framework.jar, core-libart.jar, and libart.so. The changes touch:
framework.jar—ActivityThread.javacore-libart.jar—DexFile.javalibart.so—libdexfile/dex/standard_dex_file.h,runtime/art_method-inl.h,runtime/art_method.h,runtime/native/dalvik_system_DexFile.cc
Rename framework.jar to framework.zip, extract classes.dex, and convert it to Java with dex2jar + jd-gui. But jd-gui can’t turn the core smali back into Java, so the only way to read the unpacking logic is to read the smali directly — specifically ActivityThread.smali.
The same applies to core-libart.jar: reading its smali reveals that DexFile.java just gained one function — a native method implemented later in libart.so, and called from ActivityThread.smali:
public classs DexFile {
+private static native void dumpMethodCode(Object methodid);
}
The Java-side unpacking code
The core unpacking logic in ActivityThread.smali:
.method public static fart()V
.catch Ljava/lang/Exception; { :L0 .. :L1 } :L11
.catch Ljava/lang/Exception; { :L3 .. :L4 } :L12
.catch Ljava/lang/IllegalAccessException; { :L15 .. :L16 } :L19
.catch Ljava/lang/IllegalAccessException; { :L21 .. :L22 } :L25
.catch Ljava/lang/reflect/InvocationTargetException; { :L21 .. :L22 } :L24
.registers 30
.prologue
.line 701
invoke-static { }, Landroid/app/ActivityThread;->getClassloader()Ljava/lang/ClassLoader;
move-result-object v5
.line 702
.local v5, appClassloader:Ljava/lang/ClassLoader;
new-instance v9, Ljava/util/ArrayList;
invoke-direct { v9 }, Ljava/util/ArrayList;-><init>()V
.line 703
.local v9, dexFilesArray:Ljava/util/List;, "Ljava/util/List<Ljava/lang/Object;>;"
const-string/jumbo v25, "dalvik.system.BaseDexClassLoader"
const-string/jumbo v26, "pathList"
move-object/from16 v0, v25
move-object/from16 v1, v26
invoke-static { v5, v0, v1 }, Landroid/app/ActivityThread;->getClassField(Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/reflect/Field;
move-result-object v23
.line 705
.local v23, pathList_Field:Ljava/lang/reflect/Field;
const-string/jumbo v25, "dalvik.system.BaseDexClassLoader"
const-string/jumbo v26, "pathList"
move-object/from16 v0, v25
move-object/from16 v1, v26
invoke-static { v0, v5, v1 }, Landroid/app/ActivityThread;->getFieldOjbect(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;
move-result-object v24
.line 706
.local v24, pathList_object:Ljava/lang/Object;
const-string/jumbo v25, "dalvik.system.DexPathList"
const-string/jumbo v26, "dexElements"
move-object/from16 v0, v25
move-object/from16 v1, v24
move-object/from16 v2, v26
invoke-static { v0, v1, v2 }, Landroid/app/ActivityThread;->getFieldOjbect(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;
move-result-object v4
check-cast v4, [Ljava/lang/Object;
.line 707
.local v4, ElementsArray:[Ljava/lang/Object;
const/4 v8, 0
:L0
.line 709
.local v8, dexFile_fileField:Ljava/lang/reflect/Field;
const-string/jumbo v25, "dalvik.system.DexPathList$Element"
const-string/jumbo v26, "dexFile"
move-object/from16 v0, v25
move-object/from16 v1, v26
invoke-static { v5, v0, v1 }, Landroid/app/ActivityThread;->getClassField(Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/reflect/Field;
:L1
move-result-object v8
:L2
.line 713
.end local v8
const/4 v3, 0
:L3
.line 715
.local v3, DexFileClazz:Ljava/lang/Class;
const-string/jumbo v25, "dalvik.system.DexFile"
move-object/from16 v0, v25
invoke-virtual { v5, v0 }, Ljava/lang/ClassLoader;->loadClass(Ljava/lang/String;)Ljava/lang/Class;
:L4
move-result-object v3
:L5
.line 719
.end local v3
const/16 v19, 0
.line 720
.local v19, getClassNameList_method:Ljava/lang/reflect/Method;
const/4 v7, 0
.line 721
.local v7, defineClass_method:Ljava/lang/reflect/Method;
const/4 v11, 0
.line 722
.local v11, dumpDexFile_method:Ljava/lang/reflect/Method;
const/4 v12, 0
.line 724
.local v12, dumpMethodCode_method:Ljava/lang/reflect/Method;
invoke-virtual { v3 }, Ljava/lang/Class;->getDeclaredMethods()[Ljava/lang/reflect/Method;
move-result-object v26
const/16 v25, 0
move-object/from16 v0, v26
array-length v0, v0
move/from16 v27, v0
:L6
.end local v7
.end local v11
.end local v12
.end local v19
move/from16 v0, v25
move/from16 v1, v27
if-ge v0, v1, :L13
aget-object v18, v26, v25
.line 725
.local v18, field:Ljava/lang/reflect/Method;
invoke-virtual/range { v18 .. v18 }, Ljava/lang/reflect/Method;->getName()Ljava/lang/String;
move-result-object v28
const-string/jumbo v29, "getClassNameList"
invoke-virtual/range { v28 .. v29 }, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v28
if-eqz v28, :L7
.line 726
move-object/from16 v19, v18
.line 727
.local v19, getClassNameList_method:Ljava/lang/reflect/Method;
const/16 v28, 1
move-object/from16 v0, v19
move/from16 v1, v28
invoke-virtual { v0, v1 }, Ljava/lang/reflect/Method;->setAccessible(Z)V
:L7
.line 729
.end local v19
invoke-virtual/range { v18 .. v18 }, Ljava/lang/reflect/Method;->getName()Ljava/lang/String;
move-result-object v28
const-string/jumbo v29, "defineClassNative"
invoke-virtual/range { v28 .. v29 }, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v28
if-eqz v28, :L8
.line 730
move-object/from16 v7, v18
.line 731
.local v7, defineClass_method:Ljava/lang/reflect/Method;
const/16 v28, 1
move/from16 v0, v28
invoke-virtual { v7, v0 }, Ljava/lang/reflect/Method;->setAccessible(Z)V
:L8
.line 733
.end local v7
invoke-virtual/range { v18 .. v18 }, Ljava/lang/reflect/Method;->getName()Ljava/lang/String;
move-result-object v28
const-string/jumbo v29, "dumpDexFile"
invoke-virtual/range { v28 .. v29 }, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v28
if-eqz v28, :L9
.line 734
move-object/from16 v11, v18
.line 735
.local v11, dumpDexFile_method:Ljava/lang/reflect/Method;
const/16 v28, 1
move/from16 v0, v28
invoke-virtual { v11, v0 }, Ljava/lang/reflect/Method;->setAccessible(Z)V
:L9
.line 737
.end local v11
invoke-virtual/range { v18 .. v18 }, Ljava/lang/reflect/Method;->getName()Ljava/lang/String;
move-result-object v28
const-string/jumbo v29, "dumpMethodCode"
invoke-virtual/range { v28 .. v29 }, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v28
if-eqz v28, :L10
.line 738
move-object/from16 v12, v18
.line 739
.local v12, dumpMethodCode_method:Ljava/lang/reflect/Method;
const/16 v28, 1
move/from16 v0, v28
invoke-virtual { v12, v0 }, Ljava/lang/reflect/Method;->setAccessible(Z)V
:L10
.line 724
.end local v12
add-int/lit8 v25, v25, 1
goto :L6
:L11
.line 710
.end local v18
.restart local v8
move-exception v13
.line 711
.local v13, e:Ljava/lang/Exception;
const-string/jumbo v25, "ActivityThread->err"
invoke-static { v13 }, Landroid/util/Log;->getStackTraceString(Ljava/lang/Throwable;)Ljava/lang/String;
move-result-object v26
invoke-static/range { v25 .. v26 }, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
goto/16 :L2
:L12
.line 716
.end local v8
.end local v13
.restart local v3
move-exception v13
.line 717
.restart local v13
const-string/jumbo v25, "ActivityThread->err"
invoke-static { v13 }, Landroid/util/Log;->getStackTraceString(Ljava/lang/Throwable;)Ljava/lang/String;
move-result-object v26
invoke-static/range { v25 .. v26 }, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
goto/16 :L5
:L13
.line 742
.end local v3
.end local v13
const-string/jumbo v25, "dalvik.system.DexFile"
const-string/jumbo v26, "mCookie"
move-object/from16 v0, v25
move-object/from16 v1, v26
invoke-static { v5, v0, v1 }, Landroid/app/ActivityThread;->getClassField(Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/reflect/Field;
move-result-object v21
.line 743
.local v21, mCookiefield:Ljava/lang/reflect/Field;
const/16 v20, 0
:L14
.local v20, j:I
array-length v0, v4
move/from16 v25, v0
move/from16 v0, v20
move/from16 v1, v25
if-ge v0, v1, :L26
.line 744
aget-object v17, v4, v20
.line 745
.local v17, element:Ljava/lang/Object;
const/4 v10, 0
:L15
.line 747
.local v10, dexfile:Ljava/lang/Object;
move-object/from16 v0, v17
invoke-virtual { v8, v0 }, Ljava/lang/reflect/Field;->get(Ljava/lang/Object;)Ljava/lang/Object;
:L16
move-result-object v10
:L17
.line 751
.end local v10
if-nez v10, :L20
.line 752
const-string/jumbo v25, "ActivityThread"
const-string/jumbo v26, "dexfile is null"
invoke-static/range { v25 .. v26 }, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
:L18
.line 743
add-int/lit8 v20, v20, 1
goto :L14
:L19
.line 748
.restart local v10
move-exception v14
.line 749
.local v14, e:Ljava/lang/IllegalAccessException;
invoke-virtual { v14 }, Ljava/lang/IllegalAccessException;->printStackTrace()V
goto :L17
:L20
.line 755
.end local v10
.end local v14
if-eqz v10, :L18
.line 756
invoke-interface { v9, v10 }, Ljava/util/List;->add(Ljava/lang/Object;)Z
.line 757
const-string/jumbo v25, "dalvik.system.DexFile"
const-string/jumbo v26, "mCookie"
move-object/from16 v0, v25
move-object/from16 v1, v26
invoke-static { v5, v0, v10, v1 }, Landroid/app/ActivityThread;->getClassFieldObject(Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;
move-result-object v22
.line 758
.local v22, mcookie:Ljava/lang/Object;
if-eqz v22, :L18
.line 761
const/4 v6, 0
.line 763
.local v6, classnames:[Ljava/lang/String;
const/16 v25, 1
:L21
move/from16 v0, v25
new-array v0, v0, [Ljava/lang/Object;
move-object/from16 v25, v0
const/16 v26, 0
aput-object v22, v25, v26
move-object/from16 v0, v19
move-object/from16 v1, v25
invoke-virtual { v0, v10, v1 }, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v6
.end local v6
check-cast v6, [Ljava/lang/String;
:L22
.line 771
.local v6, classnames:[Ljava/lang/String;
if-eqz v6, :L18
.line 772
const/16 v25, 0
array-length v0, v6
move/from16 v26, v0
:L23
move/from16 v0, v25
move/from16 v1, v26
if-ge v0, v1, :L18
aget-object v16, v6, v25
.line 773
.local v16, eachclassname:Ljava/lang/String;
move-object/from16 v0, v16
invoke-static { v5, v0, v12 }, Landroid/app/ActivityThread;->loadClassAndInvoke(Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/reflect/Method;)V
.line 772
add-int/lit8 v25, v25, 1
goto :L23
:L24
.line 767
.end local v6
.end local v16
move-exception v15
.line 768
.local v15, e:Ljava/lang/reflect/InvocationTargetException;
invoke-virtual { v15 }, Ljava/lang/reflect/InvocationTargetException;->printStackTrace()V
goto :L18
:L25
.line 764
.end local v15
move-exception v14
.line 765
.restart local v14
invoke-virtual { v14 }, Ljava/lang/IllegalAccessException;->printStackTrace()V
goto :L18
:L26
.line 779
.end local v14
.end local v17
.end local v22
return-void
.end method
.method public static fartthread()V
.registers 2
.prologue
.line 783
new-instance v0, Ljava/lang/Thread;
new-instance v1, Landroid/app/ActivityThread$1;
invoke-direct { v1 }, Landroid/app/ActivityThread$1;-><init>()V
invoke-direct { v0, v1 }, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V
invoke-virtual { v0 }, Ljava/lang/Thread;->start()V
.line 782
return-void
.end method
Translated to Java, that is:
public static void fart() {
try {
ClassLoader class_loader = getClassloader();
Method[] md = class_loader.loadClass("dalvik.system.DexFile").getDeclaredMethods();
Method getClassNameListMethod = null;
Method dumpMethodCodeMethod = null;
int mdCount = md.length;
for (int i = 0; i < mdCount; i++) {
if (md[i].getName().equals("getClassNameList")) {
getClassNameListMethod = md[i];// get DexFile's getClassNameList method
md[i].setAccessible(true);
} else if (md[i].getName().equals("dumpMethodCode")) {
dumpMethodCodeMethod = md[i];// get DexFile's dumpMethodCode method
md[i].setAccessible(true);
}
}
Object[] dexElementsObjs = (Object[]) getFieldOjbect("dalvik.system.DexPathList", getFieldOjbect("dalvik.system.BaseDexClassLoader", class_loader, "pathList"), "dexElements");
Field dexFileField = getClassField(class_loader, "dalvik.system.DexPathList$Element", "dexFile");
for (int i = 0; i < dexElementsObjs.length; i++) {
Object dexFileObj = dexFileField.get(dexElementsObjs[i]);
Object cookObj = getClassFieldObject(class_loader, "dalvik.system.DexFile", dexFileObj, "mCookie");// get mCookie
String[] classNames = (String[]) getClassNameListMethod.invoke(dexFileObj, new Object[]{cookObj});// call DexFile.getClassNameList to get every class name in the dex
for (int j = 0; j < classNames.length; j++) {
Log.e(TAG, "fart classNames:" + classNames[j]);
loadClassAndInvoke(class_loader, classNames[j], dumpMethodCodeMethod);
}
}
} catch (ClassNotFoundException e) {
Log.e(TAG, "fart ClassNotFoundException" + e.getMessage());
e.printStackTrace();
} catch (IllegalAccessException e) {
Log.e(TAG, "fart IllegalAccessException" + e.getMessage());
e.printStackTrace();
} catch (InvocationTargetException e) {
Log.e(TAG, "fart InvocationTargetException" + e.getMessage());
e.printStackTrace();
}
}
public static void loadClassAndInvoke(ClassLoader class_loader, String className, Method dumpMethodCodeMethod) {
try {
Class class1 = class_loader.loadClass(className);// actively load every class in the dex; by now the Method data is decrypted
Constructor[] constructors = class1.getDeclaredConstructors();
for (int i = 0; i < constructors.length; i++) {
dumpMethodCodeMethod.invoke(null, new Object[]{constructors[i]});// call DexFile.dumpMethodCode with the Constructor object
}
Method[] methods = class1.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
dumpMethodCodeMethod.invoke(null, new Object[]{methods[i]});// call DexFile.dumpMethodCode with the Method object
}
Log.e(TAG, "className:" + className + ",constructors length:" + constructors.length + ",method length:" + methods.length);
} catch (ClassNotFoundException e) {
Log.e(TAG, "fart ClassNotFoundException" + e.getMessage());
e.printStackTrace();
} catch (IllegalAccessException e) {
Log.e(TAG, "fart IllegalAccessException" + e.getMessage());
e.printStackTrace();
} catch (InvocationTargetException e) {
Log.e(TAG, "fart InvocationTargetException" + e.getMessage());
e.printStackTrace();
}
return;
}
public static void fartthread() {
(new Thread(new Runnable() {
public void run() {
try {
Log.e("ActivityThread", "start sleep......");
Thread.sleep(10000L);// sleep 10s
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
Log.e("ActivityThread", "sleep over and start fart");
ActivityThread.fart();// run the unpacking routine
Log.e("ActivityThread", "fart run over");
}
})).start();
}
It spawns a thread, sleeps 10 seconds, then gets to work. The fart() flow is:
-
Reflectively obtain
DexFile’sgetClassNameListanddumpMethodCodemethods —dumpMethodCodebeing the native method we just added toDexFile. -
Walk the current process’s classloader down the chain
pathList -> dexElements -> dexFile -> mCookieto get themCookieof every dex loaded by that classloader.mCookieis the native-layer handle to the loaded dex structure.
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
......
}
package*/ final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String zipSeparator = "!/";
private Element[] dexElements;
......
}
/*package*/ static class Element {
private final DexFile dexFile;
......
}
public final class DexFile {
private Object mCookie;
private static native void dumpMethodCode(Object methodid);
private static native String[] getClassNameList(Object cookie);
....
}
How does it get the current process’s classloader?
public static ClassLoader getClassloader() {
Object currentActivityThread = invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[]{}, new Object[]{});
Object mBoundApplication = getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mBoundApplication");
return ((Application) getFieldOjbect("android.app.LoadedApk", getFieldOjbect("android.app.ActivityThread$AppBindData", mBoundApplication, "info"), "mApplication")).getClassLoader();
}
Reflectively call ActivityThread’s static currentActivityThread to get the current ActivityThread object, then read its mBoundApplication member, then mBoundApplication.info (a LoadedApk), then info.mApplication (an Application), and finally Application.getClassLoader(). The class relationships:
public final class ActivityThread extends ClientTransactionHandler {
AppBindData mBoundApplication;
public static ActivityThread currentActivityThread() {
return sCurrentActivityThread;
}
static final class AppBindData {
LoadedApk info;
......
}
......
}
public final class LoadedApk {
private Application mApplication;
......
}
- We now have
DexFile’sgetClassNameListanddumpMethodCodemethods, plus thecookiethatgetClassNameListneeds:
public final class DexFile {
private Object mCookie;
private static native void dumpMethodCode(Object methodid);
private static native String[] getClassNameList(Object cookie);
....
}
Calling getClassNameList(mCookie) returns every class name in that dex.
loadClassAndInvokefirstloadClasses every class, then callsdumpMethodCodewith theMethod/Constructorobject.
Here is the crux of the “active invocation” this framework is named for: loadClass. A packed app hooks the class-loading method and restores the real code only when a class is actually loaded — that’s implicit loading. Our loadClass explicitly loads every class, so by now the methods have been restored.
The other reflection helpers are in the repository linked at the end.
When is fartthread invoked?
public final class ActivityThread extends ClientTransactionHandler {
/** Core implementation of activity launch. */
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
+ Log.e(TAG, "go into performLaunchActivity");
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
@@ -2951,10 +2956,161 @@ public final class ActivityThread extends ClientTransactionHandler {
+ ": " + e.toString(), e);
}
}
-
+ Log.e(TAG, "app name:" + r.packageInfo.getPackageName());
+ if (r.packageInfo.getPackageName().equals("com.example.arttest")) { // process check happens here
+ ActivityThread.fartthread();
+ }
return activity;
}
}
The original author checked the process in the native layer; here the check happens in ActivityThread.performLaunchActivity.
The native-side unpacking code
Reverse engineering libart.so reveals the key function:
int __fastcall art::DexFile_dumpMethodCode(int result, int a2, int a3)
{
__int64 v3; // kr00_8@1
int v4; // r0@2
int v5; // [sp+4h] [bp-14h]@1
int v6; // [sp+8h] [bp-10h]@1
int v7; // [sp+Ch] [bp-Ch]@1
v3 = *(_QWORD *)(result + 4);
v5 = *(_QWORD *)(result + 4);
v6 = result;
v7 = HIDWORD(v3);
if ( a3 )
{
v4 = art::ArtMethod::FromReflectedMethod(&v5, a3);
result = myfartInvoke(v4);
}
return result;
}
// local variable allocation has failed, the output may be wrong!
void __fastcall dumpArtMethod(int a1, const char *a2)
{
int v2; // r4@0
int v3; // r5@0 OVERLAPPED
int v5; // r7@0 OVERLAPPED
int v7; // r9@0 OVERLAPPED
int v9; // r11@0
int v10; // lr@0
int v11; // r5@1
const char *v12; // r8@1
int v13; // r0@2
void *v14; // r4@4
int v15; // r0@5
int v16; // r9@5
int v17; // r0@10
int v18; // r3@10
int v19; // r10@10
const char *v20; // r2@11
void *v21; // ST10_4@13
int v22; // r0@13
int v23; // r3@13
int v24; // r0@13
int v25; // r3@13
int v26; // ST00_4@13
int v27; // ST04_4@13
int v28; // ST08_4@13
int v29; // ST0C_4@13
int v30; // ST10_4@13
int v31; // ST14_4@13
int v32; // ST18_4@13
int v33; // r10@13
int v34; // r0@13
int v35; // r3@13
int v36; // ST00_4@13
int v37; // ST04_4@13
int v38; // ST08_4@13
int v39; // ST0C_4@13
int v40; // ST10_4@13
int v41; // ST14_4@13
int v42; // ST18_4@13
int v43; // r10@14
size_t v44; // r8@17
int v50; // r0@17
int v51; // r3@19
int v52; // r8@20
int v53; // r3@21
int v54; // r9@22
int v55; // r10@23
int v56; // r5@23
__int32 v57; // r0@23
int v58; // r6@23
size_t v59; // r2@24
void *v60; // r12@25
size_t v61; // r5@25
int v62; // t1@26
const void *v63; // r0@27
int v64; // r0@28
int v65; // r2@28
int v66; // r3@28
int v67; // r10@28
int v68; // r2@28
int v69; // r3@28
char *v70; // r1@29
int v71; // r0@31
int v72; // r2@31
int v73; // r3@31
int v74; // ST00_4@31
int v75; // ST04_4@31
int v76; // ST08_4@31
int v77; // ST0C_4@31
int v78; // ST10_4@31
int v79; // ST14_4@31
int v80; // ST18_4@31
int v81; // r0@34
int v82; // r9@34
const unsigned __int8 **v83; // r1@36
int v84; // [sp+0h] [bp-198h]@10
int v85; // [sp+0h] [bp-198h]@28
int v86; // [sp+4h] [bp-194h]@0
int v87; // [sp+4h] [bp-194h]@10
int v88; // [sp+4h] [bp-194h]@28
int v89; // [sp+8h] [bp-190h]@0
int v90; // [sp+8h] [bp-190h]@10
int v91; // [sp+8h] [bp-190h]@28
int v92; // [sp+Ch] [bp-18Ch]@0
int v93; // [sp+Ch] [bp-18Ch]@10
int v94; // [sp+Ch] [bp-18Ch]@28
int buf; // [sp+10h] [bp-188h]@0
const void *bufa; // [sp+10h] [bp-188h]@17
int bufb; // [sp+10h] [bp-188h]@28
int v98; // [sp+14h] [bp-184h]@0
int v99; // [sp+14h] [bp-184h]@10
char *v100; // [sp+14h] [bp-184h]@15
int v101; // [sp+14h] [bp-184h]@28
int v102; // [sp+18h] [bp-180h]@0
int v103; // [sp+18h] [bp-180h]@10
int v104; // [sp+18h] [bp-180h]@28
size_t v105; // [sp+1Ch] [bp-17Ch]@10
int v106; // [sp+20h] [bp-178h]@10
int v107; // [sp+24h] [bp-174h]@10
int v108; // [sp+28h] [bp-170h]@10
char s; // [sp+2Ch] [bp-16Ch]@5
unsigned __int8 v110; // [sp+6Ch] [bp-12Ch]@5
char v111; // [sp+6Dh] [bp-12Bh]@30
void *v112; // [sp+74h] [bp-124h]@29
int v113; // [sp+16Ch] [bp-2Ch]@1
int v114; // [sp+174h] [bp-24h]@1
__int64 v115; // [sp+178h] [bp-20h]@1
__int64 v116; // [sp+180h] [bp-18h]@1
__int64 v117; // [sp+188h] [bp-10h]@1
int v118; // [sp+190h] [bp-8h]@1
int v119; // [sp+194h] [bp-4h]@1
v114 = v2;
v118 = v9;
v119 = v10;
v115 = *(_QWORD *)&v3;
v11 = a1;
v116 = *(_QWORD *)&v5;
v12 = a2;
v117 = *(_QWORD *)&v7;
v113 = v0;
if ( !checkprocess() )
{
LABEL_2:
v13 = v0;
if ( v113 == v0 )
JUMPOUT(__CS__, v119);
LABEL_38:
_stack_chk_fail(v13);
}
v14 = malloc(0x3E8u);
if ( !v14 )
{
art::LogMessage::LogMessage(&v105, "art/runtime/art_method.cc", 595, 2);
v64 = art::Atomic<int>::LoadJavaData(&v105);
v67 = std::__1::operator<<<std::__1::char_traits<char>>(
v64,
"ArtMethod::dumpArtMethodinvoked,methodname:",
v65,
v66,
-1,
v86,
v89,
v92,
buf,
v98,
v102,
v105,
v106,
v107,
v108,
*(void **)&s);
art::PrettyMethod((art *)&v110, (art::ArtMethod *)v11, 1);
if ( v110 << 31 >= 0 )
v70 = &v111;
else
v70 = (char *)v112;
v71 = std::__1::operator<<<std::__1::char_traits<char>>(
v67,
v70,
v68,
v69,
v85,
v88,
v91,
v94,
bufb,
v101,
v104,
v105,
v106,
v107,
v108,
*(void **)&s);
std::__1::operator<<<std::__1::char_traits<char>>(
v71,
"malloc 1000 byte failed",
v72,
v73,
v74,
v75,
v76,
v77,
v78,
v79,
v80,
v105,
v106,
v107,
v108,
*(void **)&s);
if ( v110 & 1 )
operator delete(v112);
art::LogMessage::~LogMessage((art::LogMessage *)&v105);
goto LABEL_2;
}
memset(&s, 0, 0x40u);
memset(&v110, 0, 0x100u);
getpid();
_sprintf_chk(&s, 0, 64, "/proc/%d/cmdline");
v15 = open(&s, 0, 420);
v16 = v15;
if ( v15 > 0 )
{
read(v15, &v110, 0x100u);
close(v16);
}
v13 = v110;
if ( v110 )
{
art::LogMessage::LogMessage(&v105, "art/runtime/art_method.cc", 613, 2);
v17 = art::Atomic<int>::LoadJavaData(&v105);
v19 = std::__1::__put_character_sequence<char,std::__1::char_traits<char>>(
v17,
"ArtMethod::dumpArtMethodinvoked,methodname:",
43,
v18,
-1,
v86,
v89,
v92,
buf,
v98,
v102,
v105,
v106,
v107,
v108,
*(void **)&s);
art::PrettyMethod((art *)&v106, (art::ArtMethod *)v11, 1);
if ( (unsigned __int8)v106 << 31 >= 0 )
v20 = (char *)&v106 + 1;
else
v20 = (const char *)v108;
v21 = (void *)v20;
v22 = strlen(v20);
v24 = std::__1::__put_character_sequence<char,std::__1::char_traits<char>>(
v19,
v21,
v22,
v23,
v84,
v87,
v90,
v93,
(int)v21,
v99,
v103,
v105,
v106,
v107,
v108,
*(void **)&s);
v33 = std::__1::__put_character_sequence<char,std::__1::char_traits<char>>(
v24,
"from:",
5,
v25,
v26,
v27,
v28,
v29,
v30,
v31,
v32,
v105,
v106,
v107,
v108,
*(void **)&s);
v34 = strlen(v12);
std::__1::__put_character_sequence<char,std::__1::char_traits<char>>(
v33,
(void *)v12,
v34,
v35,
v36,
v37,
v38,
v39,
v40,
v41,
v42,
v105,
v106,
v107,
v108,
*(void **)&s);
if ( v106 & 1 )
operator delete((void *)v108);
art::LogMessage::~LogMessage((art::LogMessage *)&v105);
v43 = *(_DWORD *)(*(_DWORD *)(*(_DWORD *)v11 + 16) + 32);
art::PrettyMethod((art *)&v106, (art::ArtMethod *)v11, 1);
if ( v106 & 1 )
{
v100 = (char *)v108;
operator delete((void *)v108);
}
else
{
v100 = (char *)&v106 + 1;
}
v44 = *(_DWORD *)(v43 + 8);
bufa = *(const void **)(v43 + 4);
memset(v14, 0, 0x3E8u);
memset(v14, 0, 0x3E8u);
_R12 = (int)"/sdcard/fart";
__asm { VLDR D16, [R12] }
*((_DWORD *)v14 + 2) = 1953653094;
*((_BYTE *)v14 + 12) = aSdcardFart[12];
__asm { VSTR D16, [R4] }
mkdir((const char *)v14, 0x1FFu);
memset(v14, 0, 0x3E8u);
_sprintf_chk(v14, 0, 1000, "/sdcard/fart/%s", &v110);
mkdir((const char *)v14, 0x1FFu);
memset(v14, 0, 0x3E8u);
_sprintf_chk(v14, 0, 1000, "/sdcard/fart/%s/%d_dexfile.dex", &v110, v44);
v50 = open((const char *)v14, 0, 438);
if ( v50 <= 0 )
{
v81 = open((const char *)v14, 1090, 438);
v82 = v81;
if ( v81 <= 0 )
{
LABEL_19:
v13 = *(_DWORD *)(*(_DWORD *)v11 + 16);
v51 = *(_DWORD *)(v11 + 16);
if ( v51 )
{
v52 = *(_DWORD *)(*(_DWORD *)(v13 + 32) + 4) + v51;
if ( v52 )
{
v53 = *(_DWORD *)(v52 + 12);
if ( *(_WORD *)(v52 + 6) )
{
v83 = (const unsigned __int8 **)(v52 + 2 * v53 + 19);
v105 = ((unsigned int)v83 & 0xFFFFFFFC) + 8 * *(_WORD *)(v52 + 6);
v54 = art::codeitem_end((art *)&v105, v83) - v52;
}
else
{
v54 = 2 * (v53 + 8);
}
memset(v14, 0, 0x3E8u);
v55 = *(_DWORD *)(v43 + 8);
v56 = *(_DWORD *)(v11 + 20);
v57 = syscall(224);
_sprintf_chk(v14, 0, 1000, "/sdcard/fart/%s/%d_ins_%d.bin", &v110, v55, v57);
v13 = open((const char *)v14, 1090, 438);
v58 = v13;
if ( v13 > 0 )
{
lseek(v13, 0, 2);
memset(v14, 0, 0x3E8u);
_sprintf_chk(
v14,
0,
1000,
"{name:%s,method_idx:%d,offset:%d,code_item_len:%d,ins:",
v100,
v56,
v52 - (_DWORD)bufa,
v54);
v59 = *(_BYTE *)v14;
if ( *(_BYTE *)v14 )
{
v60 = v14;
v61 = 0;
do
{
v62 = *((_BYTE *)v60 + 1);
v60 = (char *)v60 + 1;
++v61;
v59 = v61;
}
while ( v62 );
}
write(v58, v14, v59);
v105 = 0;
v63 = (const void *)base64_encode(v52, v54, &v105);
write(v58, v63, v105);
write(v58, "};", 2u);
fsync(v58);
v13 = close(v58);
}
}
}
goto LABEL_8;
}
write(v81, bufa, v44);
fsync(v82);
v50 = v82;
}
close(v50);
goto LABEL_19;
}
LABEL_8:
if ( v113 != v0 )
goto LABEL_38;
j_j_free(v14);
}
In short: convert the Java Method object to an ArtMethod via ArtMethod::FromReflectedMethod, check whether it’s the target process, then dump the CodeItem.insns_ out of the ArtMethod. The code below is my own implementation — the reverse-engineered version was mangled beyond recognition, and wasn’t necessarily Android 9.0-compatible.
runtime/native/dalvik_system_DexFile.cc — implement DexFile’s private static native void dumpMethodCode(Object methodid):
+static void DexFile_dumpMethodCode(JNIEnv* env, jclass, jobject j_method) {
+ ScopedFastNativeObjectAccess soa(env);
+ ArtMethod* method = ArtMethod::FromReflectedMethod(soa, j_method); // convert to ArtMethod
+ LOG(ERROR) << "fartlog, method:" << method->GetName();
+ method->DumpArtMethod();// call ArtMethod's DumpArtMethod
+}
+
static void DexFile_setTrusted(JNIEnv* env, jclass, jobject j_cookie) {
Runtime* runtime = Runtime::Current();
ScopedObjectAccess soa(env);
@@ -877,7 +885,8 @@ static JNINativeMethod gMethods[] = {
NATIVE_METHOD(DexFile, getStaticSizeOfDexFile, "(Ljava/lang/Object;)J"),
NATIVE_METHOD(DexFile, getDexFileOptimizationStatus,
"(Ljava/lang/String;Ljava/lang/String;)[Ljava/lang/String;"),
+ NATIVE_METHOD(DexFile, dumpMethodCode, "(Ljava/lang/Object;)V")
};
runtime/art_method.h:
+ void DumpArtMethod() REQUIRES_SHARED(Locks::mutator_lock_);// declare the function
runtime/art_method-inl.h:
+inline void ArtMethod::DumpArtMethod() { // implementation
+ const DexFile* dex_file = GetDexFile();
+ const DexFile::CodeItem* code_item= GetCodeItem();
+ if (dex_file != NULL && code_item != NULL && !dex_file->IsCompactDexFile())
+ {
+ const StandardDexFile::CodeItem& standardCodeItem = down_cast<const StandardDexFile::CodeItem&>(*code_item);// must downcast to the subclass object
+ LOG(ERROR) << "fartlog, DumpArtMethod code_item length:" << standardCodeItem.insns_size_in_code_units_;
+ for (int i = 0 ; i < (int)standardCodeItem.insns_size_in_code_units_; i++)
+ {
+ LOG(ERROR) << "fartlog, DumpArtMethod code_item content:" << standardCodeItem.insns_[i];
+ }
+ }
+}
libdexfile/dex/standard_dex_file.h:
- private:
+ public:// CodeItem must be public, or we can't call it
CodeItem() = default;
As you can see, this version just prints logs.
Experiment
The demo APK, its packed version, and the source are all on GitHub.
Here is the dump result for invokeStaticMethod in a test dex class:
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, method:invokeStaticMethod
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item length:35
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:8210
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:113
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:16564
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:0
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:10
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:16724
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:7693
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:546
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:2125
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:4208
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:16765
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:2
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:794
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:11456
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:8302
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:16775
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:50
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:524
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:8302
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:16771
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:2
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:524
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:4206
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:16780
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:2
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:524
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:786
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:12401
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:16529
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:801
01-05 12:25:27.037 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:268
01-05 12:25:27.038 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:4206
01-05 12:25:27.038 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:16531
01-05 12:25:27.038 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:1
01-05 12:25:27.038 2528 3589 E com.example.arttest: fartlog, DumpArtMethod code_item content:14
Compared to the original dex, opened in a hex editor:



Only a few bytecodes differ — likely a difference in indices.
Areas for improvement
-
This post uses a demo APK, not a complex one. If a real packed app trips you up, the approach here is a starting point to keep iterating on.
-
This version only logs the
ArtMethodCodeItem content — it doesn’t dump the CodeItem to a file, nor dump the whole dex for later repair. That work is left to the reader.
Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch