lundi 4 avril 2011

Prequals NDH2011: RCE200 (Android)

On va commencer par l'épreuve de reversing Android.
C'était la première fois que je jouais avec de l'Android, seems fun :).

Introduction

On avait à disposition une simple application dans laquelle il nous fallait parler.

Au lancement de l'application, nous somme accueillis par le screen suivant (sans le petit texte que j'ai ajouté :)):

 Il fallait prononcer un mot correctement pour obtenir le flag:
Non ce n'est pas le flag :).
J'ai pas réussi à l'obtenir par ce biais.

Pour ce challenge, plusieurs tools étaient disponibles, tel que Dex2Jar ou APKTool par exemple.
Ils ont plusieurs avantages et inconvénients, Dex2Jar nous sort du bytecode Java qu'on peut décompiler avec JD mais nous n'avons pas la possibilitée de facilement modifier l'APK.
APKTool nous permet de modifier l'APK et de le reconstruire correctement, par contre pas forcément évident à trouver la routine qui nous intéresse (à coup de grep ça peut se faire :)).

J'ai fais usage de la premire méthode pour analyser le programme:
dex2jar.sh RCE200.apk
jd-gui RCE200.apk.dex2jar.jar

Et le deuxième tool pour ajouter mon texte et patcher une condition afin de montrer un hash tous le temps (normalement il doit y avoir un affichage que lorsque le bon mot est prononcé).
Je me suis donc fais 2 helpers pour manipuler facilement le dump smali obtenu:

depack.sh
#!/bin/sh

cd apktool
java -jar apktool.jar d ../$1
mv `echo $1 | cut -d '.' -f 1` ../

pack.sh
#!/bin/sh

# if android key does not exist, we create one
if [ ! -f ~/keystore-android ]
then
    keytool -genkeypair -v -keystore ~/keystore-android -alias rce200 -keyalg RSA -keysize 2048
fi

#
if [ -f $1.aligned.apk ]
then
    rm $1.aligned.apk
fi

cd apktool
export PATH=./:$PATH
java -jar apktool.jar b ../$1
cd ../
jarsigner -verbose -keystore ~/keystore-android $1/dist/$1.apk rce200
cp $1/dist/$1.apk ./$1.rebuilt.apk
./zipalign -v 4 $1.rebuilt.apk $1.aligned.apk
rm $1.rebuilt.apk


Let's reverse it!

Après avoir transformer notre .apk and .jar, nous ouvrons celui-ci avec jd-gui et on se retrouve avec 4 sources .java: ReverseMe.java, a.java, b.java et c.java.

Le premier fichier (ReverseMe.java):

package ndh.prequals.rce;

import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;

public class ReverseMe extends Activity
{
  private a a = null;
  private TextView b = null;

  protected void onActivityResult(int paramInt1, int paramInt2, Intent paramIntent)
  {
    if ((paramInt1 == 1234) && (paramInt2 == -1))
    {
      ArrayList localArrayList = paramIntent.getStringArrayListExtra("android.speech.extra.RESULTS");
      if ((!localArrayList.isEmpty()) && (a.b((String)localArrayList.get(0))))
      {
        TextView localTextView = this.b;
        String str = a.a((String)localArrayList.get(0));
        localTextView.setText(str);
      }
    }
    super.onActivityResult(paramInt1, paramInt2, paramIntent);
  }

  public void onCreate(Bundle paramBundle)
  {
    super.onCreate(paramBundle);
    setContentView(2130903040);
    Button localButton = (Button)findViewById(2131034114);
    TextView localTextView = (TextView)findViewById(2131034113);
    this.b = localTextView;
    PackageManager localPackageManager = getPackageManager();
    String str1 = c.d();
    Intent localIntent = new Intent(str1);
    if (localPackageManager.queryIntentActivities(localIntent, 0).size() != 0)
    {
      String str2 = c.b();
      String str3 = Build.PRODUCT;
      if (!str2.equals(str3))
      {
        b localb = new b(this);
        localButton.setOnClickListener(localb);
      }
    }
    a locala = new a();
    this.a = locala;
  }
}

onCreate() va mettre en place les différents éléments de l'application à sa création comme l'image, le texte et le button.
onActivityResult() est le handler qui va afficher notre flag si nous prononçons le bon mot. (Patchez la condition pour toujours avoir un hash d'affiché :)).

Le deuxième fichier (a.java):

package ndh.prequals.rce;

import android.os.Build;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public final class a
{
  public static String a(String paramString)
  {
    try
    {
      MessageDigest localMessageDigest = MessageDigest.getInstance(c.e());
      byte[] arrayOfByte = paramString.getBytes();
      localMessageDigest.update(arrayOfByte);
      localObject1 = localMessageDigest.digest();
      StringBuffer localStringBuffer1 = new StringBuffer();
      int i = 0;
      int j = localObject1.length;
      if (i >= j)
      {
        localObject1 = localStringBuffer1.toString();
        return localObject1;
      }
      String str;
      for (Object localObject2 = Integer.toHexString(localObject1[i] & 0xFF); ; localObject2 = str)
      {
        if (((String)localObject2).length() >= 2)
        {
          StringBuffer localStringBuffer2 = localStringBuffer1.append((String)localObject2);
          i += 1;
          break;
        }
        str = "0" + (String)localObject2;
      }
    }
    catch (NoSuchAlgorithmException localNoSuchAlgorithmException)
    {
      while (true)
        Object localObject1 = null;
    }
  }

  public static boolean b(String paramString)
  {
    try
    {
      MessageDigest localMessageDigest = MessageDigest.getInstance(c.c());
      byte[] arrayOfByte1 = paramString.getBytes();
      localMessageDigest.update(arrayOfByte1);
      byte[] arrayOfByte2 = localMessageDigest.digest();
      StringBuffer localStringBuffer1 = new StringBuffer();
      String str1 = c.b();
      String str2 = Build.PRODUCT;
      if (str1.equals(str2))
        StringBuffer localStringBuffer2 = localStringBuffer1.append(65);
      int i = 0;
      int j = arrayOfByte2.length;
      if (i >= j)
      {
        String str3 = localStringBuffer1.toString();
        String str4 = c.a();
        bool = str3.equals(str4);
        return bool;
      }
      String str5;
      for (Object localObject = Integer.toHexString(bool[i] & 0xFF); ; localObject = str5)
      {
        if (((String)localObject).length() >= 2)
        {
          StringBuffer localStringBuffer3 = localStringBuffer1.append((String)localObject);
          i += 1;
          break;
        }
        str5 = "0" + (String)localObject;
      }
    }
    catch (NoSuchAlgorithmException localNoSuchAlgorithmException)
    {
      while (true)
        boolean bool = false;
    }
  }
}

La méthode a() nous renvoi le hash sha1 de la chaine qu'on met en paramètre et b() nous renvoi un hash MD5.

Le troisième fichier (b.java):
package ndh.prequals.rce;

import android.content.Intent;
import android.view.View;
import android.view.View.OnClickListener;

final class b
  implements View.OnClickListener
{
  b(ReverseMe paramReverseMe)
  {
  }

  public final void onClick(View paramView)
  {
    ReverseMe localReverseMe = this.a;
    if (paramView.getId() == 2131034114)
    {
      String str = c.d();
      Intent localIntent1 = new Intent(str);
      Intent localIntent2 = localIntent1.putExtra("android.speech.extra.LANGUAGE_MODEL", "free_form");
      Intent localIntent3 = localIntent1.putExtra("android.speech.extra.PROMPT", "Enter password");
      localReverseMe.startActivityForResult(localIntent1, 1234);
    }
  }
}

Cette classe a une unique méthode qui va lancer la boîte de dialogue de reconnaissance vocale.

Le dernier fichier (c.java):
package ndh.prequals.rce;

public final class c
{
  private static byte[] a = { 90, 5, 88, 88, 13, 13, 90, 90, 10, 4, 9, 11, 93, 90, 11, 15, 93, 95, 5, 93, 5, 8, 8, 88, 90, 95, 9, 14, 90, 8, 13, 94 };
  private static byte[] b = { 91, 83, 83, 91, 80, 89, 99, 79, 88, 87 };
  private static byte[] c = { 113, 120, 9 };
  private static byte[] d = { 93, 82, 88, 78, 83, 85, 88, 18, 79, 76, 89, 89, 95, 84, 18, 93, 95, 72, 85, 83, 82, 18, 110, 121, 127, 115, 123, 114, 117, 102, 121, 99, 111, 108, 121, 121, 127, 116 };
  private static byte[] e = { 111, 116, 125, 17, 13 };

  public static String a()
  {
    return a(a);
  }

  private static String a(byte[] paramArrayOfByte)
  {
    byte[] arrayOfByte = new byte[paramArrayOfByte.length];
    int i = 0;
    while (true)
    {
      int j = paramArrayOfByte.length;
      if (i >= j)
        return new String(arrayOfByte);
      int k = (byte)(paramArrayOfByte[i] ^ 0x3C);
      arrayOfByte[i] = k;
      i += 1;
    }
  }

  public static String b()
  {
    return a(b);
  }

  public static String c()
  {
    return a(c);
  }

  public static String d()
  {
    return a(d);
  }

  public static String e()
  {
    return a(e);
  }
}
Ici nous avons affaire à plusieurs chaînes de caractères obfusquée par un XORing avec une clé de 0x3C.

J'ai coder un rapide utilitaire pour me dé-obfusquer ces chaînes:
// author : m_101
// licence: beerware
// year   : 2011
// ctf    : ndh2011 prequals

#include <stdio.h>
#include <stdlib.h>

void decrypt(unsigned char *encrypted) {
    size_t idxEnc;
    int c;

    printf("Decrypted: '");
    for (idxEnc = 0; encrypted[idxEnc] != 0; idxEnc++) {
        c = encrypted[idxEnc] ^ 0x3c;
        printf("%c", c); 
    }
    printf("'\n");
}

int main (int argc, char *argv[]) {
    unsigned char a[] = {
        90, 5, 88, 88, 13, 13, 90, 90,
        10, 4, 9, 11, 93, 90, 11, 15,
        93, 95, 5, 93, 5, 8, 8, 88,
        90, 95, 9, 14, 90, 8, 13, 94,
        0
    };
    unsigned char b[] = { 91, 83, 83, 91, 80, 89, 99, 79, 88, 87 };
    unsigned char c[] = { 113, 120, 9 };
    unsigned char d[] = {
        93, 82, 88, 78, 83, 85, 88, 18,
        79, 76, 89, 89, 95, 84, 18, 93,
        95, 72, 85, 83, 82, 18, 110, 121,
        127, 115, 123, 114, 117, 102, 121, 99,
        111, 108, 121, 121, 127, 116,
        0
    };
    unsigned char e[] = { 111, 116, 125, 17, 13, 0 };

    decrypt(a);
    decrypt(b);
    decrypt(c);
    decrypt(d);
    decrypt(e);


    return 0;
}

Vous obtenez ceci:
$ ./decode
Decrypted: 'f9dd11ff6857af73ac9a944dfc52f41b'
Decrypted: 'google_sdk|'
Decrypted: 'MD5'
Decrypted: 'android.speech.action.RECOGNIZE_SPEECH'
Decrypted: 'SHA-1'

On trouve un hash MD5, tiens tiens ...
Une petite recherche google nous donne ceci:
md5(salope) = f9dd11ff6857af73ac9a944dfc52f41b

Donc au final, l'application va faire un hash sha1 du mot qu'on prononce et l'afficher si celui-ci est correct.
Je n'ai pas réussi à l'obtenir par ce biais, mais nous savons que c'est un sha1.
$ printf "salope" | openssl dgst -sha1
913beccad686975f8c686d9b3b1ee6bb97c22d6f

Et voilà, done :).

J'espère que ce rapide tour d'horizon du reversing Android vous a plut.
J'ai mis plus de doc en lien si vous voulez approfondir ;).

Je n'ai pas encore fini de reverser le RCE300 par contre, donc l'article de reversing NDS va attendre un peu.

Cheers,

m_101

- ReverseMe: RCE200
- Tool: dex2jar
- Tool: apktool
- Doc: DalvikVM
- Doc: dalvik opcodes
- Doc: Reversing Android par virtualabs
- Doc: Primer on Android OS Reversing by ARTeam

2 commentaires :

  1. t'as pu le runner dans un emulateur ? le speech marchait pas mais bon c'etait assez facile pour le resoudre en lisant le java ;)

    RépondreSupprimer
  2. Non, je l'ai runné sur mon téléphone :).
    C'était l'épreuve que j'ai le plus aimé, j'ai bien joué avec apktool (modification de l'APK, etc :)).

    RépondreSupprimer