How to reverse engineer Android applications—using a popular word game as an example

How to reverse engineer Android applications—using a popular word game as an example

Short introduction to the basic methods and techniques used in reverse engineering Android applications. A popular word game is used as an example app.

The slides describe obtaining the application code, decompiling it, debugging Android applications, using a proxy server (Man-in-the-Middle) to extract communication protocols and automating Android applications.

Published under CC BY-NC-SA 3.0

4ab2845fa4f5ab860bc3476c9f11b759?s=128

Christoph Matthies

August 27, 2014
Tweet

Transcript

  1. How to reverse engineer Android applications Finding Vulnerabilities through Reverse

    Engineering Hasso Plattner Institute, Potsdam Hubert Hesse, Lukas Pirl, Christoph Matthies, Conrad Calmez using a popular word game as an example ? ? Images: “Freepik” on flaticon.com (CC BY 3.0), Google (CC BY 3.0)
  2. 1 Get the .apk 2 3 4 Extract the .apk

    5 Decompilation to Smali Debugging 6 Putting it together 7 8 Automation Proxy Decompilation to Java
  3. Our Example—a word game • Top 10 word game in

    145 countries (as of July 2014) • More than 10.000.000 installs • Over 50 million players • Play online (with friends) • 14 languages • Free and premium version
  4. 1:58 0 points S N B I L U S

    F E I T T E R P A
  5. 1:58 15 points S N B I L U S

    F E I T T E R P A FLUT +15
  6. • APK (application package file), archive file, based on JAR

    format • Similar to Deb packages (in Ubuntu) or MSI packages (in Windows) • Contains program code, resources, assets, certificates, and manifest file • Can’t be directly downloaded from App Store 1 Get the .apk Download using online “APK Downloader” (http://apps.evozi.com/apk-downloader/) - or - Install on device and download using SDK tools (adb pull <app_path> downloaded.apk)
  7. 2 Extract the .apk • Normal decompression using unzip fails

    • Special tool: APKTool ◦ Standard is APKTool 1.5.2. (not able to recompress correctly) (https: //code.google.com/p/android-apktool/downloads/list) ◦ APKTool 2.0.0 Beta 9 works (http://connortumbleson.com/2014/02/apktool-2-0-0-beta-9-released/) Decrompressing: apktool d -d game.apk -o outdir
  8. 2 Extract the .apk

  9. 2 Modifying resources • Change arbitrary resources • Repack into

    .apk file and install Recrompressing: apktool b -d outdir -o com.company.game.free_patch.apk • Recompression works, Android fails with “can’t install”, wrong certificate ◦ APKTool tries to reuse as much as possible, doesn’t recompute signature
  10. 2 Manually sign repacked apk: • Create custom CA •

    Java JAR Signing and Verification Tool (http://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html) jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my- release-key.keystore com.company.game.free_patch.apk alias_name Modifying resources
  11. None
  12. .apk contains compiled code • Dalvik bytecode interpreted by the

    Dalvik Process virtual machine • Stored in .dex (Dalvik EXecutable) files APKTool translates this to “smali” (https://code.google.com/p/smali/) • Abstraction of bytecode, closer to Java • Dalvik opcodes (http://s.android.com/tech/dalvik/dalvik-bytecode.html) • Can be edited directly 3 Decompilation to Smali
  13. .class public LHelloWorld; .super Ljava/lang/Object; .method public static main([Ljava/lang/String;)V .registers

    2 sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; const-string v1, "Hello World!" invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V return-void .end method 3 Smali Hello World
  14. Interactive debugging • Set debuggable=”true” in AndroidManifest.xml ◦ Repack using

    APKTool • Need to connect smali sources to binary • Workaround: pretend we have valid Java code 4 Debugging <application android:allowBackup="true" android:hardwareAccelerated="true" android:icon="@drawable/launcher_icon" android:label="@string/app_name" android:name="com.company.game.core.GameApplication" android:theme=" @style/Theme.GameTheme" android:debuggable="true">
  15. a=0;// .class public abstract La; a=0;// .super Ljava/lang/Object; a=0;// a=0;//

    a=0;// # instance fields a=0;// .field protected final a:Ljava/lang/Object; a=0;// a=0;// .field private final b:Landroid/os/Handler; a=0;// 4 Debugging Smali code in comments Placeholder Java
  16. Two ways to obtain java code • Convert .dex files

    to .jar ◦ Use standard java bytecode decompilers • Disassemble .dex directly to .java 5 Decompilation to Java
  17. Using dex files • Androguard (https://code.google.com/p/androguard/) ◦ Maps DEX format

    into full Python objects ◦ Works in memory (My 4GB machine wasn’t enough) ◦ Doesn’t immediately dump code into Java files 5 Decompilation to Java
  18. Using jar files • dex2jar (https://code.google.com/p/dex2jar/) ◦ dex2jar, jar2dex, apk-sign

    ◦ Supports recreating .dex from Java • JD-GUI (http://jd.benow.ca/) ◦ Popular jar-decompiler ◦ Works 100% with “Hello World” app 5 Decompilation to Java
  19. Combining Java decompilation and Smali • Java more readable than

    Smali • Unfortunately Java decompilation not 100% perfect ◦ Invalid Java constructs or only method signatures ◦ Cannot recompile from Java sources 6 Putting it together
  20. private void fixSpecialChars() { int i; char ac[]; int j;

    int k; i = 0; ac = tiles; j = ac.length; k = 0; _L9: if(k >= j) break MISSING_BLOCK_LABEL_161; ac[k]; JVM INSTR lookupswitch 6: default 80 // 40: 125 // 41: 137 // 47: 149 // 91: 89 // 92: 101 // 93: 113; goto _L1 _L2 _L3 _L4 _L5 _L6 _L7 _L4: break MISSING_BLOCK_LABEL_149; _L1: break; /* Loop/switch isn't completed */ _L5: break; /* Loop/switch isn't completed */ _L10: i++; k++; if(true) goto _L9; else goto _L8 _L8: 6 When Decompilation fails an example Goto not supported in Java Bare JVM instructions
  21. Combining Java decompilation and Smali • Approach: Use multiple Java

    decompilers ◦ They tend to fail in different places 6 Putting it together 1. Find interesting parts in Java source 2. Check corresponding smali sources 3. Edit those
  22. protected void roundEnd(boolean paramBoolean) { // … this.resultData.setTotalScore(this.totalScore); // …

    startRoundSummary(); if (!this.isPractice) { this.currentRound.setWordsInRound(this.resultData.getMoves().size()); // … this.currentRound.setPlayer1Moves(GameHelper.encodeMoves(this.resultData. getMoves())); this.currentRound.setPlayer1Score(this.totalScore); // … 6 Manipulating the score Opportunities for manipulation • Server validation disallows this
  23. a=0;// sget-boolean v0, Lcom/company/game/core/statics/Statics;->DEBUGGING:Z a=0;// a=0;// #v0=(Boolean); -a=0;// if-eqz v0,

    :cond_0 +a=0;// #if-eqz v0, :cond_0 a=0;// 6 Enable Logging public class Toolkit { // … public static void Logw(String s, String s1) { if(Statics.DEBUGGING) Log.w(s, s1); } // …
  24. a=0;// # static fields a=0;// .field public static ROUND_DURATION_IN_SECONDS_FOR_NORMAL_GAME:I a=0;//

    .field public static ROUND_DURATION_IN_SECONDS_FOR_TUTORIAL:I a=0;// a=0;// .method static constructor <clinit>()V a=0;// .locals 1 … -a=0;// const/16 v0, 0x78 +a=0;// const/16 v0, 0x12c a=0;// a=0;// #v0=(PosByte); a=0;// sput v0, Lcom/company/game/core/statics/GameStatics;->ROUND_DURATION_IN_SECONDS_FOR_NORMAL_GAME:I 6 More time per round 120s 300s
  25. public static boolean allowPremiumContent(PremiumType premiumtype, Context context) { if(premiumIsPurchased(context)) return

    true; synchronized(lock) { if(!isLicensed(context)) break MISSING_BLOCK_LABEL_31; } return true; 6 Getting Premium a=0;// .line 129 -a=0;// invoke-static {p0}, Lcom/company/game/util/PremiumCampaignHelper;->premiumIsPurchased(…;)Z +a=0;// # invoke-static {p0}, Lcom/company/game/util/PremiumCampaignHelper;->premiumIsPurchased(…;)Z a=0;// -a=0;// move-result v0 +a=0;// # move-result v0 a=0;// -a=0;// #v0=(Boolean); -a=0;// if-eqz v0, :cond_0 +a=0;// #v0=(One); +a=0;// # if-eqz v0, :cond_0
  26. 6 Getting Premium free version premium (stats unlocked, no ads)

  27. 7 Proxy Route all app traffic through custom proxy •

    Used MitMProxy (https://github.com/mitmproxy/mitmproxy) • Retrieve real server URL via Wireshark • Redirect app traffic via /etc/hosts on device • Custom SSL certificate ◦ Install own CA in device ◦ No certificate pinning • Avoid compressed responses via HTTP header ◦ Accept-Encoding: gzip;q=0,deflate,sdch
  28. 7 Proxy AES encryption • Shared key in decompiled code

    • No key derivation function • AES initialization vector in HTTP header ◦ Payload-session: 2e2f6a61642f7372… ◦ Unencrypted // file APIConnector.java private static byte sharedKey[] = { 57, -116, 126, 39, 116, -25, -95, -106, -81, 48, -33, -19, 120, 118, 35, 40, 66, 126, 31, 30, -83, 76, 31, 93, 13, -122, -50, 68, -108, -114, 28, -80 };
  29. SSL MitM Proxy SSL HTTP Server by “aLf “, thenounproject.com

    (CC BY 3.0 US) Spy by “Hopstarter ”, iconarchive.com (CC BY-NC-ND 4.0) #! python #decrypt AES #using IV 7 Proxy Header: AES IV AES payload HTTP # /etc/hosts # redirect # to proxy
  30. 7 Proxy { "cacheTimestamp": "1405377910521", "userId": "0", "conversationId": "-1", "player1MostWordsInRound":

    "32", "id": "6602198229545556683", "player1Score": "214", "player1LongestWord": "HEAPS", "player1User": { "username": "username", "ranking": "0", "premium": "false", "recruits": "0", "deleted": "false", "newUser": "false", "bestScoreInMatch": "0", "userId": "3005807464", "bestScoreInRound": "0", "online": "false", "facebookConnected": "false", "avatarId": "0", "matchesPlayed": "0", "useFacebookImage": "false", "mostWordsInRound": "0" }, {"rounds": [ { "seed3": "14657688", "player2MoveErrors": "0", "gameId": "6602198229545556683", "player2SwipeDistance": "681", "player2Moves": "1AB2BAE2EAB216227612AEF2DA73840127652567354013DAB723673 B7654EAB72", "player1MoveErrors": "19", "player2Done": "true", "seed1": "2073207065", "seed2": "680974433", "player1SwipeDistance": "1608", "board": { "bonus": [" ", " ", " ", " ", " ", " ", "D", " ", " ", " ", " ", " ", " ", " ", " ", "T" ], "board": ["A", "T", "E", "H", "E", "P", "O", "T", "H", "S", "A", "S", "T", "F", "T", "E" ], "words": [ "TATE", "SOTS", "HOST", "SAPS", "FATSOS", … Server response request size up to 100kB
  31. 8 Automation Play the game automatically • Generic external approach

    ◦ No modification of binary necessary ◦ Works for any app Monkeyrunner (http://developer.android.com/tools/help/monkeyrunner_concepts.html) • Test apps at the functional/framework level • Able to simulate keystrokes, take screenshots • Python bindings
  32. 8 Obtain all possible words to play correctly • apk

    contains .jet “dictionary” for each language • Btw, also a wordlist (probably) used to check for cheaters Automation
  33. 8 Automation Ruzzle .jet files • Binary files • Trie

    / Radix tree structure • Optimal for the way the game is played • No duplicate encoding of characters • List of all excepted words constructable G GA GAM GAME GO GOD GOT G O D T A M E
  34. 8 Automation Achieving the highscore • Get all 16 letters

    ◦ Input by hand / screenshot + OCR • Find all valid words using the extracted dictionary • Simulate keystrokes for found words ◦ Actually not enough time to enter all valid words
  35. 8 Automation DEMO

  36. Achievements Found possibilities to: ✓ Enable logging ✓ Unlock premium

    features ✓ Achieve insanely high score through automation ✓ Extract protocol via man-in-the-middle attack
  37. Backup slides

  38. Pinned certificate (installed at dev. time) App Server Get current

    server certificate 1 Compare current and pinned certificates 2 if identical: establish connection else: reject 3 Certificate Pinning