Qiling: A true instrumentable binary emulation framework

Published: 2021-04-30. Last Updated: 2021-04-30 08:13:53 UTC
by Remco Verhoef (Version: 1)
0 comment(s)

A while ago, during the FLARE On 7 challenge last autumn, I had my first experience with the Qiling framework. It helped me to solve the challenge CrackInstaller by Paul Tarter (@Hefrpidge). If you want to read more about this (very interesting) challenge: https://www.fireeye.com/content/dam/fireeye-www/blog/pdfs/flareon7-challenge9-solution.pdf

Qiling is an advanced binary emulation framework, supporting many different platforms and architectures. It uses the well known Unicorn Engine and understands operating systems. It  knows how to load libraries and executables, how to relocate shared libraries, handles syscalls and IO handlers. Qiling can execute binaries without the binaries native operating system. You’ll probably won’t use Qiling to emulate complete applications, but emulating (large) functions and code works flawlessly. 

The Qiling framework comes out of the box supporting 40% of the Windows API calls, Linux syscalls and has also some UEFI coverage. Qiling is capable of creating snapshots, hooking into sys- and api calls, hot patching, remote debugging and hijacking stdin and stdout. Because Qiling is a framework, it is easy to extend and use by writing Python code.

Let me walk you through some of the features. Qiling can be started by a docker container, using the following command: 

docker run -p 5000:5000 -v (pwd):/work -it qilingframework/qiling

To start with Windows binaries, you need to collect the dlls (and registry) from a Windows system first. The batch file https://github.com/qilingframework/qiling/blob/master/examples/scripts/dllscollector.bat is available for this purpose, this will collect all the necessary files to get started.

After collecting the files, the following will make Qiling to load the library, configure the architecture and operating system and point to the root filesystem (containing the windows or linux libraries):

ql = Qiling(["/work/credhelper.dll"], archtype="x86", ostype="windows", rootfs="/work/10_break/rootfs/x8664_windows/", output="debug", console=True)

Now the framework is initialized, it is ready to execute specific address ranges. But first you’ll probably want to set-up, memory, stack and register values, which are being offered by the Unicorn Engine:

# set up memory pool
pool = ql.mem.map_anywhere(size)

# write memory at specific location
ql.mem.write(0x14000608c, ql.pack64(0x41424143414242))

# configure register values
ql.reg.rdx = param2
	
# setting up stack values
ql.stack_write(0x358, ql.reg.rsp+576) # local_358

Now we have set the memory, stack and registry, we can start the emulation:

ql.run(begin=start_offset, end=end_offset)

If you add the disassembly capabilities of Capstone, parts of the memory can be disassembled easily. The snippet below will hook every instruction and run the print_asm function.

from capstone import *
md = Cs(CS_ARCH_X86, CS_MODE_64)

def print_asm(ql, address, size):
    buf = ql.mem.read(address, size)
    for i in md.disasm(buf, address):
        print(":: 0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str))

ql.hook_code(print_asm)

Dynamically hooking into the application, can be done using memory hooks, like intercepting memory errors and invalid reads:

def ql_x86_windows_hook_mem_error(ql, access, addr, size, value):
    ql.dprint(D_INFO, "[+] ERROR: unmapped memory access at 0x%x" % addr)
    return False

ql.hook_mem_unmapped(ql_x86_windows_hook_mem_error)

def hook_mem_read_invalid(ql, access, addr, size, value):
    ql.dprint(D_INFO, "[+] ERROR: invalid memory read at 0x%x" % addr)
    return True

ql.hook_mem_read_invalid(hook_mem_read_invalid)

def hook_mem_invalid(ql, access, addr, size, value):
    ql.dprint(D_INFO, "[+] ERROR: invalid memory access at 0x%x" % addr)
    return True

ql.hook_mem_invalid(hook_mem_invalid)

Often you want to intercept specific API calls, or maybe add calls that haven’t been implemented yet. The following code will implement the StringFromGUID2 api call, write the value to memory to the location lpsz parameter points to and return the size of the bytes written.

@winsdkapi(cc=STDCALL, dllname="ole32_dll", replace_params={
  "rguid": POINTER,
  "lpsz": POINTER,
  "cchMar": DWORD,
})
def hook_StringFromGUID2(ql, address, params):
        ql.nprint("StringFromGuid2", address, params)
        ql.mem.write(params.get('lpsz'), "test".encode('utf-16le') + '\x00'.encode('utf-16le'))
        return 5

ql.set_api("StringFromGUID2", hook_StringFromGUID2)

Hot patching code, is just writing to memory locations:

# patch kernel execute
ql.mem.write(0x140002b59, b'\x90\x90\x90\x90\x90\x90')

For this specific FlareOn challenge, I created a small Ghidra python plugin, to be able to visually select the address range to emulate. The Ghidra plugin communicates with a flask server, running my Qiling code to run the emulation and return results back to Ghidra. Using this approach it was easy to emulate small parts of the code and eventually leading to the solution of the challenge. 
 

There is much more Qiling, but for now have a great day!

References


Remco Verhoef (@remco_verhoef)
ISC Handler - Founder of DTACT
PGP Key
 

Keywords: reversing
0 comment(s)
ISC Stormcast For Friday, April 30th, 2021 https://isc.sans.edu/podcastdetail.html?id=7480

Comments


Diary Archives