Introduction
QiLing, a cross platform and multi architecture binary emulator (Linux, macOS, Windows, FreeBSD, DOS,…). It helps a lot when we need to debug and fuzz binary to find vulnerability (supported x86 - 16bit, 32bit, x64, ARM, ARM64 và MIPS). We had an interesting project that required the support of this framework, so I want to share with everyone how to simulate using Qiling and hunt for a firmware bug.
The first thing is finding the pin and scanning port of the device, this is quite simple because these circuits still have UART pins to debug.
Use the serial to connect to and check what is CPU architecture of the device. There hasn't got "file" command on the device, so I took the binary and checked it.
Device uses ARM, little endian architecture. Check for open ports with netstat.
For the purpose of this article which is introducing about how to use Qiling, I will only focus on port 5060/UDP. The rest of the ports, I will continue to write at the suitable time in the future.
Pull the binary and open it in the Ghidra:
To find the handle of port 5060, I will find places to call bind and recv, recvfrom. These functions will involve creating socket and receiving data sent to the device.
After digging for a while, you can find the data processing section of port 5060
Reading through the program's log, I saw that the program uses the eXosip library, which is basically built on the osip library, for VoIP programming (Voice over Internet Protocol).
I can get the source of the library and do fuzzing, but the current source of the library may not have bugs, and the version that's running on the device may not the latest version. The flow of a function call also differs quite a bit between the original library and the code in the program, with the high chance that the library has been modified. And the most important, we always try to have real PoC for every vulnerability we may find.
So I decided to fuzz on this executable file itself using Qiling, which is also part of my personal desire to apply Qiling to our work to evaluate its effectiveness. Instructions for installing and using Qiling are available here.
I recommend the manual installation because several times I tried to install automatically by pip, there were quite a lot of errors. The goal is to pick a function that processes the received data, setup the required parameters, emulate the function, and do fuzzing. As shown in the picture above it can be seen that the osip_parse function is the appropriate target. The function takes two parameters: buf - received data, len - the length of the received data. To be able to use Qiling to emulate, the program needs two things: script and environment. We'll create a simple rootfs directory by copying Qiling's sample directory.
Script:
```
import sys
from qiling import *
def run_sandbox(path, rootfs, output):
ql = Qiling(path, rootfs, output = output)
ql.run()
if __name__ == "__main__":
run_sandbox(["./arm_linux/bin/main"], "./arm_linux",
"none")
```
$ python3 myLoad.py
The result was unsuccessful, now we will find the cause
One thing I like very much about Qiling is, when an error occurs, Qiling will print out information about the registers and memory of the program. Scrolling up a little, we can see the Qiling log printed out, where the program tried to find and load libminigui_ths library but could not find it, the results of open functions were -2.
In fact, when I did, I took the whole /lib directory, folders containing other libraries and necessary environment variables in device to set up.
I will edit the script a bit to add environment variables:
```
import sys
from qiling import *
def run_sandbox(path, rootfs, output,
env):
ql = Qiling(path, rootfs, output=output, env=env)
input("env: " + env["LD_LIBRARY_PATH"])
ql.run()
if __name__ == "__main__":
run_sandbox(["./arm_linux/bin/main"], "./arm_linux",
"none",\
{"LD_LIBRARY_PATH":"/lib:"+\
"/env/config/wifi:"+\
"/env/customer/tslib/lib:"+\
"/env/customer/libminigui:"+\
"/env/customer/lib:"+\
"/env/customer/morfeicore/libs"})
```
However, when I was doing this way, I had a problem with Qiling that with the variable LD_LIBRARY_PATH, the linker only finds the library in the first directory in the variable LD_LIBRARY_PATH. I'm not really sure if it was Qiling or the setup. But this can be overcome by finding all the dependencies libraries and put them in one directory so I decided not to take the time to research carefully.
After finish setup libraries, try running again:
Looking at the printed log, we can see that the program has been successfully loaded and started running into main(), however /dev/mi_sys does not exist, so the program ended. We can skip this because the ultimate goal is to fuzz the osip_parse function, rather than run the program completely. To be able to do that task, we need to specify the start and end address for ql.run() as well as the appropriate value on the registers.
I will choose addresses 0x207568 and 0x20756c
Along with that is setup, the parameters of osip_parse are buf and len: The variable buf is a pointer to a memory allocated by the osip_malloc function, going deep into the function, we know the memory will be allocated by the calloc function.
So we'll hook into the address 0x207568 and allocate a memory area, pass memory address and length of data respectively in registers r0 and r1.
The script will be changed as below:
```
#!/usr/bin/env python3
import sys
from qiling import *
BEGIN_ADDRESS = 0x207568
END_ADDRESS = 0x20756c
myMap = 0
size = 3000
payload = b"this is payload"
# we will replace that with real one later
def setupReg(ql):
global
myMap
myMap
= ql.mem.map_anywhere(size)
ql.mem.write(myMap,
payload)
ql.reg.write("r0",
myMap)
ql.reg.write("r1",len(payload))
def run_sandbox(path, rootfs, output):
ql = Qiling(path, rootfs, output = output)
ql.hook_address(setupReg, BEGIN_ADDRESS)
# here we hook BEGIN_ADDRESS with setReg function
ql.run(begin=BEGIN_ADDRESS, end=END_ADDRESS)
print("[.] Done")
if __name__ == "__main__":
run_sandbox(["./arm_linux/bin/main"], "./arm_linux",
"none")
```
And try:
Here, error is Invalid memory fetch, but PC = 0x0, which means the program has not been loaded, there was an error loading. I will set the output mode of Qiling to debug to get more information and compare it when running normally and when running from BEGIN_ADDRESS.
On the left is when running from BEGIN_ADDRESS, the right is when running from scratch, as can be seen that when we ran from BEGIN_ADDRESS we ignored the library loading which resulted in error. To solve this problem, we will wait for the program to fully load and then pass the execution thread to the osip_parse function. I will hook into the _start function, specifically when the _start function calls the __libc_start_main function with the first parameter being the address of the main function. Instead of calling the main function, __libc_start_main will call osip_parse, then I will set up the necessary parameters. I did not setup immediately because parameters of main(int argc, char ** argv) will be found and initialized by the __libc_init_first function, not passed in from __libc_start_main.
The script will change as below:
```
#!/usr/bin/env python3
import sys
from qiling import *
BEGIN_ADDRESS = 0x207568
END_ADDRESS = 0x20756c
myMap = 0
size = 3000
payload = b"this is payload"
def _startHook(ql):
global
myMap
#
malloc memory for payload
myMap
= ql.mem.map_anywhere(size)
ql.mem.write(myMap,
payload)
#
replace main addr with target addr
ql.reg.write("r0",
BEGIN_ADDRESS)
def setupReg(ql):
#
osip_parse(buf, len)
ql.reg.write("r0",
myMap) # buf
ql.reg.write("r1",len(payload))
# len
def run_sandbox(path, rootfs, output):
ql = Qiling(path, rootfs, output=output)
ql.hook_address(_startHook, 0x000A6144) # call target function instead
of main
ql.hook_address(setupReg, BEGIN_ADDRESS) # setup arguments for target
function
ql.run(end=END_ADDRESS)
#ql.run()
print("[.] Done")
if __name__ == "__main__":
run_sandbox(["./arm_linux/bin/main"], "./arm_linux",
"debug")
```
It seems that I have successfully emulate the osip_parse function, next step is interaction with AFL++.
In this section, Qiling also has samples, so we just need to read and understand, I also commented on a few lines as simple as possible.
Script:
```
#!/usr/bin/env python3
import sys
from qiling import *
import unicornafl
unicornafl.monkeypatch()
BEGIN_ADDRESS = 0x207568
END_ADDRESS = 0x20756c
myMap = 0
size = 3000
def _startHook(ql):
global
myMap
#
map memory for payload
myMap
= ql.mem.map_anywhere(size)
#
replace main addr with target addr
ql.reg.write("r0",
BEGIN_ADDRESS)
def setupReg(ql):
#
osip_parse(buf, len)
ql.reg.write("r0",
myMap) # buf
ql.reg.write("r1",len(payload))
# len
def run_sandbox(path, rootfs, output,
input_file):
ql = Qiling(path, rootfs,console=True, output=output)
### AFL go from here
# callback where we write content from input to memory
def place_input_callback(uc, payload, _, data):
# write payload into our memory
ql.mem.write(myMap, payload)
# osip_parse(buf, len)
ql.reg.write("r0", myMap) # buf
ql.reg.write("r1",len(payload)) # len
#
afl hook at target function
def start_afl(_ql: Qiling):
"""
Callback from inside
"""
# We start our AFL forkserver
or run once if AFL is not available.
# This will only return after the fuzzing stopped.
try:
#print("Starting
afl_fuzz().")
# input_file: this is where we feed
corpus
# place_input_callback: we will
implement how input is handle in this callback
if not
_ql.uc.afl_fuzz(input_file=input_file,
place_input_callback=place_input_callback,
exits=[ql.os.exit_point]):
print("Ran once without
AFL attached.")
print(input_file)
os._exit(0) # that's a looot faster than tidying up.
except unicornafl.UcAflError as ex:
print("[.] Error: " + ex)
ql.hook_address(_startHook, 0x000A6144) #
call target function instead of main
ql.hook_address(start_afl, BEGIN_ADDRESS) # setup afl
ql.run(end=END_ADDRESS)
#ql.run()
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Require 2 arguments")
else:
run_sandbox(["./arm_linux/bin/main"], "./arm_linux",
"debug", sys.argv[1])
```
We can see that I have replaced the setupReg hook function with the start_afl function to receive input from the file and interact with AFL, as well as add the input_file parameter to the run_sandbox function. And this is the result:
Actually, when I got to this part, I made a pretty dumb error. If you noticed in the first version of the script, I used the input() function to log out to see if the env variable was configured correctly. Because of this function, when running with AFL, I didn't know why there was this error and it took me a lot of time to fix it.
However, at this point, the script was still incomplete. As in this case, as we can find the relevant document, it is easy to know how the library parses the data:
In most other cases, we may not have this information, this will lead to the fuzzing will not run correctly and we may have to do the reverse engineering to get all the information. If we just stopped at the last step above, we will see a lot of crashes and most of them are false positive. I will take some samples in the crash directory to see where the osip_parse function failed when processing these files.The program crashes at address 0x77fc5e10 with the instruction "ldrb r2, [r0]" - loads a byte of the value r0 pointing to r2. Based on mapped memory addresses, 0x77fc5e10 is located in libc's memory. Most likely a function in libc was called with an incorrectly processed parameter.
We will add a small Qiling code to trace which function has called this function and run the script again.
The value of register r0 is 0, the program tried to read the content at address 0 so it should fail. The last address in the program that called the libc function is 0x234bdc. We check this address on Ghidra. The strcmp function accesses the array 0x753d34, most likely because this array has not been initialized, so an error occured when the strcpy function accessed this array.
Checked the references, we see the function parser_init() has impact on this address.
It can be seen that the values in the array starting at address 0x753d34 was initialized here, so we need to add this function to the harness to be able to run thread of the program correctly. The flow of execution will now be:
After parser_init finishes running, I will write straight to the PC register to switch the flow of execution```
#!/usr/bin/env python3
import sys
from qiling import *
import unicornafl
unicornafl.monkeypatch()
start_osipParse = 0x207568
end_osipParse = 0x20756c
start_parserInit = 0x0022AF8C
end_parserInit = 0x0022AF90
myMap = 0
size = 3000
#payload = b"a"*1000
def _startHook(ql):
global
myMap
#
map memory for payload
myMap
= ql.mem.map_anywhere(0x10000)
#
replace main addr with target addr
ql.reg.write("r0",
start_parserInit)
def directToTarget(ql):
ql.reg.write("pc",
start_osipParse)
def setupReg(ql):
ql.mem.write(myMap,
payload)
#
osip_parse(buf, len)
ql.reg.write("r0",
myMap) # buf
ql.reg.write("r1",len(payload))
# len
def run_sandbox(path, rootfs, output,
input_file):
ql = Qiling(path, rootfs,console=True, output=output)
### AFL go from here
# callback where we write content from input to memory
def place_input_callback(uc, payload, _, data):
# write payload into our memory
ql.mem.write(myMap, payload)
# osip_parse(buf, len)
ql.reg.write("r0", myMap) # buf
ql.reg.write("r1",len(payload)) # len
#
afl hook at target function
def start_afl(_ql: Qiling):
"""
Callback from inside
"""
# We start our AFL forkserver or run once if AFL is not available.
# This will only return after the fuzzing stopped.
try:
#print("Starting
afl_fuzz().")
# input_file: this is where we feed
corpus
# place_input_callback: we will
implement how input is handle in this callback
if not
_ql.uc.afl_fuzz(input_file=input_file,
place_input_callback=place_input_callback,
exits=[ql.os.exit_point]):
print("Ran once without
AFL attached.")
os._exit(0) # that's a looot faster than tidying up.
except unicornafl.UcAflError as ex:
print("[.] Error: " + ex)
ql.hook_address(_startHook, 0x000A6144) # call target function instead
of main
ql.hook_address(directToTarget, end_parserInit) # direct to osip_parse
ql.hook_address(start_afl, start_osipParse) # setup afl
ql.run(end=end_osipParse)
#ql.run()
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Require 2 arguments")
else:
run_sandbox(["./arm_linux/bin/main"], "./arm_linux",
"debug", sys.argv[1])
```
After a running for a while, it had result with some crashes, and I checked by sending the packets to port 5060. The process crashed but on the device there was a script to restart the process if the process didn't exist.
When tested with the latest version of the library, there hasn't got the bug above. It was enough for the simple PoC, so I stopped analyze it further at that moment.
At last, I hope I can deliver the basic fuzzing tutorial with Qiling. You can see this is not so hard but requires some knowledge. Surely, Qiling will be developed and applied to many targets that save us time and resources.
Thank you for taking the time to read this blog.
Reference
Click here for
Vietnamese version
Trần Phạm Thành (aka Radcet)
Services Center - VinCSS (a member of Vingroup).
No comments:
Post a Comment