A few days ago, Th3Zer0 from the IT security company Shielder published the Qiling Labs challenge :
Qiling is a binary emulation framework built on top of the Unicorn engine which understands OS concepts (executable format such as ELF, dynamic linkers, syscalls, IO handlers…). Very convenient to quickly emulate an executable binary without emulating its entire OS. If you want more details about the differences between Qiling and other emulators, you can read the associated section on Github
Inspired by FridaLab, Qiling Labs is as a serie of 11 small challenges that aims at showcasing some useful features of Qiling. The idea is to encourage newcomers to learn about the framework while having fun.
As for me, I discovered and used Qiling for the first time a few weeks ago for a small research project and really enjoyed working with the framework.
This article presents my solutions for the QilingLab challenges. Of course, if you are interested in learning about Qiling, I encourage you to first try the challenges yourself (here) before reading further.
The challenges are contained in a single Linux ELF binary which is available for x86_64 or aarch64. Since I am working on a x86_64 machine, I chose to play with the aarch64 binary (qilinglab-aarch64) to better demonstrate the usefulness of the emulation framework.
First of all, let’s try to launch (aka emulate) the binary with Qiling. To do that, we only need to provide thepathof the binary and arootfs(the root of the filesystem from the point of view of the emulated binary) :
Here I specified the root of my machine’s filesystem as rootfs, let’s run the script and see the results:
As you can see, we get an error because shared libraries required to load the ELF binary are missing. It isn’t really surprising because I provided the rootfs of my x86_64 machine which does not contain any aarch64 libraries.
NB : if other shared libraries were required to emulate the binary, we would have needed to download them and add them to our rootfs
Now by specifying our new custom rootfs in the script, we can successfully emulate the binary:
Yay! The binary is executed and prints the list of the challenges before crashing because of an invalid memory read. This is the expected behavior and the first issue we will have to solve during Challenge 1.
For each of the challenge, in addition to the given instruction, a tiny bit of reverse engineering (I’ll use Ghidra) is required to understand what we have to do in order to pass each check.
Since it’s not a reverse engineering challenge, the binary is neither stripped, nor obfuscated. Therefore, we can focus on the Qiling part.
Challenge 1 : Memory mapping
The program tries to read memory at the address 0x1337 which is not mapped, hence the UC_ERR_READ_UNMAPPED we get when we run the binary.
To pass this check, we just need to map this area of virtual memory and write the expected value:
Note: we can display the complete memory map with ql.mem.show_mapinfo() to see the area we just mapped (it is necessary to increase the level of verbosity to see the output)
Challenge 2 : Syscall return hijack
Theunamesyscall returns information about the underlying OS. In our case, it returns a pointer to the following structure:
In order to pass the check of the challenge, thesysnamemust be QilingOS and theversionmust be ChallengeStart. To satisfy those conditions, we can use Qiling API to hook the syscall just before it returns usingset_syscallandQL_INTERCEPT.EXIT:
Challenge 3 : FS & Syscall hijack
The above code fetches 32 random bytes from two different sources : the file/dev/urandomand the syscallgetrandom. To pass the check, the following conditions must be met:
The 32 bytes obtained from the two sources have to be identical
The code also reads one byte from/dev/urandom: this byte must be different from all the other bytes
Two Qiling mechanisms will be used to solve this challenge:
Theset_syscallfunction to hijack thegetrandomsyscall and make him return 00 bytes. This time, instead of hijacking the exit of the syscall, we will completly overwrite it with our function.
Theadd_fs_mapperfunction coupled with aQlFsMappedObjectto define a custom behavior when operations are performed on/dev/urandom. In particular, we will make it return 00 bytes to matchgetrandomwhen several bytes are requested (condition 1) and a different byte when only one byte is requested (condition 2)
Challenge 4 : Hook address 1
The challenge 4 contains a loop with an impossible entering condition.To pass this check, we can use thehook_addressfunction to enter the loop:
Challenge 5 : External function hooking 1
To pass the check of this challenge, all the random numbers obtained withrand()must be equal. Since rand is an external function, we can useset_apito hijack it and make it return the same value every time:
Challenge 6 : Hook address 2
For this one, the program is stuck in an infinite loop. We can reuse the same strategy we used for challenge 4 withhook_address:
Challenge 7 : External function hooking 2
Here the code is stuck because of the call tosleep. There are several ways to bypass this call :
Hook the sleep function withset_apiand replace it with an empty function:
Hook the beginning of the sleep function withset_apiand change its argument:
Hook the underlyingnanosleepsyscall and replace it with an empty function (or change its argument):
Challenge 8 : Find a structure in memory
Here, the spirit of the challenge is get the address ofcheckfrom thes structure on the heap, and write the value 1.
One way to do that is to place a hook at the end of thechallenge8function and get the address of the structure on the stack:
To demonstrate the use of more Qiling functionnalities, let’s also solve the challenge with another strategy. Instead of directly reading the address of the heap structure from the stack, we will find the structure in memory usingql.mem.search:
Challenge 9 : External function hooking 2
To pass the check in this challenge, we need to hijack thetoloweroperation to prevent the modification of the stringaBcdeFghiJKlMnopqRstuVWxYzbefore the final comparaison. This can easily be done with theset_apifunction we saw earlier:
Challenge 10 : Hijack FS
The goal of this challenge is to modify the content read from/proc/self/cmdline. To do that, we can use aQlFsMappedObjectlike we did for/dev/urandomin challenge 3:
By the way, for simple case like this one, it is also possible to directly replace the target file with another one of our host filesystem. For instance here, after creating a filefake_cmdline, we can use the following code to pass the check:
Finally, another valid way to solve this challenge without writing any code is to create the/proc/self/cmdlinein our fake rootfs with the qilinglab string inside.
Challenge 11 : Hooking instructions
MIDR_EL1register contains information about the current CPU (arm documentation). It is a system register which is accessed in the challenge using the following assembly instruction:
To pass the check, we need to replace the returned value with the custom value0x13370000.
To do that, we will use thehook_codefunction which allows us to hook every instruction used by the CPU. When the target instruction is reached, the hook will hijack its execution.
Bonus : if we want to be more precise, we can only hook instructions executed by the main binary. This way, our hook will not be triggered in shared libraries where the target instruction is also used:
NB: Since hooking every instruction is quite expensive in terms of performance, we could optimize the code above by activating the hook at the of challenge 10 (just before its needed in challenge 11) and deactivate the hook after its execution using ql.hook_del
Thanks to Th3Zer0 and Shielder for this nice little challenge. I think this format is a great way to get started with new analysis tools before using them on bigger project.
If you want to see more Qiling API example, you can check Qiling’s documentation which contains several sample of its main functionalities.