32C3 CTF 2015: libdroid Write-up
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
.
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.