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:

Screenshot_2013-11-19-16-24-22

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:

Temp

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 😀

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

0

34 Comments

  • Antworten Chris |

    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)

    0
  • Antworten Chris |

    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.

    0
    • Antworten Easysurfer |

      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!

      0
    • Antworten Inge |

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

      0
  • Antworten Inge |

    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? 🙂

    0
    • Antworten Easysurfer |

      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 😉

      0
  • Antworten Fork |

    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

    0
    • Antworten Easysurfer |

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

      0
      • Antworten Fork |

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

        0
  • Antworten Squall |

    Danke für den tollen Beitrag. 😀

    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

    0
    • Antworten Easysurfer |

      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.

      0
    • Antworten Sebastian |

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

      Grüße

      0
  • Antworten Ahmet |

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

    0
  • Antworten Christoph |

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

    0
  • Antworten Unknown |

    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^^

    0
  • Antworten jdistlr |

    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!

    0
    • Antworten Rodney |

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

      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).

      0
  • Antworten JP3300 |

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

    Gruß, JP3300

    0
  • Antworten Marki |

    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“
    },
    […]
    }
    }

    0
  • Antworten Martin |

    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 😀

    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).

    0
  • Antworten tr4ceflow |

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

    0
  • Antworten ben |

    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?

    0

So, what do you think ?