An Adventure in Android Reversing

When I was in middle school, I spent a ton of time playing video games. One of the many I played was a mobile MMORPG. It was wholly unexceptional, but delightful in the way that all MMORPGs are: you fight stuff, level up, buy some items, fight more stuff, and so on. For my 6.858 project, I'd like to take another look at this game, this time with my security hat on.

We'll start by pulling the APK in question from the Google Play store. APKs are just zip files, so we unzip it, and the results look promising!

Aside from a bunch of config files, we have classes.dex and classes2.dex, and the folders lib, assets, andres.

.dex (Dalvik Executable) files are compiled from .class files, which are in turn compiled from .java files, so these look like a likely home for the logic in this app. Running them through a series of decompilers, we get stuff like this.

Agh!! This Java code is all reflection on obfuscated strings, and is way too short to be the game itself. Somehow it must serve as a loader for the real application, but exactly how is anyone's guess. Hilariously, they've named all the classes with combinations of upper and lower case "I"s, just to make reversing that bit more difficult :).

Looking more carefully, we figure out that the obfuscated strings get passed to the k class, which does some classic deobfuscation (bit shifts, xors, etc) to compute the real string.

Looks like it'll be only a minor inconvenience to deal with this, until we discover that the algorithm is seeded with

StringBuffer(stackTraceElement.getClassName()).insert(0, stackTraceElement.getMethodName()).toString()

I have no idea what this value will be at runtime. Since I'm not in the mood to manually patch a print statement into this Dalvik bytecode to determine this seed value and deobfuscate these strings, we'll admit defeat in the realm of static analysis and move onto dynamic techniques.

To do dynamic analysis, you need a rooted device. Although I tried with an Android emulator, this app specifically detects and blocks emulators, so we'll stick with physical devices. I've got out my trusty Note 4, and after no fewer than 10 hours of flashing ROMs and running sketchy bootloader exploits, I've got it unlocked and rooted. (If I'd known it was going to take that long, I would have just bought an old pre-rooted OnePlus.)

I'll be using Frida, which is relatively recent, and claims to be a "world class dynamic instrumentation framework." The basic setup is that you push a frida-server executable to your phone, and run it as root. Then you connect to the server from your computer and ask to run an app, and then immediately attach to that app and pause its execution. You write some JavaScript to do the stuff that you want, and that gets injected into the app. Then Frida resumes the app, and you get to see what your JavaScript does. Here's the launch script:

The interesting parts are all in the JavaScript. Here's a simple example that prints out all the interesting classes that are loaded when the script is run.

When we run this, we can see our friendly IiiIIiIIii classes being run! Let's try to hook calls to open()--maybe we can catch our app sticking a decrypted APK on the disk and then launching it?

Unfortunately, it turns out that our app doesn't like to be run with Frida. It always tells us there's a "security policy violation" after 10 or 15 seconds, and then kills its process. We don't catch it opening anything interesting before it dies, either.

Seems like we'll need to do some digging into what exactly is causing the app to crash. Some people online suggest hooking Java's string building functions, so we'll give that a go. We'll also grab a stack trace, and print that along with the strings. (As an aside, you should check out all the collections of cool Frida snippets online, like here and here. Many of these snippets, like this one, are inspired by them!)

And, bingo! The following string gets constructed before the crash.

TAMACA4=|DETECT_INVALID_LIBC_SO|_Exit;sigaction;abort;exit;signal;|terminated|5.1.1|armeabi-v7a (armeabi-v7a)|samsung|SM-N910V||||||[20003] Platform:11.3 Build:cb.65.1ee java.lang.StringBuilder.toString(Native Method),

Looks like somebody is finding an INVALID_LIBC_SO, and killing the process. This isn't overly surprising, since we're patching to get at functions like open(). Disconcertingly, our string hook then prints the name of every package on the phone! When the app discovers that it's being tampered with, it must send back a list of the potentially culpable packages.

Unfortunately, does other stuff that's important, so we can't just patch it to return true. After a lot of thought about how to get around this, I figure the app is hashing and checking it against a stored digest. We know from our string dump that this app uses Java's MessageDigest, which ultimately returns a byte array. So we'll just patch Array.equals on bytes to return true on this comparison!

Surprisingly, this turns out not to work. The most likely explanation is that the hashing is happening in native code. To patch a native function, I need to know what it's called, and I can't figure that out yet.

Maybe we can use ptrace to step through the code and figure out which function is doing the hashing. We fire up ptrace, run a quick ps aux to get the PID, and, to our surprise, discover three processes spawned by this package. Indeed, if we hook fork(), it turns out that the original process forks twice! Furthermore, ptrace won't stick to any of the three processes, which means that they must be ptraceing each other already. Hooking ptrace in libc gives no results, to my surprise and disappointment.

( A quick note on this--upon reflection, I suspect that perhaps Frida's hooks don't carry into fork()'d processes. The easier way to do this project would be to figure out this behavior, patch out the ptrace calls at the libc level, then stick a hook into the native DexFile::OpenMemory and dump the unpacked dex file to disk, like Strazzere does here. But since this is not what I did, onward!)

We're in a bit of a tough place, here. The app won't do anything interesting with a hooked libc, and the code that checks the integrity of libc is in a native routine that's annoying to find. We can't use ptrace to find the routine, because the app ptraces itself, and our hook on libc's ptrace isn't catching. The good news is that, much like all roads lead to Rome, all ptrace calls must eventually lead to the kernel's ptrace_attach function. Into the kernel we go.

Unfortunately, you can't just patch parts of the kernel--if you want to change it, you need to recompile the whole thing from source (barring LKMs, which we'll touch on later). That means, of course, that you need to have the source. In theory, this won't be a problem--the Linux kernel's GPL license means that all derivative works need to be available online, including whatever wacky set of configuration options and drivers run a 6-year-old Samsung Note 4.

This is the first time I've had direction interaction with the GPL, and also the first time I've understood its power. Were Samsung not forced to provide its kernel source, they certainly wouldn't publish it, and the vibrant Android modding community probably wouldn't exist. It's a really delightful experience to be able to pick the best kernel out there for the Note, run it, and be certain that I'll be able to find the sources to hack on it.

It's also the first time I've understood the following weakness of the GPL: making your source available doesn't necessarily make it useful. In fact, if your kernel build process is poorly documented and convoluted, your sources are entirely useless to me.

In the end, I had to download 5 (!) separate versions of the kernel and flash 3 different OSes before getting one that I could figure out how to fully build and pack. And by the way, the one that finally did work had syntactical mistakes and didn't even compile at first--I have a PR in against it now. Just getting the kernel to run took me a whole day. For any kindred souls who stumble upon this post, I couldn't get Stock 5.1.1, OscarKernel 6.0.1, Emotion 6.0.1, or Flashpoint 6.0.1 to compile, even with much effort, but Flashpoint for Android 9.0.1 ended up working for me after the fixes.

Although cross compiling ( = when you compile for a different platform, like ARM) is time consuming and annoying, I'm glad to have the experience. Kernel stuff in general seems to loom larger than it ends up being, and I have a lot more confidence in my ability to deal with it now.

Anyway, back to the regularly scheduled programming! We need to patch the kernel's ptrace_attach function, but only sometimes. We need the app's ptrace calls to be no-ops, but our ptrace should still work. After a bunch of thinking, the solution turns out to be easy:

When the app tries to ptrace itself, it should get 0 back, like it would if the ptrace were successful. But we'll still be able to ptrace it for real as root!

Unfortunately, this is another case of the good ol' sounds good, doesn't work. Although the OS boots successfully, the app never gets past its first black screen. My presumption here is that the app checks carefully to make sure that ptrace works as expected before forking. Darn!

I tried some other stuff, like hooking open, to no avail. But it's crazy to admit defeat at this point--I control the kernel, so I can do basically whatever I want. In particular, I'd like to run the app, and then dump the memory of the entire phone onto the disk. I made a first go-around by compiling the kernel with /proc/mem and /proc/kmem enabled, which expose the physical memory of the system. Unfortunately my dds kept crashing, so I opted to compile LiME, a purpose built Linux Kernel Module, instead. Compiling this against the kernel was also a huge adventure, but I'll spare you the details. I eventually got it working and dumped a 700mb RAM image.

binwalk (one of my favorite tools, by the way!) confirms that this image did indeed pick up several classes.dex files loaded into memory, along with a bunch of images etc. Unfortunately  I don't have the time to really dig into them. I'm also a bit disappointed with this only getting a memdump, since I really wanted to get a ptrace on the live process, but oh well.

I wanted to find out more about the obfuscator used here, and I think it's this product, which sheds some light on why the anti-debugging is so well thought out and interesting. A good challenge!

Although not exactly a success, I quite enjoyed this project. I knew basically nothing about this stuff going in, and I got to patch an executable and a kernel for the first time, as well as learn quite a bit about how Android's boot process is structured. On a more personal note, it's also been a long time since I've had something as adversarial as this project, and this was good fun.