Android RE - Quizduell

Die Android Applikation “Quizduell” testet in abwechselnden Spielrunden das eigene und gegnerische Wissen diverser Themengebiete. Wer nach 6 Runden die meisten der Multiple Choice Fragen richtig beantworten konnte, gewinnt und steigt im Ranking auf.

Der eigentliche Fragebildschrim sieht dabei so aus:

Ziel ist es, schon vor dem Auswählen einer Antwort zu wissen, ob diese richtig ist oder nicht. Nach der Auswahl wird die richtige Antwort Grün markiert, alle anderen Rot. Wir müssen also die Activity untersuchen, was nach dem Klicken auf eine Antwort passiert und den Code zum setzen der Button-Hintergrundfarbe schon beim Laden der Activity aufrufen.

Wie schon beim Billiard RE decompilen wir die App zunächst mit apktool, um das Manifest, Strings und Smali-Code zu erhalten. Dann entpacken wir die App und wandeln den .dex-Code in eine jar-Datei um, welche in JD-Gui geladen wird. Doch welch Überraschung, die Klassen sind obfuscated und nur ein paar Activites erkennbar:

Der eigentliche Quizbildschirm ist gut versteckt und nicht unter den benannten Activities zu finden. Also wählen wir einen anderen Weg: Wir suchen einen (statischen) String, der nur in dieser gesuchten Activity zu finden ist. Die Fragen und Antworten sind dafür ungeeignet (dynamisch geladen), aber was ist mit “Runde X gegen Y”? In der Strings.xml sind alle Strings mit internem Namen zu finden. Durch diesen internen Namen erhalten wir über die Public.xml die “Namen -> Token” Verknüpfung. Die Suche nach “Runde” liefert Erfolg!

<string name=”game_round_vs_xx” formatted=”false”>Runde %d gegen %s</string>
<public type=”string” name=”game_round_vs_xx” id=”0x7f0b0053″ />

 

Also konvertieren wir die Hex Value in unser Dezimalsystem (JD-Gui stellt diese nur als 10er Basis dar -.-) und durchsuchen den zuvor erstellten Sourcedump. Voila! Wir finden die Klasse “aC”, welche von RelativeLayout erbt. Also ist diese auch darstellbar. Folgenden Source ist für uns interessant:

// Der String game_round_vs_xx
String str = getResources().getString(2131427411);
// Neues Object mit 2 Einträgen
Object[] arrayOfObject = new Object[2];
// Das ist also die Rundennummer in param4
arrayOfObject[0] = Integer.valueOf(paramInt4);
// paramD.c() liefert wohl den gegnerischen Namen
arrayOfObject[1] = paramD.c();
// Formatiere und setze den Text
localTextView.setText(String.format(str, arrayOfObject));

Allerdings ist ist hier von den gesuchten Fragen/Antworten nichts zu sehen! Dieses Layout scheint nur den oberen “Header” zu beinhalten. Also suchen wir, welche Klasse die von uns gefundene instanziert. Eine Suche nach “new aC(” liefert ein Resultat in der “NewGameActivity.java” und einer ominösen “h.java”. Letzere ist der gesuchte Quizbildschirm, oder zumindest die dahinterstehende Logik.

Die Klasse ist schlimm obfuscated, Typenamen und lokale Namen sind jeweils nur mit Buchstaben bezeichnet und auch sonst gibt der Source nicht viel her. Nun heißt es, Variablen und Klassen zurück zu verfolgen und ihnen eine Bedeutung zu geben. Nach ein weiteren paar Minuten dieses analysierens konnte ich wiederrum über die schon bekannte Gegnernamen-Variable eine Zuordnung der “schon vom Gegener ausgewählte Antwort” finden. Dies passierte in folgender Loop:

for (int i3 = 0; i3 &lt; this.d.size(); i3++) // Loop throuhg all answers ?
    {
      // Hol die Antwort an dem Index i3
      b CurrentAnswer = (b)this.d.get(((Integer)localArrayList.get(i3)).intValue());
      // Initialisiere sie?
      CurrentAnswer.c();
      // Hole dir die interne Antwortklasse mit dem Index i3
      z localz = localr.c(i3);
      // Ist die Antwort richtig, dann setze die RightAnswer-Variable
      if (localz.b())
        RightAnswerQ = CurrentAnswer;
      // Initialisiere sie?
      CurrentAnswer.a(localz);
      w localw = localr.d();
      // Wenn: Enemy zuerst dran war und die Antwort ID des Enemies der aktuellen Antwort ID entspricht
      // Setze EnemySelectedAnswer auf die aktuell durchlaufene Antwort 
      if ((localw.d()) &amp;&amp; (localw.a().c() == CurrentAnswer.a().c()))
        EnemySelectedAnswerQ = CurrentAnswer;
    }

Ziemlich verwirrend, nicht? Nunja, alles was uns interessiert ist die neue Klasse “b”. Die Klasse “b”, die wohl die Antwort-Klasse zu sein scheint. Wir haben dabei ein Auge auf die “z.b()”-Funktion, die wohl zurückgibt, ob die Antwort richtig ist oder nicht.

Lange Rede kurzer Sinn, hier wird also in der b-Klasse die Antwort gecheckt und dementsprechend Booleans gesetzt: (Natürlich wieder nach Analyse und umbenennen der Variablen)

 public final void b()
  {
    if (this.RealAnswerClass.CheckIsRight())
    {
      this.IsWrongAnswer = false;
      this.IsCorrectAnswer = true;
      return;
    }
    this.IsWrongAnswer = true;
    this.IsCorrectAnswer = false;
  }

Am Anfang der Klasse finden wir folgende Arrays:

public final class b extends FrameLayout
{
  private static final int[] i = { 2130772076 };
  private static final int[] j = { 2130772077 };

Wenn wir diese Zurücktracen, so lauten die Werte:

<public type=”attr” name=”state_correct” id=”0x7f01006c” />
<public type=”attr” name=”state_wrong” id=”0x7f01006d” />

Sieht also gut aus. Und wo werden diese gesetzt?

protected final int[] onCreateDrawableState(int paramInt)
  {
    int[] arrayOfInt = super.onCreateDrawableState(paramInt + 2);
    if (this.IsCorrectAnswer)
      mergeDrawableStates(arrayOfInt, i);
    if (this.IsWrongAnswer)
      mergeDrawableStates(arrayOfInt, j);
    return arrayOfInt;
  }

Diese States verweißen also auf eine Art Templateklasse (wie in CSS), die u.a. die Backgroundcolor des Buttons festlegt. Und genau diese obere Funktion wird einmal nach dem Init der Antwort aufgerufen, und einmal nach einem Click Event. Wir müssen es also schaffen, dass die Booleans “IsCorrect/WrongAnswer” nach dem Initalisieren nicht beide auf false stehen, sondern einen Wert annehmen. Und da hatten wir doch die b()-Funktion, die genau diese Booleans setzt!

Also fügen wir im Smali-Code einen Aufruf der b()-Funktion ein, nachdem der Text des Buttons (und somit auch die interene Antwortklasse) der Antwort gesetzt ist. Diese Funktion zum setzen der Antwort sieht wie folgt aus:

public final void a(z paramz)
  {
    this.RealAnswerClass = paramz;
    this.ButtonTextView.setText(paramz.a());
  }

Und in Smali fügen wir nun nach der setText-Instruction unseren Methodenaufruf ein:

invoke-virtual {v0, v1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
invoke-virtual {p0}, Lse/feomedia/quizkampen/views/b;->b()V

Dabei ist p0 der this-Pointer und b() die aufzurufende Funktion.

Das Resultat builden wir wieder mit apktool und signieren es noch mit einem Testkey. Nun wird die App installiert und ausprobiert. Und nunja, es geht :D

Auch wenn dieser Blogeintrag etwas trocken geworden ist, so wollte ich doch zeigen, wie man an solch unbekannte Applikationen herrangeht, sich von Klasse zu Klasse hangelt und versucht, das gesammte Konzept hinter dem verschleierten Code zu verstehen. Und ein solch simpler Patch freut einen jedes Reverserherz, denn es zeigt, dass man das man die Logik des Codes verstanden hat und sie somit auch kontrollieren kann.

Greez Easy

11 people like this post.
    • Pain
    • 21. Nov. 2013 10:46am

    mal wieder ein super Beitrag. Danke!

    • Chris
    • 4. Jan. 2014 6:37pm

    Hi. Ich konnte erfolgreich dekompilieren und auch wieder zurück kompilieren.
    Nun wollte ich mir den Code mal genauer anschauen und habe dazu eine Frage: der .dex-Code soll in eine jar-Datei umgewandelt werden um sie mit JD-Gui zu laden. Wie konvertiere ich diese?

    Zuerst habe ich auch den entsprechenden Aufruf in die smali, der, wie du sagst, bei dir geklappt hat, eingefügt. Jedoch führt dies bei meiner gebauten apk zum Absturz sobald ich eine neues Spiel starte und die Kategorie gewählt habe. Hast du eine Idee dazu? (v1.2.9)

      • Easysurfer
      • 6. Jan. 2014 7:17pm

      Schaue Dir mal den anderen Blogpost zu Android Reversing an. Dort wird das Tool Dex2Jar vorgestellt.

      Bist Du sicher, dass Du den richtigen Methodenaufruf in die richtige Methode plaziert hast? Müsste so gehen :)
      Greez

    • Chris
    • 6. Jan. 2014 9:19pm

    Danke hat funktioniert. War tatsächlich in der Falschen Datei.

    Nachdem ich die Änderung vorgenommen habe scheint ein Login mit facebook nicht mehr möglich zu sein. Änderungen an den Class-Dateien ohne neu zu signieren ist nicht möglich, da es allein schon dem Prinzip verstößt, oder? Habe die zurückgebaute apk durch ZipSigner mit einem auto-testkey signiert, um sie auf dem Handy installieren zu können.

      • Easysurfer
      • 6. Jan. 2014 11:08pm

      Den Login mit Facebook habe ich nie gebraucht, daher kann ich Dir nicht sagen ob da ein Art Hash oder so mit übertragen wird. Musst Du anschauen und dementsprechend umgehen. Aber die Facebook API ist ja soweit ich weiß ganz gut Dokumentiert…

      Änderungen ohne Neusignierung sind nicht möglich, nein. Aus dem Zip Archiv wird auch wieder ein Hash erstellt, dies ist die Signatur. Mehr kann ich Dir dazu leider nicht sagen, weiß ich zu wenig drüber. Viel Erfolg!

      • Inge
      • 14. Jan. 2014 2:37pm

      Bei mir kommt auch der Absturz. Welche ist denn die richtige Datei, inkl. Pfad? Und muss ich die .line Einträge auch manipulieren?

    • Inge
    • 14. Jan. 2014 2:14pm

    Kannst Du mal konkret sagen, wie die geänderte SMALI aussehen muss? Muss ich die .line Einträge auch manipulieren?

    Und - hättest Du nicht Lust, Dir den Facebook-Login auch mal anzusehen? :)

      • Easysurfer
      • 15. Jan. 2014 8:51pm

      Wenn ich sage wie das geänderte SMALI aussehen muss, so ist Quizduell bald voll von diesem Hack. Beschäftige Dich etwas damit, schau Dir die Java Datei per jar2dex und jdgui an und dann solltst Du es bald finden ;)

        • Inge
        • 20. Jan. 2014 4:30pm

        Ok, das Ding stürzt jetzt nicht mehr ab, aber der Facebook-Login klapp halt nicht…

          • Easysurfer
          • 20. Jan. 2014 8:06pm

          Dann müsst Ihr wohl nen Weg finden das zu umgehen ;)
          Viel Erfolg

    • Fork
    • 18. Jan. 2014 2:42am

    Gute Arbeit Easysufer ;)

    Funktioniert einwandfrei im Emulator.
    Auf dem Handy verweigert es aber die Installation(Adb zeigt Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES]) obwohl ich es gesignt habe.
    Ich vermute es liegt an der com.android.vending.CHECK_LICENSE Berechtigung die es benötigt.
    Nachdem ich es durch AntiLVL gejagt habe lässt es sich zwar installieren aber nicht mehr öffnen ;]
    Aber auch egal, Hauptsache was gelernt. Wirklich Spass macht es ja eh nicht mehr mit den Lösungen.

    Danke

      • Easysurfer
      • 18. Jan. 2014 4:32pm

      Womit signierst Du denn? Ich hab mit ZipSigner (App) und dem Testkey bisher nie Probleme gehabt.
      Gerne, freut mich dass Du was lernen konntest! :)

        • Fork
        • 19. Jan. 2014 1:32pm

        Ok danke für den Tipp, mit dem ZipSigner funktioniert es.
        Ich hatte es mit dem Jarsigner aus dem JDK7 signiert.

    • Squall
    • 28. Jan. 2014 9:57pm

    Danke für den tollen Beitrag. :D

    Was mich noch interessieren würde: kennst du eine Möglichkeit, die Daten auszugeben, die an feomedia.se gesendet werden? Die Verbindungen laufen leider über HTTPS, einen Proxy dazwischenzuschalten hat also nicht gefruchtet. Ich würde gerne das dahinterliegende API nachvollziehen können. ;)

    Viele Grüße
    Squall

      • Easysurfer
      • 29. Jan. 2014 11:10am

      Soweit ich weiß läuft die Server Connection eh verschlüsselt ab. Du müsstest Dich also vor dem eigentlichen Verschlüsseln und senden reinklemmen. Soweit ich mich erinnere ist in dem Screen mit den Fragen auch eine Methode, die nur eine andere statische Aufruft. Dies sendet die Frage AFAIK. Da kannst Du mal weiter nachforschen.

      • Sebastian
      • 10. Feb. 2014 7:31pm

      Hi :) Frage mich das gleiche und würde daher mal gern nachhaken, ob ihr bereits irgendwas herausgefunden habt in die Richtung?

      Grüße

    • Mirko
    • 29. Jan. 2014 7:30pm

    kann mir mal jemand eine fertige apk hochladen und den Link schicken bitte?

    • Ahmet
    • 29. Jan. 2014 11:23pm

    Ja wäre echt cool die fertige apk hochzuladen, falls es nicht zu viel Aufwand ist bitte ich auch darum :D

    • Tim
    • 31. Jan. 2014 8:23pm

    Gibt es eine APK zum downloaden auch für die Premiumversion?

    • Christoph
    • 4. Feb. 2014 11:39am

    Mich würde auch mal interessieren, ob es eine fertige APK zum download gibt, da meine Fähigkeiten in diesem Bereich gleich null sind :D Würde aber trotzdem gerne mal so eine APK testen

    • Unknown
    • 8. Feb. 2014 1:26am

    Für alle die das selbe Problem haben wie ich:
    Nutzt einfach Smali-Me um die classes.dex zu decompilen und nach den Änderungen wieder zu compilen. Danach die gemoddete dex wieder in die apk mergen und überschreiben und danach mit Zip-Signer signieren.

    Alle anderen Methoden mit dem apktool sind bei mir fehlgeschlagen, da die App dann immer beim Versuch sich anzumelden, egal ob über Facebook oder nicht, abgestürzt ist. Da hat auch kein erneutes Signieren geholfen…

    Den Rest müsst ihr euch selbst anlesen, sollte aber kein Problem sein, wenn man ein bisschen Zeit über hat^^

    • jdistlr
    • 17. Feb. 2014 1:25am

    Genau aus diesen Gruenden hat wohl feomedia nur die APK obfuscated - und nicht die IPA fuer iPhones. Weil im kaputten Java-Kontext sich mittlerweile jeder austoben kann. ;-) Wenn man die App dann mal wirklich grundlegend reversed hat, stoesst man auf eine REST/JSON API, die mit OAuth gesichert ist.

    Wenn du also noch eine Herausforderung brauchst, dann such nach den zwei Secret-Keys in der App, die fuer den Login und die Authorization im HTTPS verantwortlich sind - erst dann geht der Spass so richtig los.

    Dennoch. Gute Arbeit, Kollege!

      • Rodney
      • 21. Mrz. 2014 5:16pm

      Die HTTPS verbindung lässt sich bei der iOS variante der App entschlüsseln (Fiddler ftw), anscheinend hat da einer Mist gebaut :P

      Was ich sagen kann ist das die Anfragen eine Authorization Header haben der eine base64 encodierte Prüfsumme (HMACSHA256 laut code) hat. Diese Prüfsumme wird aus Datum und anscheinend dem Body (bei POST) und der Request URL gebildet.
      Der Key ist der User-Agent soweit ich das per jd-gui nachvollziehen kann.

      Ich war aber mit meinem beschränkten verständnis von Java und Android programmierung nicht in der lage die genau kombination nachzubauen.

      Wen es interessiert, der kann mal in “/smali/se/feomedia/quizkampen/c/f.smali” nachschauen (oder in jd-gui dann se.feomedia.quizkampen.c.f).

    • JP3300
    • 12. Mrz. 2014 8:23pm

    Ich fände es sehr toll, wenn du die fertig bearbeitete APP (apk) hochladen oder mir per E-Mail schicken könntest!

    Gruß, JP3300

    • Marki
    • 20. Mrz. 2014 5:46pm

    Auch spannend für alle die sich mal den Traffic ansehen wollen:
    Das SSL-Cert ist in der App gespeichert (nach dem Decompilen unter ‘res/raw/ts.bks’) und damit wird vor dem Verbinden geschaut, ob es sich um den korrekten Server handelt. Nun kann man aber folgendes machen:

    - Eigenes Cert erstellen für MITM
    - Mit dem “Keytool” das Cert in den Keystore importieren (das Passwort für den Keystore steht im Code - einfach mal ein paar “Passwort-ähnliche Strings” testen ;)):
    keytool -keystore ts.bks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -importcert -trustcacerts -file server.crt
    - Neu packen + signieren
    - Auf dem Handy die Hosts editieren (alternativ eigener DNS Server oder ähnliches):
    echo “192.168.0.xxx qksvandroid.feomedia.se” >> /etc/hosts
    echo “192.168.0.xxx qksvfreedroid.feomedia.se” >> /etc/hosts
    echo “192.168.0.xxx qkgermany.feomedia.se” >> /etc/hosts
    - Und starten

    Happy sniffing ;)

    {
    “game”: {
    “your_answers”: [],
    “state”: 10,
    “questions”: [
    {
    “wrong3″: “Anthroposophie”,
    “wrong2″: “Junge Kirche”,
    “wrong1″: “Laestadianismus”,
    “cat_name”: “Glaube & Religion”,
    “q_id”: XXX,
    “timestamp”: XXX,
    “cat_id”: 4,
    “question”: “Nach welcher Bewegung kann der Mensch erst frei und glücklich sein, wenn er von christlichen Begriffen, wie z.B. Sünde befreit wurde? “,
    “correct”: “Church of Satan”
    },
    [...]
    }
    }

    • Martin
    • 10. Apr. 2014 5:53pm

    Hi Marki & Rodney,

    eure Kommentare haben mir wirklich geholfen, ein Interface für die Quizduell-API zu schreiben… das Resultat ist dieser Bot http://quizduellbot.appspot.com :D

    Ich werde das API Interface (python) noch auf github hochladen.

    @Rodney:
    Der Authentifizierungs-Code wird gebildet aus ‘https://’ + host_name + url + client_date + sortierte POST parameter values, der verwendete Key lautet für Android ‘irETGpo…’ (schnell im Code zu finden).

  1. Hab mich auch mal dran versucht einen API client zu schreiben. Das ist bei mir rausgekommen: https://github.com/jcla1/goquizduell
    Schaut mal rein, es ist auch ein automatischer Spieler mit beinhaltet.
    Natuerlich waere dies ohne eure brilliante Vorarbeit nicht moeglich gewesen!

    • Joshimoo
    • 13. Mai. 2014 5:11pm

    Hier findet ihr meine C# API Implementation:
    https://github.com/joshimoo/QuizDuell

    Die Api unterstützt den IOS sowie Android Kontext.

    Konstruktive Kritik ist sehr erwünscht.
    Und auch von mir ein großes Dankeschön an die fleißigen Kommentatoren.

    • tr4ceflow
    • 20. Mai. 2014 9:50pm

    Musste grad beim Akte Beitrag aus dem Fernsehen an den Post denken. Wusste gar nicht, dass es einen solchen Hype um das Spiel gibt.

    • ben
    • 11. Aug. 2014 11:27am

    quizduell app was updated.
    apis mentioned above do not work anymore :-(

  2. Quizduell wird inzwischen mit DexGuard kompiliert. Keystore und viele Strings sind mit AES verschlüsselt.

    https://github.com/mtschirs/quizduellapi ist jetzt wieder mit der aktuellen Version 1.4.9 kompatibel.

    • ben
    • 3. Jul. 2015 11:32am

    Thank you for your excellent work!

    But with 1.8.0 a new access prevention mechanism seems to be in place based on Google Play validation.

    Is there a solution for this issue available?

  1. Noch keine TrackBacks.