My journey into Android exploitation at the binary level started with a deep passion for the subject. I was determined to simplify the process as much as possible, but it turned out to be quite challenging. In this post, you'll learn various aspects of Android exploitation, including:
- How to use the NDK to write an application.
 - How to create a vulnerable lab environment for demonstrating Stack Overflow vulnerabilities.
 - How to exploit Stack Overflow in Android binaries.
 - How to Debug an Android Native Binary Remotely with GDB
 - A deep dive into the capabilities of Frida!
 
I conducted all the steps in this post on an x86 Android emulator (Genymotion).
Why Frida?
You might wonder why I used Frida. There were two main reasons:
- Passion: I absolutely love working with Frida!
 - Practicality: I couldn't find another way to send my binary data for overriding the EIP.
 
I hope you find this content innovative and insightful.
Creating the Vulnerable Application
Implementing Graphical User Interface
The first step in creating our vulnerable application is to edit the style of the MainActivity. The goal here is to set up an interface that allows us to input data for buffer exploitation and includes a button to submit the value.
Below is the XML code for activity_main.xml:
Implementing Vulnerable Native C++ Code
To introduce a vulnerability, we'll use native C++ code that is susceptible to a stack overflow.
Below, I've defined three functions, starting with the main function. This function simply returns the user input value.
Here's the C++ code for the main function:
The second function in our vulnerable native C++ code is kousha. This function is called by Java_com_example_myapplication_MainActivity_stringFromJNI and is responsible for copying the user input characters one by one into a fixed-size buffer. 
Below is the C++ code for the kousha function:
Implementing the getSecret Function
The final piece of our vulnerable application is the Java_com_example_myapplication_MainActivity_getSecret function. The goal of this lab is to exploit the stack overflow and override the EIP register to call this function. When successfully called, this function will log "Yeah!" in the adb logcat output.
Before diving into the function itself, don't forget to define the logging macro at the beginning of your C++ file:
Here's the C++ code for the getSecret function:
Code Explanation:
- Logging Setup: The macro 
LOGIis defined to log informational messages to the Androidlogcat. This is essential for seeing the "Yeah!" message in the log output. - Function Declaration: Like the previous functions, this one is declared with 
extern "C"to ensure proper linkage for JNI. - Logging the Message: The function logs the string "Yeah!" to the 
adb logcatusing theLOGImacro. - Returning the String: Finally, the function returns the "Yeah!" string as a new 
jstringback to the Java layer. 
Exploit Goal:
The goal of this lab is to craft an exploit that causes the kousha function's stack overflow to override the EIP register, redirecting execution to the getSecret function. When successful, you’ll see "Yeah!" logged in the adb logcat output, confirming that the exploit worked.
Full Code for Vulnerable Native C++ Functions
Below is the complete C++ code for the vulnerable native functions used in our Android application. This code includes logging macros, the vulnerable kousha function, and the getSecret function, which we aim to call via an exploit.
Modifying CMakeLists.txt for Security Settings and Library Inclusion
To set up our build environment for the vulnerable application, we need to modify the CMakeLists.txt file. This involves disabling the FORTIFY security feature and adding the native-lib library.
Here is the updated CMakeLists.txt configuration:
Build and run the project now.
Exploiting Stack Overflow
Now it's time to trigger a crash. We'll send an excessive number of characters to cause a buffer overflow and crash the app.
Send the following input to trigger the buffer overflow:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Now that we know the application is vulnerable, use the cyclic function from the pwntools Python framework to determine the amount of data needed to reach and override the EIP register.
We observed that the fault address is 0x61656161, which corresponds to the last value of EIP when the app crashes.
Now, we can determine the offset needed to override the EIP register and execute our getSecret function.
Everything has been smooth so far, but we're about to hit a challenging part. Grab a cup of coffee and let's dive in.
Even if we have the address of the getSecret function, passing it as a text input could be tricky. We might explore using Unicode characters, though their effectiveness is uncertain. That's why I'm turning to Frida for a solution.
Before we dive into Frida, let's complete the final step of our current task. We'll use GDB for remote debugging to confirm that the EIP register has been successfully overwritten.
I’ve set up gdbserver on the Android device and attached it to the vulnerable application with the following command:
I then forwarded port 7777 from the Android device to my host using:
Next, I started GDB and connected to the remote application using:
The application is currently stopped. Set a breakpoint on the kousha function using the following GDB command:
I used aaaaaaaaaaaaaabcde as the input. This consists of 14 'a' characters to reach the offset and bcde to overwrite the EIP register.
The breakpoint will be hit, and GDB will stop at the first instruction of the kousha function.
You can view the next 30 instructions with:
You might see a jmp instruction creating a loop that corresponds to the while loop in the kousha function. Set a breakpoint after the loop and continue debugging.
Use the ni (next instruction) command in GDB to step through the instructions one by one. After executing the pop ebp instruction, the ebp register will show aaaa.
When the ret instruction is executed, the eip register will be set to bcde.
We successfully overwrote the EIP register with the value bcde. Now, the goal is to override it with the address of our getSecret function. To do this, let's dive into Frida.
I’ve set up the Frida Server on the Android Emulator, enabling us to connect to it. The plan involves three steps:
- Find the Base Address of the 
getSecretFunction. - Craft a Payload 
(offset + *getSecret). - Send the Payload as User Input to Trigger the Overflow.
 
However, it's not as straightforward as it sounds. After trying several methods to modify the user input, I found that hooking GetStringUTFChars was the most effective approach. So, we'll hook GetStringUTFChars from the libnative-lib.so library.
For additional learning, I've included some extra code to explore Frida's capabilities, such as finding the base address of a library using Module.findBaseAddress("libname.so").
Here's the Frida script:
Explanation:
- Finding the Base Address: The base address of 
libnative-lib.sois retrieved and logged. - Offset Calculation: We calculate the dynamic address of 
GetStringUTFCharsby adding its offset to the base address. - Hooking GetStringUTFChars: We use Frida's 
Interceptor.attachto hook theGetStringUTFCharsfunction. If the user input matches "exploit", it confirms that the hook is working as expected. 
This setup allows us to intercept and modify the input before it's used in the vulnerable function, helping us to overwrite the EIP with the getSecret address.
Now, let's craft our payload. We need 14 bytes as an offset to reach the EIP register, followed by the base address of the getSecret function. However, there's a crucial detail: in our previous code, we only used console.log(). To exploit the vulnerability, we need to return an address as the retval. This requires allocating space in the heap.
Below is the code that does this:
Explanation:
- Memory Allocation:
 - Memory.alloc(100): Allocates 100 bytes in the heap for our payload.
 - Crafting the Payload:
 - payload = [0x41, ...]: Fills the first 14 bytes with 0x41 ('A') to reach the 
EIPregister. - 
payload.push(0x42, 0x42, 0x42, 0x42): Adds 0x42 ('B') to overwrite the
EIPregister. - 
Replacing the Return Value:
 - retval.replace(memoryForPayload): Replaces the original return value of 
GetStringUTFCharswith our crafted payload. 
This code will modify the user input to include the crafted payload, allowing us to overwrite the EIP register and execute the getSecret function.
Now that we’ve identified how to trigger the overflow, it’s time to execute the real exploit: overriding the EIP register with the address of the getSecret function.
Here's the process:
- Find the Base Address of getSecret:
 - 
We use
Module.findExportByName()to locate the base address of the getSecret function within thelibnative-lib.solibrary. - 
Convert the Address:
 - 
After finding the address, we convert it to a hexadecimal format suitable for the
EIPregister. - 
Adjust for Endianness:
 - 
The address is pushed in reverse order due to endianness, which ensures the correct function call.
 - 
Execute the Exploit:
 - Finally, we hook the 
GetStringUTFCharsfunction to replace the user input with our crafted payload, which includes the address ofgetSecret. 
Summary
- We first locate the 
getSecretfunction's address usingModule.findExportByName(). - The address is then formatted and adjusted for system endianness.
 - Finally, by hooking the 
GetStringUTFCharsfunction, we replace the user input with a payload that includes the address ofgetSecret. - When the payload is executed, the application logs "Yeah!" to indicate success.
 
And just like that, we’ve successfully exploited the application to call the getSecret function!
Bonus
If you're interested in writing and executing shellcode in memory—like a reverse shell—this section will guide you through the process. The steps include writing the shellcode, allocating memory for it, changing the allocated memory's permissions to make it executable, and then writing the shellcode into that memory.
Steps to Execute Shellcode:
- Write Your Shellcode:
 - 
First, craft your shellcode. This could be something like a reverse shell.
 - 
Allocate Memory:
 - 
Allocate memory for your shellcode, ensuring it's large enough to hold the shellcode and any additional data.
 - 
Change Memory Permissions:
 - 
Modify the permissions of the allocated memory region to make it executable.
 - 
Write Shellcode into Memory:
 - 
Write the shellcode into the allocated memory.
 - 
Verify with hexdump():
 - Use 
hexdump()to inspect the memory where the shellcode is located and ensure everything is as expected. 
Summary
- Memory Allocation: We allocate memory and change its permissions to allow execution.
 - Shellcode Injection: The shellcode is injected into the allocated memory.
 - Execution: The script is set up to replace the return value of 
GetStringUTFCharswith the payload that includes the shellcode. - Verification: Use 
hexdump()to inspect the shellcode in memory and ensure it's correctly placed. 
This approach demonstrates how to write and execute shellcode directly in memory using Frida.
Authors
Written by Kousha Zanjani
