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
LOGI
is 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 logcat
using theLOGI
macro. - Returning the String: Finally, the function returns the "Yeah!" string as a new
jstring
back 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
getSecret
Function. - 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.so
is retrieved and logged. - Offset Calculation: We calculate the dynamic address of
GetStringUTFChars
by adding its offset to the base address. - Hooking GetStringUTFChars: We use Frida's
Interceptor.attach
to hook theGetStringUTFChars
function. 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
EIP
register. -
payload.push(0x42, 0x42, 0x42, 0x42): Adds 0x42 ('B') to overwrite the
EIP
register. -
Replacing the Return Value:
- retval.replace(memoryForPayload): Replaces the original return value of
GetStringUTFChars
with 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.so
library. -
Convert the Address:
-
After finding the address, we convert it to a hexadecimal format suitable for the
EIP
register. -
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
GetStringUTFChars
function to replace the user input with our crafted payload, which includes the address ofgetSecret
.
Summary
- We first locate the
getSecret
function's address usingModule.findExportByName()
. - The address is then formatted and adjusted for system endianness.
- Finally, by hooking the
GetStringUTFChars
function, 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
GetStringUTFChars
with 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