There comes a point in the world of Android app manipulation you need to find just where the bodies are buried. Today's little whack at the project involves subverting every avoided call to the android.util.Log.* functions.
If you're unfamiliar with Android's logging framework, I'll give you a rundown:
- android.util.Log is a class filled with magical calls that let you drag out some debug information while your app is running.
- The logs are transferred over USB via ADB. It's a fairly straightforward process.
- Android helpfully throws anything you accidentally write to stderr to the log. This means that stderr is, by in large, a "spare" log for you to dump things to. It's often used for libraries targeting the JNI.
- Debug logs can contain sensitive things, magical things, and probably be a bad thing for your digestive health.
Amazingly, your phone probably spits out a lot of logging information as it's going on its day to day activities. Developers know about this, but it's always cute to keep an adb logcat terminal open just to see what is going on.
Today, however, we must do some fantastic magic. You see, some application obfuscators wrap the calls to util.log.* and make it harder to debug the application, looking for some call somewhere to switch on debugging. Sometimes, this is called. Sometimes, it's not. I'm looking to make sure that the debugging always happens.
Let's look at an obfuscated function. I'm not going to deobfuscate this because there's simply too many calls to it to fix. First, we'll look at the "decompiled" (in quotes) from Dex bytecode to Java bytecode to Java. I'm using Luyten for my frontend to a beautiful decompiler called Procyon.
The following assumes you are familiar with: a) basic java decompilation. b) apktool and its usage, c) Smali, the language used to express Dalvik bytecode.
public static void a(final boolean b) {
if (cn.com.smartdevices.bracelet.o.p && !b) {
a("DEBUG", ">>> `TRUE` ASSERTION FAILED <<<", 0, 'e');
}
}
This function takes in a boolean, b, and logs a little note to say "did this do a thing?" Basic assertions like this are nice and handy when it comes to doing debugging in development, but not so useful when you're running the application elsewhere.
.method public static a (Z)V .locals 4 sget-boolean v0, Lcn/com/smartdevices/bracelet/o;->p;Z if-eqz v0, :cond_0 if-nez p0, :cond_0 const-string v0, "DEBUG" const-string, v1, ">>> `TRUE` ASSERTION FAILED <<<" const/4 v2 0x0 const/16 v3, 0x65 invoke-static {v0, v1, v2, v3}, Lcn/com/smartdevices/bracelet/o;->a(Ljava/lang/String;Ljava/lang/string;IC)V :cond_0 return-void .end method
Any assembler-heads will know this looks like a basic bytecode language. For those who are curious:
- android class names are surrounded by L...;
- android member names are "pointed" to
- Z, V, I, C mean Bool, Void, Integer and Character, respectively.
This function is easy to circumvent. It has one exit point (cond_0) and has no special exit conditions (nothing early-return). The obfuscation is light with this one.
The flow goes like this:
- fetch the value cn.com.smartdevices.bracelet.o.p (as a boolean) into v0
- if v0 is zero, jump to cond_0
- if p0 (the first parameter; here, a boolean) is one (not-zero) jump to cond_0
- set v0 to the string "DEBUG"
- set v1 to the string ">>...<<"
- v2 gets the integer value 0 (one nybble)
- v3 gets the integer value 0x65 (one UTF16 character)
- call a(v0,v1,v2,v3)
- (and here's cond_0): return void.
Changing this function such that the check for v0 being zero is easy. Hell, save a byte or 20, drop the fetch instruction. Now, our code checks the parameter only, not the new value.
Others are... not so easy. This one for example, has a small amount of logic:
public static void debugCall(final String s, final String str) {
if (e > a && e < c) {
Log.i(s, e() + str);
}
writeDebugFile(s, str);
}
I've quietly deobfuscated this one.The key point here is that first if statement. If E is between A and C, log the thing. I suspect e was at one point the printed logging level, while a was a minimum log level and c was a maximum log level (useful for debugging). This however is a problematic game to play. Let's see what it looks like in Smali! I've truncated this to the relevant bit:
.method public static d( Ljava/lang/String; Ljava/lang/String; ) V .locals 2 sget v0, e sget v0, a if-le v0, v1, :cond_0 sget v1, c if-ge v0, v1 :cond_0 # ... code :cond_0 return-void .end method
Again, this is "Strip it out and don't worry" sort of stuff. Some of these methods with early-return are going to have a label and a return somewhere you might not expect it, such as just after the check for null or 0. This happens, and you have to watch for it.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.