Reversing (150)

Solves: 17

Please install this on your new android phone, enter pass code and get the flag.

Hints:

  • Updated the libdroid.apk, this one is now also able to run on a device. No changes to internal logic.

If the above link doesn’t work, please use this link.

Decompiling the APK

First things first, rename the libdroid_fixed.apk to libdroid_fixed.zip and unzip it. Then there should be a classes.dex file, and we have dex2jar.

On Windows,

d2j-dex2jar.bat classes.dex

Otherwise,

dsj-dex2jar.sh classes.dex

Then we can obtain classes-dex2jar.jar, now it’s JD-GUI’s turn. Open the jar file with JD-GUI and you will see the content of class ctf.stratumauhhur.libdroid.a.

JD-GUI navigating classes-dex2jar.jar

The full content of the a.class is available on this gist.

There are quite many methods with name a in class ctf.stratumauhhur.libdroid.a:

static String a(String paramString, int paramInt) {...}
public void a(View paramView) {...}
void a(String paramString) {...}
byte[] a(byte[] paramArrayOfByte, String paramString) {...}

At first, static variables of the class are assigned, and since the class is an activity, protected void onCreate(Bundle paramBundle) should be called.

The following is the code assigning static variables:

static String a;
static String b;
static String c;
static String d;
static String f;
static String flag = a(getFlag(), 1);
static String g;
String e;

static
{
  System.loadLibrary("libdroid");
  a = a(getOperatingSystem(), 1);
  b = a(getPhoneNumber(), 1);
  c = a(installRootkit(), 1);
  d = a(generateConfusion(), 1);
  f = a(obtainWorldDomination(), 1);
  g = a(installiOS(), 1);
}

The method static String a(String paramString, int paramInt) is used for the static variables, and getFlag(), getOperatingSystem(), getPhoneNumber(), installRootkit(), generateConfusion(), obtainWorldDomination(), installiOS() are native methods.

We should look into the static String a(String paramString, int paramInt). See the following lines:

Object localObject1 = new Exception().getStackTrace()[paramInt];
Object localObject2 = new StringBuilder();
((StringBuilder)localObject2).append(((StackTraceElement)localObject1).getClassName()).insert(paramInt, ((StackTraceElement)localObject1).getMethodName());
localObject1 = localObject2.toString();

The paramInt is always 1, so localObject1 is new Exception().getStackTrace()[1], and it’s the name of class is ctf.stratumauhhur.libdroid.a, and the name of method is <clinit>. So, the value of localObject1 will be "c<clinit>tf.stratumauhhur.libdroid.a".

The native methods

To know the behavior of native methods, we should decompile lib/x86/liblibdroid.so file using IDA, OllyDbg, or something. By looking into it, we can easily know that the native methods just return constant string. This piece of Ruby code will translate them.

def a(str, int)
  obj1 = 'c<clinit>tf.stratumauhhur.libdroid.a'
  obj2 = [0] * str.size
  i = 0
  int = obj1.size
  loop do
    if i < str.size
      j = str[i].ord
      obj2[i] = (obj1[int - 1].ord ^ j ^ 0x12)
      i += 1
      if i >= str.size
        return obj2.select { |v| v != 0 }.map(&:chr).join
      end
    else
      return obj2.select { |v| v != 0 }.map(&:chr).join
    end
    j = str[i].ord
    obj2[i] = (obj1[int - 1].ord ^ j ^ 0xFA)
    int -= 1
    i += 1
    if int <= 0
      int = obj1.size
    end
  end
end

getOperatingSystem = "\x10\xF4\x52\xB2\x1F\xF9\x55\xFA\x13\xFC".bytes
getPhoneNumber = "\x11\xF7\x5D\xB6\x1A\xFF\x19\xFF\x1C\xF7\x0C\xE9".bytes
generateConfusion = "\x53\xAA\x0E\xE7\x42\xAB\x4D\xA4\x45\xAC\x50".bytes
getFlag = "\x20\xF4\x4E\xA6\x0F\xBE\x15\xFC\x5D\xE7\x0F\xE7\x02\xF5\x19\xEC\x5B\xF5\x11\xE4\x1C\xAD\x0F\xFD\x47\xB5\x52".bytes
installRootkit = "\x30\xF4\x52\xB3\x04\xFF\x0F\xE6\x11\xF4".bytes
obtainWorldDomination = "\x18\xFE\x45\xE9".bytes
installiOS = "\x01\xF4\x53\xA0\x1D\xF7\x0F\xAE".bytes

_flag = a(getFlag, 1)
# => "Sorry no rootkit for you :("
_a = a(getOperatingSystem, 1)
# => "config.ini"
_b = a(getPhoneNumber, 1)
# => "blablablabla"
_c = a(installRootkit, 1)
# => "Congratula"
_d = a(generateConfusion, 1)
# => " 1234567890"
_f = a(obtainWorldDomination, 1)
# => "key="
_g = a(installiOS, 1)
# => "rootkit="

Decompiling onCreate

Now we have static variables, so the next thing is protected void onCreate(Bundle paramBundle):

protected void onCreate(Bundle paramBundle)
{
  super.onCreate(paramBundle);
  setContentView(2130968601);
  try
  {
    a(a);
    this.e = "";
    return;
  }
  catch (Exception paramBundle)
  {
    for (;;) {}
  }
}

The method is void a(String paramString), and the variable a is static variable that we have calculated right before, which is "config.ini". Then let’s look into the method.

void a(String paramString)
  throws Exception
{
  paramString = getAssets().open(paramString);
  Object localObject = new ByteArrayOutputStream();
  byte[] arrayOfByte = new byte[0x4000];
  for (;;)
  {
    int i = paramString.read(arrayOfByte, 0, arrayOfByte.length);
    if (i == -1) {
      break;
    }
    ((ByteArrayOutputStream)localObject).write(arrayOfByte, 0, i);
  }
  ((ByteArrayOutputStream)localObject).flush();
  paramString = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(a(((ByteArrayOutputStream)localObject).toByteArray(), b))));
  for (;;)
  {
    localObject = paramString.readLine();
    if (localObject == null) {
      break;
    }
    if (((String)localObject).startsWith(g)) {
      g = ((String)localObject).substring(g.length());
    }
    if (((String)localObject).startsWith((String)f)) {
      f = (byte[])Base64.decode(((String)localObject).substring(((String)f).length()), 0);
    }
  }
}

It reads an asset with its name paramString into localObject, and then execute byte[] a(byte[] paramArrayOfByte, String paramString) with localObject and b.

Again, this is the Ruby version of the code. You can find config.ini in the assets directory.

def byte_a(paramArrayOfByte, paramString)
  arrayOfByte = [0] * paramArrayOfByte.size
  arrayOfByte.size.times do |i|
    arrayOfByte[i] = (paramArrayOfByte[i].ord ^ paramString[i % paramString.size].ord)
  end
  arrayOfByte.map(&:chr).join
end

config_ini = "^\x1E\x0E\r\x18_h\a\x04\eQ(3/&\x16CJ%40;\x18,#Q\\h\x1E\x0E\r" \
  "\x18\n\v\x18\\\x03C\x05M\x0FN\x01B\x05\x03\x18k^C\x13\r\x03\x15\\"
puts paramString = byte_a(config_ini, _b)
# <root>
# key=IQCGt/+GXQYtMA==
# rootkit=a/d/c/c.dat
# </root>

So g will be replaced with "a/d/c/c.dat", and f will be replaced with the value of Base64.decode("IQCGt/+GXQYtMA=="), which is "!\x00\x86\xB7\xFF\x86]\x06-0".

The main logic

Now we’re almost close. There’s only public void a(View paramView) left to analyze.

if (paramView.getId() == 2131492969) {
  this.e += d.charAt(1);
}
if (paramView.getId() == 2131492970) {
  this.e += d.charAt(2);
}
if (paramView.getId() == 2131492971) {
  this.e += d.charAt(3);
}
if (paramView.getId() == 2131492972) {
  this.e += d.charAt(4);
}
if (paramView.getId() == 2131492973) {
  this.e += d.charAt(5);
}
if (paramView.getId() == 2131492974) {
  this.e += d.charAt(6);
}
if (paramView.getId() == 2131492975) {
  this.e += d.charAt(7);
}
if (paramView.getId() == 2131492977) {
  this.e += d.charAt(8);
}
if (paramView.getId() == 2131492978) {
  this.e += d.charAt(9);
}
if (paramView.getId() == 2131492976) {
  this.e += d.charAt(0);
}

Obviously, this handles a button click. d is " 1234567890", so we can think the button 1 to 9 is mapped to "1" to "9" respectively, and the button 0 is mapped to " ".

String str;
Object localObject2;
Object localObject1;
if ((this.e.length() == 6) || (paramView.getId() == 2131492979))
{
  str = flag;
  try
  {
    InputStream localInputStream = getAssets().open(g);
    localObject2 = new ByteArrayOutputStream();
    byte[] arrayOfByte = new byte[0x4000];
    for (;;)
    {
      int i = localInputStream.read(arrayOfByte, 0, arrayOfByte.length);
      if (i == -1) {
        break;
      }
      ((ByteArrayOutputStream)localObject2).write(arrayOfByte, 0, i);
    }
    Snackbar.make(paramView, (CharSequence)localObject1, 0).setAction("Action", null).show();
  }
  catch (Exception localException)
  {
    localException.printStackTrace();
    localObject1 = str;
  }
}
for (;;)
{
  this.e = "";
  return;
  ((ByteArrayOutputStream)localObject2).flush();
  localObject2 = ((ByteArrayOutputStream)localObject2).toByteArray();
  localObject1 = new byte[16];
  System.arraycopy((byte[])f, 0, localObject1, 0, ((byte[])f).length);
  System.arraycopy(this.e.getBytes(), 0, localObject1, 10, this.e.getBytes().length);
  phoneHome((byte[])localObject2, (byte[])localObject1);
  localObject1 = str;
  if (new String((byte[])localObject2).startsWith(c)) {
    localObject1 = new String((byte[])localObject2);
  }
}

Snackbar line makes a toast pop-up with the value of localObject1, which is flag by default, which is "Sorry no rootkit for you :(". When the value of localObject2 after the phoneHome(localObject2, localObject1) starts with the value of c, which is "Congratula", the localObject1 changes. So our goal is to find the value of e that leads localObject1 to change.

The value of localObject2 is the content of the asset with its path g, which is assets/a/d/c/c. The first 10 bytes of localObject1 are the same with f, and the last 6 bytes are the value of e. So we need to know the value of e, which will be at most six digits of number.

Emulating phoneHome

We can obtain a decompiled code of phoneHome, and this is the exploit.

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define _BYTE unsigned char
#define BYTEn(x, n) (*((_BYTE *) &(x) + n))
#define BYTE1(x) BYTEn(x, 1)
#define BYTE2(x) BYTEn(x, 2)
#define BYTE3(x) BYTEn(x, 3)

void check(char *input) {
    char g[11] = "Congratula";
    char a_d_c_c[113] = "\xFE\xA0\xAD\x80 Y\xAB\x12\xD7\xC3\x9C\x88\xFA,\x1D\xFC\x81""F\r\xDC\xE9\xCE\xCCWx\xF5""A_R\x02""6\xD5""3\x18""f:@&\xE8n\xB6\xCDr\xB7<\x01""f\xB1O\x99#c\x95w4ai\xF6\xA9S@7ACO\x98\x95,z'<\x98h\x1A\x88\xA8\xB7\x85\xBB\x15O\x1A\x01M\xC9\xC8\x9BuxW\x7F\x98\r\xD8Q\xA8\"\xB9^YMqO\x1A\x81\xA9\xBF\a)\xED\xFD\x83";
    int v4, v5, v6, v7, v8, v9, v10, v12, v13, v14, v15, v17, v18;
    unsigned int v16;

    v17 = a_d_c_c;
    v18 = input;
    v4 = v18;
    v5 = 112;
    v12 = (*(_BYTE *) (v4 + 2) << 16) | (*(_BYTE *) (v4 + 3) << 24) | *(_BYTE *) v4 | (uint16_t) (*(_BYTE *) (v4 + 1) << 8);
    v13 = (*(_BYTE *) (v4 + 6) << 16) | (*(_BYTE *) (v4 + 7) << 24) | *(_BYTE *) (v4 + 4) | (uint16_t) (*(_BYTE *) (v4 + 5) << 8);
    v14 = (*(_BYTE *) (v4 + 10) << 16) | (*(_BYTE *) (v4 + 11) << 24) | *(_BYTE *) (v4 + 8) | (uint16_t) (*(_BYTE *) (v4 + 9) << 8);
    v6 = (*(_BYTE *) (v4 + 15) << 24) | *(_BYTE *) (v4 + 12) | (uint16_t) (*(_BYTE *) (v4 + 13) << 8) | (*(_BYTE *) (v4 + 14) << 16);
    if (v5 > 0) {
        v15 = v17 + 1;
        v16 = v17 + ((v5 - 1) & 0xFFFFFFF8) + 9;
        do {
            v7 = (uint16_t) (*(_BYTE *) v15 << 8) | (*(_BYTE *) (v15 + 2) << 24) | *(_BYTE *) (v15 - 1) | (*(_BYTE *) (v15 + 1) << 16);
            v8 = 0xD5B7DDE0;
            v9 = (uint16_t) (*(_BYTE *) (v15 + 4) << 8) | *(_BYTE *) (v15 + 3) | (*(_BYTE *) (v15 + 6) << 24) | (*(_BYTE *) (v15 + 5) << 16);
            do {
                v9 -= (v7 + v8) ^ (16 * v7 + v14) ^ (v6 + ((unsigned int) v7 >> 5));
                v7 -= (v9 + v8) ^ (16 * v9 + v12) ^ (v13 + ((unsigned int) v9 >> 5));
                v8 += 0x21524111;
            } while (v8);
            *(_BYTE *) (v15 - 1) = v7;
            v10 = v15 + 8;
            *(_BYTE *) (v10 - 8) = BYTE1(v7);
            *(_BYTE *) (v10 - 7) = BYTE2(v7);
            *(_BYTE *) (v10 - 6) = BYTE3(v7);
            *(_BYTE *) (v10 - 5) = v9;
            *(_BYTE *) (v10 - 4) = BYTE1(v9);
            *(_BYTE *) (v10 - 3) = BYTE2(v9);
            *(_BYTE *) (v10 - 2) = BYTE3(v9);
            v15 += 8;
        } while (v10 != v16);
    }
    if (strncmp(v17, g, strlen(g)) == 0) {
        printf("Found: e: '%s'\n", input + 10);
        printf("%s\n", v17);
        exit(0);
    }
}

void replace_zero(char *f) {
    int i = 10;
    while (f[i] != '\0') {
        if (f[i] == '0') {
            f[i] = ' ';
        }
        i++;
    }
}

int main() {
    char f[17] = "!\x00\x86\xB7\xFF\x86]\x06-0";
    int e;
    int len;
    int i;

    for (e = 0; e < 1000000; e++) {
        for (i = 10; i < 17; i++) {
            f[i] = 0;
        }
        if (e < 10) {
            sprintf(f + 10, "%d", e);
            replace_zero(f);
            check(f);
        }
        if (e < 100) {
            sprintf(f + 10, "%02d", e);
            replace_zero(f);
            check(f);
        }
        if (e < 1000) {
            sprintf(f + 10, "%03d", e);
            replace_zero(f);
            check(f);
        }
        if (e < 10000) {
            sprintf(f + 10, "%04d", e);
            replace_zero(f);
            check(f);
        }
        if (e < 100000) {
            sprintf(f + 10, "%05d", e);
            replace_zero(f);
            check(f);
        }
        sprintf(f + 10, "%06d", e);
        replace_zero(f);
        check(f);
    }

    return 0;
}

Executing it is simple:

gcc libdroid.c -m32 -o libdroid
./libdroid
Found: e: '1 3875'
Congratulations! The rootkit is sucessfully installed. The Flag is 32C3_this_is_build_for_flag_ship_phones

So the flag is 32C3_this_is_build_for_flag_ship_phones. If you want to see the flag within the application, just press 103875 on it.

libdroid showing the flag