Android Reverse Engineering Workflow
Introduction
Reverse engineering is the process of analyzing a subject system to identify its components and their interrelationships, and to create representations of the system in another form or at a higher level of abstraction. In the context of Android, reverse engineering is often used to understand the behavior of an application, to identify security vulnerabilities, or to modify the application to add new features or fix bugs.
Prerequisite
- ApkTool: A decompiler tool;
- Android Development SDK: Recompile smali code and sign the api
- Java Development SDK: Run apktool, create keystore for signing
General Workflow
Obtain APK
- Download apk file from the Internet if available.
- Retrieve the apk from installed Android devices:
1
2adb shell pm path com.example.app
adb pull PATH_OF_THE_BASE_APK
Decompile APK to smali code
Using apktool to decompile .dex binaries into smali code, which is similar to assembly code used in native libraries.
1 | apktool d release.apk --no-debug-info --force --no-res |
where:
--no-debug-info: don’t write out debug info, including.param,.linedirectives.--force: overwrite the output directory if it already exists.--no-res: don’t extract resource files. This flag could speed up decompiling and recompiling, and useful if resources couldn’t be recompiled. However, this flag prevents making a release application debuggable because it does not extract the manifest file.
Identify Modification Points
Every java/kotlin class, including nested class, has a corresponding .smali file, but find the smali file for a given class isn’t straitforward as their names are obfuscated in a released apk. There are still some methods to infer their locations.
1. find string literals
String literals, such as trace names and log messages, remain the same in smali code.. For example, I want to find smali class corresponding to DefaultAudioSink of ExoPlayer, and I find DefaultAudioSink.java contains a log:
1 | try { |
Therefore, the corresponding smali class could be found with ripgrep command:
1 | $ rg '\"Failed to set playback params\"' -w -g "*.smali" ./reverse/release |
So, J1.S is the obfuscated name for androidx.media3.exoplayer.audio.DefaultAudioSink.
2. compare input parameters and return values
The most efficient way to identify a smali function is to compare its input parameters and return value with candidate java functions. It’s rather useful when the target class has been located or search range has been narrowed down.
For example, if we have a function:
1 | .method private x0(Z)Ljava/util/List; |
we could know the java function must like:
1 | private List<XXX> FunctionName(boolean) |
After we identify which class does x0 belong to, the function signature could help narrow down the range to search function.
3. find AOSP’s interface functions
AOSP’s interface functions like MediaCodec‘s won’t be mangled in smali code. Therefore, I can directly search for a AOSP function like:
1 | $ rg 'Landroid/media/MediaCodec;->dequeueOutputBuffer' -w -g "*.smali" ./reverse/release |
By cross validation with ExoPlayer’s code, it could be known that O1.K is mangled androidx.media3.exoplayer.mediacodec.SynchronousMediaCodecAdapter
4. translate smali code to Java
Reading complex smali functions could be very hard. Converting them back to Java code could help this.
We need first install jadx, then unzip the apk and decompile a target .dex file.
1 | unzip release.apk -d apk |
Now, we can check decompiled source java code in the class0 directory. jadx also supports to decompile a whole .apk file, but it’s unnecessary for most use cases.
Modify code
Once target code is located, we can modify it to:
- change behavior;
- add log;
- add trace events;
- print backtrace;
- etc.
Repack the apk
Recompile the code and resources
1
apktool b release -o recompile.apk
This command simply recompile the smali code and pack them into a new apk. Add other flags as you need: You may use
apktool -advanceto check out what flags are supported.Create a keystore if not have one already
Make sure that JAVA environment has been set up properly, Run command:
1
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore resign.keystore recompile.apk recompile
Enter fields interactively when the command asks. Name and organazation don’t have to be real. Please remember the password you set to it.
Sign the apk
Make sure path of the Android SDK has been exported to $PATH. For example:
1
export PATH=/Users/zaijun/Library/Android/sdk/build-tools/30.0.3:$PATH
Then, sign the apk with:
1
apksigner sign -verbose --v4-signing-enabled=false -ks ./resign.keystore --out signed.apk ./recompile.apk
This command will ask for the password you set in the step 2. Note that
--v4-signing-enabled=falseflag is highly recommended, as v4 schema will cause FOS8 system crashing.Install the application
The signed apk can now be installed on a device. It’s fine to use overwrite flag “-r”:
1
adb install -r signed.apk
Practical Smali Code Snippets
Debug Log
1 | # print out an Uri object |
Usage example
1 | # p1 contains an Uri object |
Add Traces
Code
1 | const-string v0, "MyTraceName" |
Print Backtrace
Code
1 | .method private printBacktrace()V |
Usage example
1 | invoke-direct {p0}, Lcom/dss/sdk/media/adapters/exoplayer/AdSourceEventListener;->printBacktrace()V |
Constructor Log
Code
1 | const-string v0, "mydebug-AdPlaybackEndEvent" |
