In using the Qiling simulation execution framework on the Windows platform, I encountered many difficulties, and some issues did not have solutions found through research. Therefore, I am writing this article, hoping it can provide some reference for everyone.
The following points are listed as the content I want to express in this article:
◆ How to perform simulation execution of Aarch64 SO based on Qiling
◆ Some problem-solving methods for using Qiling on Windows
◆ Simulation execution of the algorithm in the virtual machine
This article aims to explain as much as possible from a beginner’s perspective, allowing beginners to practice on their own.
Recently, I needed to analyze an encryption algorithm of a libxx.so file and found that it was an Aarch64 SO. Therefore, I have the following three ideas:
◆ Use IDA for static analysis
◆ Debugging with a debugger to trace the algorithm flow
◆ Use Unidbg and Unicorn for simulation execution
3
Reasons for Choosing Simulation Execution
The Cost of Static Analysis is Too High
I looked at the encryption function using IDA; it resembles nested control flow flattening, but in reality, it is a virtual machine, which significantly increases the difficulty of static analysis.
Debugging with a Debugger
The initial thought was to compile an ARM64 executable, load libxx.so, and call the encryption function for debugging. By checking the string information in IDA, I found that it was an SO compiled with NDK+LLVM for use on the Android platform. Therefore, I thought of installing an ARM64 Ubuntu virtual machine for debugging, but it seems that Ubuntu does not provide an ARM64 client version, only a server version. Other Linux distributions (like CentOS) might be prepared for debugging, but considering that simulation execution is more convenient than debugging in terms of trace and other functions, I ultimately chose the simulator method.
Looking back, using Android Studio to write an app and then debugging with LLDB might be the most stable debugging method. Does anyone have other good suggestions?
Simulation Execution
Unidbg, based on Unicorn, can simulate the execution of Android SOs and meets the requirements well. However, according to an article by Bet4 (https://bbs.kanxue.com/thread-272605.htm), Unidbg also has simulation defects, so I chose Qiling as the simulation execution framework; detailed reasons can refer to the article by Bet4 mentioned above.
After understanding the general usage of Qiling from the official documentation, I found that Qiling has its own QDB debugger, but it does not support multithreading, while Bet4’s open-source UDB server supports multithreading. The effect is shown below (image from Bet4’s post):
I later discovered that Qiling provides an IDA plugin, which facilitates direct simulation execution of SO in IDA. Since analyzing algorithms often references some views in IDA, using UDB server would require frequent switching between GDB debugger and IDA. Therefore, I chose to use Qiling’s IDA plugin to “visually debug SO”.
In the Qiling source code examples\rootfs\arm64_linux\bin directory, there are many ARM64 programs available for simulation execution. The lib directory contains the corresponding dynamic linker for the program, and the dynamic linker corresponding to ARM64 is generally ld-linux-aarch64.so.1. This dynamic linker is very crucial as it is responsible for loading the libraries that the program depends on and the relocation of the program itself.
Next, taking arm64_hello in the bin directory as an example, I will demonstrate how to use the IDA plugin to simulate the execution of an Aarch64 SO. The function of arm64_hello is to output the string “Hello, World!” and then exit. The complete demo from the official website can be viewed at this link (https://docs.qiling.io/en/latest/ida/).
Initialize the Qiling Environment
Load arm64_hello into IDA, select “File->Script file…” to load qiling\extensions\idaplugin\qilingida.py.
On my computer, the soft link does not work; Qiling may have only tested Linux, so here I directly load the script into IDA.
Then, right-click in the IDA assembly view -> Qiling Emulator -> Setup (hereafter referred to as Qiling->
), to load the IDA plugin. Here, I used the default auxiliary script of Qiling (custom_script.py). Some automation logic in IDA can be placed in this script, such as adding some syscalls or hooks for specific addresses. An example of the script’s usage will be provided later.
As the saying goes, the beginning is always difficult; when running the demo, a loading failure occurred, as follows:
File "C:\Program Files\Python39\lib\site-packages\qiling\os\linux\linux.py", line 30, in __init__
super(QlOsLinux, self).__init__(ql)
File "C:\Program Files\Python39\lib\site-packages\qiling\os\posix\posix.py", line 190, in __init__
super().__init__(ql)
File "C:\Program Files\Python39\lib\site-packages\qiling\os\os.py", line 63, in __init__
sys.stdin.fileno()
AttributeError: 'NoneType' object has no attribute 'fileno'
Observing the source code of os.py:
try:
# Qiling may be used on interactive shells (ex: IDLE) or embedded python
# interpreters (ex: IDA Python). such environments use their own version
# for the standard streams which usually do not support certain operations,
# such as fileno(). here we use this to determine how we are going to use
# the environment standard streams
sys.stdin.fileno()
except UnsupportedOperation:
# Qiling is used on an interactive shell or embedded python interpreter.
# if the internal stream buffer is accessible, we should use it
self._stdin = getattr(sys.stdin, 'buffer', sys.stdin)
self._stdout = getattr(sys.stdout, 'buffer', sys.stdout)
self._stderr = getattr(sys.stderr, 'buffer', sys.stderr)
The error message indicates that sys.stdin is None because IDA has modified sys.stdin to use its own input/output streams. This issue has been mentioned in Qiling’s GitHub issues and pull requests, so I directly catch the AttributeError exception as follows:
except (UnsupportedOperation, AttributeError):
After fixing this, I restarted IDA to load the Qiling environment (the Python-installed Qiling environment). Qiling->Setup, and the following error occurred:
The fixed os.py file is in the Qiling python library, which is C:\Program Files\Python39\lib\site-packages\qiling\os\os.py.
File "E:\gitRepo\qiling\examples\extensions\idaplugin\custom_script.py", line 1, in <module>
from future import __annotations__
ImportError: cannot import name '__annotations__' from 'future' (C:\Program Files\Python39\lib\site-packages\future\__init__.py)
[INFO][(unknown file):0] Custom user script not found.
I am using Python version 3.9.13. After checking, the Python library has __future__, but not future. I don’t know if it relates to the Python version, so I changed it to the following form, and it loaded successfully again.
from __future__ import annotations
Start Simulation Execution
Now that arm64_hello has been loaded into memory, the dynamic linker needs to fix the relocation table, set the program entry point, and then start running.
By reading the Qiling\os\linux.py’s QlOsLinux::run method, since this is a single-threaded environment, and no explicit entry address is specified, the program’s entry address is the entry point of the dynamic linker. After the dynamic linker initializes, it will jump to the program’s actual entry point:
try:
# If it is a piece of binary code
if self.ql.code:
self.ql.emu_start(self.entry_point, (self.entry_point + len(self.ql.code)), self.ql.timeout, self.ql.count)
else:
# If it is a multithreaded environment
if self.ql.multithread:
# start multithreading
thread_management = thread.QlLinuxThreadManagement(self.ql)
self.ql.os.thread_management = thread_management
thread_management.run()
else:
# Not a multithreaded environment
# Is the program entry point explicitly specified
if self.ql.entry_point is not None:
self.ql.loader.elf_entry = self.ql.entry_point
# do we have an interp?
elif self.ql.loader.elf_entry != self.ql.loader.entry_point:
entry_address = self.ql.loader.elf_entry
if self.ql.arch.type == QL_ARCH.ARM:
entry_address &= ~1
# start running interp, but stop when elf entry point is reached
self.ql.emu_start(self.ql.loader.entry_point, entry_address, self.ql.timeout)
self.ql.do_lib_patch()
self.run_function_after_load()
self.ql.loader.skip_exit_check = False
self.ql.write_exit_trap()
self.ql.emu_start(self.ql.loader.elf_entry, self.exit_point, self.ql.timeout, self.ql.count)
Right-click in the IDA assembly view -> Qiling Emulator -> Continue to start the simulation execution, and the following error occurred:
[+] brk: increasing program break from 0x555555568000 to 0x555555589000
[+] 0x00007fffb7e9e93c: brk(inp = 0x555555589000) = 0x555555589000
[+] Received interrupt: 0x2
[+] write() CONTENT: b'Hello, World!\n'
[x] Syscall ERROR: ql_syscall_write DEBUG: A string expected
Traceback (most recent call last):
...
File "C:\Program Files\Python39\lib\site-packages\qiling\os\posix\syscall\unistd.py", line 410, in ql_syscall_write
f.write(data)
File "D:\Tools\IDA 7.5\python\3\init.py", line 63, in write
ida_kernwin.msg(text)
File "D:\Tools\IDA 7.5\python\3\ida_kernwin.py", line 236, in msg
return _ida_kernwin.msg(*args)
TypeError: A string expected
Here, we see that the puts function has been successfully executed, printing “Hello, World”. However, when passing this string to Python for printing, an exception occurs because the type of the data passed to the ida_kernwin.msg method by Qiling is a byte string, not a string.
◆ Directly modify the source code of D:\Tools\IDA 7.5\python\3\ida_kernwin.py to support byte strings.
◆ Hook the write function, replacing Qiling’s own ql_syscall_write.
The first method is global and will affect the analysis of other binary files, and since hooking system functions is a feature of Qiling itself, I chose the second method.
Modify custom_script.py as follows:
def my_syscall_write(ql: Qiling, fd: int, buf: int, count: int):
ql.log.info('my_syscall_write called')
try:
# read data from emulated memory
data = ql.mem.read(buf, count)
# select the emulated file object that corresponds to the requested
# file descriptor
fobj = ql.os.fd[fd]
if fobj == None:
ql.log.ingo('none file descriptor')
# write the data into the file object, if it supports write operations
elif hasattr(fobj, 'write'):
fobj.write(data.decode('utf-8'))
except:
ret = -1
else:
ret = count
ql.log.info(f'my_syscall_write({fd}, {buf:#x}, {count}) = {ret}')
return ret
class QILING_IDA:
def _show_context(self, ql: Qiling):
registers = tuple(ql.arch.regs.register_mapping.keys())
grouping = 4
for idx in range(0, len(registers), grouping):
ql.log.info('\t'.join(f'{r:5s}: {ql.arch.regs.read(r):016x}' for r in registers[idx:idx + grouping]))
This implements a my_syscall_write function, which is then called in the custom_prepare method of the QILING_IDA class to register the write syscall.
After modifying custom_script, right-click -> Qiling Emulator -> Reload User Scripts (hereafter referred to as Qiling menu) to reload the script. Finally, click Restart
in the Qiling menu to restart execution, and the result is normal, printing “Hello, World!”.
[+] Received interrupt: 0x2
[=] my_syscall_write called
Hello, World!
[=] my_syscall_write(fd = 0x1, buf = 0x555555568260, count = 0xe) = 0xe
[+] 0x00007fffb7e98afc: my_syscall_write(fd = 0x1, buf = 0x555555568260, count = 0xe) = 0xe
[+] Received interrupt: 0x2
[+] 0x00007fffb7e78a2c: exit_group(code = 0x0) = ?
Explicitly Set the PC Register to Specify Program Execution Entry
Having successfully run Qiling->Continue, I now attempt to set the run address in the Qiling menu.
First, Qiling->Restart, set a breakpoint at the __libc_start_main function in IDA, and then Qiling->Continue. At this point, I will see that it successfully breaks here:
Qiling->View Register to check the PC register and confirm whether the address is consistent. Here, it is found that the value of PC is 0x5E0 + base address of the program.
One can use Qiling->Step to step through a few times, and see different colors in IDA. These colors represent different execution operations. Blue is for stepping, while green represents direct execution covering the path.
The sub_724 function is the actual print function. Move the cursor to the first instruction of sub_724, then Qiling->Set PC, and at this point, it prints:
[INFO][(unknown file):0] QIling PC set to 0x724
After setting the PC, continue with Qiling->Continue, and the result is an error:
[x] CPU Context:
[x] x0 : 0x555555554724
[x] x1 : 0x1
[x] x2 : 0x80000000de18
...
[x] PC = 0x0000000000000724 (unreachable)
...
File "C:\Program Files\Python39\lib\site-packages\qiling\core.py", line 771, in emu_start
self.uc.emu_start(begin, end, timeout, count)
File "C:\Program Files\Python39\lib\site-packages\unicorn\unicorn.py", line 547, in emu_start
raise UcError(status)
unicorn.unicorn.UcError: Invalid memory fetch (UC_ERR_FETCH_UNMAPPED)
The error message indicates that the PC at 0x724 is inaccessible. Checking the registers:
Here, the PC is 0x724. Remembering that the Qiling register is in the form of IDA address + base address, I realize that the reason is that Qiling->Set PC sets the IDA address without adding the module base address.
Modify the IDA plugin of Qiling, qilingida.py as follows:
def ql_set_pc(self):
if self.qlinit:
# ea = IDA.get_current_address()
ea = self.qlemu.ql_addr_from_ida(IDA.get_current_address())
self.qlemu.ql.arch.regs.arch_pc = ea
logging.info(f"QIling PC set to {hex(ea)}")
self.qlemu.status = self.qlemu.ql.save()
self.ql_update_views(self.qlemu.ql.arch.regs.arch_pc, self.qlemu.ql)
else:
logging.error('Qiling should be setup firstly.')
After updating the IDA plugin, first “Qiling->Unload Plugin” to unload the plugin, then IDA->File->Script file to load the plugin again, and then repeat the operation. This time, the PC is set correctly with the base address:
[INFO][(unknown file):0] QIling PC set to 0x555555554724
Finally, move the cursor and click the last instruction of sub_724, then Qiling->Execute Till, and you will see that this time it successfully executes, with orange representing the instructions executed by Execute Till:
You can see that the next instruction is 0x748:
Subsection
From here, it can be seen that the Qiling IDA plugin is still not perfect. Even with the official dev branch (which is recommended for use), the simulation execution demo still has many issues, making it not very beginner-friendly. Therefore, I recorded relevant problem-solving suggestions here for everyone’s reference. There will also be slightly more troublesome bugs to solve later.
5
Simulation Execution of libxx.so (Troubleshooting)
OK, the demo has been run successfully. Now let’s simulate the execution of libxx.so.
The core steps of the algorithm are init, encrypt, and decrypt. So let’s first analyze the tps_init function.
After loading libxx.so and initializing Qiling, you cannot directly point the PC register to the tps_init function because the runtime environment is not ready yet, such as the tpidr_el0 register, which is similar to the fs segment register in Windows (TEB), pointing to the current thread’s runtime environment. Moreover, the relocation of the SO has not been done yet; many global references in the SO need to be relocated, such as calling a memset function.
First, load libxx.so into IDA, initialize Qiling (Qiling->Setup), set a breakpoint at the entry address of libxx.so (the start function), and then start simulation execution (Qiling->Continue), resulting in an error:
[x] Syscall ERROR: ql_syscall_futex DEBUG: 'NoneType' object has no attribute 'cur_thread'
Traceback (most recent call last):
File "C:\Program Files\Python39\lib\site-packages\qiling\os\posix\posix.py", line 374, in load_syscall
retval = syscall_hook(self.ql, *params)
File "C:\Program Files\Python39\lib\site-packages\qiling\os\posix\syscall\futex.py", line 43, in ql_syscall_futex
regreturn = ql.os.futexm.futex_wake(ql, uaddr,ql.os.thread_management.cur_thread, val)
AttributeError: 'NoneType' object has no attribute 'cur_thread'
Checking the source code:
elif op & (FUTEX_PRIVATE_FLAG - 1) == FUTEX_WAKE:
regreturn = ql.os.futexm.futex_wake(ql, uaddr,ql.os.thread_management.cur_thread, val)
The futex is a lock in Linux, and the thread_management here needs to be initialized only when Qiling starts in multithreaded mode. Therefore, I will add the multithreading parameter when creating the Qiling instance in qilingida.py:
class QlEmuQiling:
def __init__(self):
self.path = None
self.rootfs = None
self.ql: Qiling = None
self.status = None
self.exit_addr = None
self.baseaddr = None
self.env = {}
def start(self, *args, **kwargs):
self.ql = Qiling(argv=self.path, rootfs=self.rootfs, verbose=QL_VERBOSE.DEFAULT, multithread=True, env=self.env, log_plain=True, *args, **kwargs)
# ...
Here, I changed the log output from QL_VERBOSE.DEBUG to QL_VERBOSE.DEFAULT to reduce some debugging output, and then explicitly opened multithread. After unloading and loading qilingida.py once again, I restarted the Qiling environment, and the following error occurred:
[x] [Thread 2000] Syscall ERROR: ql_syscall_writev DEBUG: A string expected
Traceback (most recent call last):
File "C:\Program Files\Python39\lib\site-packages\qiling\os\posix\posix.py", line 374, in load_syscall
retval = syscall_hook(self.ql, *params)
File "C:\Program Files\Python39\lib\site-packages\qiling\os\posix\syscall\uio.py", line 23, in ql_syscall_writev
ql.os.fd[fd].write(buf)
File "D:\Tools\IDA 7.5\python\3\init.py", line 63, in write
ida_kernwin.msg(text)
File "D:\Tools\IDA 7.5\python\3\ida_kernwin.py", line 236, in msg
return _ida_kernwin.msg(*args)
TypeError: A string expected
This issue is the same as the previous demo problem; this time the syscall is writev instead of write. In custom_script.py, I will add the writev syscall hook and define the my_syscall_writev function. Reload the custom script with Qiling->Reload User Script, restart the Qiling environment, and run it. This time, it successfully runs to the start function:
However, note that tpidr_el0 is 0, indicating that the thread runtime environment is not ready. If you directly Qiling->continue at this point, another error will occur:
[x] [Thread 2000] PC = 0x000000000009aec0 (unreachable)
[x] [Thread 2000] Memory map:
[x] [Thread 2000] Start End Perm Label Image
[x] [Thread 2000] 00555555554000 - 00555555847000 r-x libxx.so E:\gitRepo\qiling\examples\rootfs\arm64_linux\bin\libxx.so
[x] [Thread 2000] 00555555856000 - 0055555588f000 rw- libxx.so E:\gitRepo\qiling\examples\rootfs\arm64_linux\bin\libxx.so
[x] [Thread 2000] 0055555588f000 - 00555555891000 rwx [hook_mem]
[x] [Thread 2000] 007ffffffde000 - 0080000000e000 rwx [stack]
Traceback (most recent call last):
...
File "C:\Program Files\Python39\lib\site-packages\unicorn\unicorn.py", line 547, in emu_start
raise UcError(status)
unicorn.unicorn.UcError: Invalid memory fetch (UC_ERR_FETCH_UNMAPPED)
Here, it can be seen that the PC is about to execute the instruction at 0x9aec0, but from the printed memory mapping, 0x9aec0 is not mapped, resulting in a failure to read the instruction.
Looking more closely at the memory mapping information, I found that the dynamic linker has not been loaded into memory. Let’s check it with readelf:
$ readelf -S libxx.so | grep interp
---
$ readelf -S arm64_hello | grep interp
[ 1] .interp PROGBITS 0000000000000200 00000200
libxx.so does not have an interp section, while arm64_hello does, so Qiling did not load the dynamic linker when loading libxx.so.
Now, I need to explicitly specify the dynamic linker for libxx.so. To determine where to specify this, I will first open the debug level log output:
def start(self, *args, **kwargs):
self.ql = Qiling(argv=self.path, rootfs=self.rootfs, verbose=QL_VERBOSE.DEBUG, multithread=True, env=self.env, log_plain=True, *args, **kwargs)
After reloading qilingida.py and initializing the Qiling environment, there was no related output. Then, I opened arm64_hello with IDA, and the related output was as follows:
[INFO][qilingida:1034] Custom env: {}
[+] Profile: default
[+] Mapped 0x555555554000-0x555555555000
[+] Mapped 0x555555564000-0x555555566000
[+] mem_start : 0x555555554000
[+] mem_end : 0x555555566000
[+] Interpreter path: /lib/ld-linux-aarch64.so.1
[+] Interpreter addr: 0x7ffff7dd5000
[+] Mapped 0x7ffff7dd5000-0x7ffff7df2000
[+] Mapped 0x7ffff7e01000-0x7ffff7e04000
It can be seen that the dynamic linker for arm64_hello is /lib/ld-linux-aarch64.so.1. Based on this print log, I will check the Qiling source code:
def load_with_ld()
def load_elf_segments()
# ...
# determine interpreter path
interp_seg = next(elffile.iter_segments(type='PT_INTERP'), None)
interp_path = str(interp_seg.get_interp_name()) if interp_seg else ''
Here, the interp_path is the path of the dynamic linker. Therefore, I will specify it as follows:
if len(interp_path) == 0:
interp_path = "/lib/ld-linux-aarch64.so.1"
After restarting the Qiling environment, I can see the loading of the dynamic linker:
[INFO][qilingida:1034] Custom env: {}
[+] Profile: default
[+] Mapped 0x555555554000-0x555555847000
[+] Mapped 0x555555856000-0x55555588f000
[+] mem_start : 0x555555554000
[+] mem_end : 0x55555588f000
[+] Interpreter path: /lib/ld-linux-aarch64.so.1
[+] Interpreter addr: 0x7ffff7dd5000
[+] Mapped 0x7ffff7dd5000-0x7ffff7df2000
[+] Mapped 0x7ffff7e01000-0x7ffff7e04000
Then, start the Qiling simulation execution (Qiling->Continue), and the following error occurred:
[+] [Thread 2000] b'Inconsistency detected by ld.so: '
Inconsistency detected by ld.so: [+] [Thread 2000] b'rtld.c'
rtld.c[+] [Thread 2000] b': '
: [+] [Thread 2000] b'1266'
1266[+] [Thread 2000] b': '
: [+] [Thread 2000] b'dl_main'
dl_main[+] [Thread 2000] b': '
: [+] [Thread 2000] b'Assertion `'
Assertion [+] [Thread 2000] b'GL(dl_rtld_map).l_libname'
GL(dl_rtld_map).l_libname[+] [Thread 2000] b"' failed!\n"' failed!\n...
File "C:\Program Files\Python39\lib\site-packages\qiling\os\linux\linux.py", line 167, in run
thread_management.run()
File "C:\Program Files\Python39\lib\site-packages\qiling\os\linux\thread.py", line 613, in run
previous_thread = self._prepare_lib_patch()
File "C:\Program Files\Python39\lib\site-packages\qiling\os\linux\thread.py", line 593, in _prepare_lib_patch
raise QlErrorExecutionStop('Dynamic library .init() failed!')
qiling.exception.QlErrorExecutionStop: Dynamic library .init() failed!
The first message directly indicates that the dynamic linker is incompatible with the current SO. I remembered that this SO was compiled with NDK for use on Android, so the dynamic linker should be the linker64 under Android.
Bet4 forked the Qiling GitHub repository and included linker64 and other SOs required for Android, such as libz, libm, and liblog.
Using readelf, I saw that libxx.so references these libraries, so I prepared them all, with the paths as follows:
$ readelf -d libxx.so
Dynamic section at offset 0x311f88 contains 29 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [liblog.so]
0x0000000000000001 (NEEDED) Shared library: [libz.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so]
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x000000000000000e (SONAME) Library soname: [libtps.so]
0x0000000000000019 (INIT_ARRAY) 0x302dc0
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x302dc8
...
E:\gitRepo\qiling\examples\rootfs\arm64_linux\system\lib64:Save libm.so and other libraries
E:\gitRepo\qiling\examples\rootfs\arm64_linux\lib:Save linker64
After replacing the dynamic linker with linker64, I restarted IDA, initialized the Qiling environment, and Qiling->Continue, resulting in the following error:
[x] [Thread 2000] Disassembly:
[=] [Thread 2000] 00007ffff7ddfbd0 [linker64 + 0x00abd0] 9c 20 40 79 ldrh w28, [x4, #0x10]
[=] [Thread 2000] 00007ffff7ddfbd4 [linker64 + 0x00abd4] 9f 0f 00 71 cmp w28, #3
[=] [Thread 2000] 00007ffff7ddfbd8 [linker64 + 0x00abd8] 81 34 00 54 b.ne #0x7ffff7de0268
...
File "C:\Program Files\Python39\lib\site-packages\qiling\core.py", line 771, in emu_start
self.uc.emu_start(begin, end, timeout, count)
File "C:\Program Files\Python39\lib\site-packages\unicorn\unicorn.py", line 547, in emu_start
raise UcError(status)
unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
At this point, X4 is 0, leading to a memory access exception. Opening IDA, the crash point is as follows:
After looking at the context, this function is _dl___linker_init, which is used to initialize dynamic libraries. To know why X4 is 0, I need to trace back a bit. It may take some time. Moreover, this linker64 is for Android low version 6.0, which might have compatibility issues. Therefore, I wanted to try another linker64. Luckily, I had a PIXEL device at hand, so I copied the linker64 from it and tested again. The result was as follows:
[+] [Thread 2001] b'libc'
libc[+] [Thread 2001] b': '
: [+] [Thread 2001] b'unable to stat "/proc/self/exe": Operation not permitted'
unable to stat "/proc/self/exe": Operation not permitted[+] [Thread 2001] b'\n'
The error indicates that it cannot read /proc/self/exe, which is a symbolic link that the linker uses to obtain its absolute path. The pseudo-code for the linker is as follows:
Here, I will hook the address 0x27DA8 and then return the absolute path and length. Update custom_script.py:
def custom_continue(self, ql: Qiling) -> List[HookRet]:
# ...
def addr_27DA8_hook(ql: Qiling) -> None:
ql.arch.regs.W0 = 0
ql.arch.regs.PC += 4
# ...
return [ql.hook_address(addr_27DA8_hook, 0x27DA8+linker_baseaddr)]
After repeating the process, another error occurred:
[+] [Thread 2000] b'libc'
libc[+] [Thread 2000] b': '
: [+] [Thread 2000] b'Could not find a PHDR: broken executable?'
Could not find a PHDR: broken executable?[+] [Thread 2000] b'\n'
Here, it indicates that the PHDR program header table cannot be found. Let’s check with readelf:
$ readelf -l libxx.so
Elf file type is DYN (Shared object file)
Entry point 0xa5370
There are 8 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000002f25b4 0x00000000002f25b4 R E 0x10000
LOAD 0x00000000002f2dc0 0x0000000000302dc0 0x0000000000302dc0
0x0000000000034478 0x0000000000037d58 RW 0x10000
DYNAMIC 0x0000000000311f88 0x0000000000321f88 0x0000000000321f88
0x0000000000000210 0x0000000000000210 RW 0x8
NOTE 0x0000000000000200 0x0000000000000200 0x0000000000000200
0x0000000000000024 0x0000000000000024 R 0x4
NOTE 0x00000000002f251c 0x00000000002f251c 0x00000000002f251c
0x0000000000000098 0x0000000000000098 R 0x4
GNU_EH_FRAME 0x00000000002da5dc 0x00000000002da5dc 0x00000000002da5dc
0x000000000000355c 0x000000000000355c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x00000000002f2dc0 0x0000000000302dc0 0x0000000000302dc0
0x0000000000025240 0x0000000000025240 R 0x1
Generally, the PHDR is the first element of the program table. Here, it is indeed missing. Let’s take a look at IDA:
Here, there is also a judgment that appears at the variable +0x10. Comparing the context of Bet4’s linker64 and the PIXEL’s linker64, it can be confirmed that it is the same problem, and it seems necessary to reverse engineer it further.
The annotation of the image is the result of the analysis.
There are two ways to approach this: one is to trace, using Qiling’s hook_code to print the environment before executing each instruction; the other is to set a breakpoint at the corresponding code and perform dynamic debugging. Here, I choose the dynamic debugging approach. How to set a breakpoint at the corresponding code address? Here, I need to familiarize myself with the relevant part of qilingida.py:
def ql_continue(self):
logging.info("before continue...")
if self.qlinit:
userhook = None
# Call hook_code, invoking ql_path_hook before each instruction execution
pathhook = self.qlemu.ql.hook_code(self.ql_path_hook)
def ql_path_hook(self, ql, addr, size):
addr = addr - self.qlemu.baseaddr + get_imagebase()
set_color(addr, CIC_ITEM, 0x007FFFAA)
# Get the number of breakpoints
bp_count = get_bpt_qty()
bp_list = []
if bp_count > 0:
for num in range(0, bp_count):
bp_list.append(get_bpt_ea(num))
# If the current instruction to be executed is at a breakpoint, call ql.save and ql.os.stop() to save the current simulation execution environment
if addr in bp_list and (addr != self.lastaddr or self.is_change_addr > 1):
self.qlemu.status = ql.save()
ql.os.stop()
self.lastaddr = addr
self.is_change_addr = -1
jumpto(addr)
self.is_change_addr += 1
This part of the code indicates that when the simulation execution is running, if it hits a breakpoint, it saves the environment and stops. Therefore, to break at the code address we want to analyze, we can manually add a breakpoint in ql_path_hook as follows:
def ql_path_hook(self, ql, addr, size):
addr = addr - self.qlemu.baseaddr + get_imagebase()
set_color(addr, CIC_ITEM, 0x007FFFAA)
bp_count = get_bpt_qty() + 1
bp_list = []
bp_list.append(0x27f2c + 0x007ffff7dd5000 - 0x555555554000)
if bp_count > 0:
for num in range(0, bp_count):
bp_list.append(get_bpt_ea(num))
This adds an element to bp_list, where 0x27f2c is the relative virtual address (RVA), and the following is the relocation base address adjustment. This way, we can hit the breakpoint and then debug step by step.
◆ Since the second line of the addr variable is subtracted from the base address of the libxx module during relocation, the element added to bp_list should be subtracted from the base address of the libxx module, not 0x27f2c + 0x007ffff7dd5000 – 0x007ffff7dd5000 = 0x27f2c.
◆ Because the PC register points to the linker64 module after interruption, it will report the following error, but this error does not affect continuing the step debugging:
[x] [Thread
2000
] [Thread
2000
] Expect
0x5555555f9370
but get
0x7ffff7e9e920
when running loader.
Traceback (most recent call last):
...
File