Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis

1



This Article's Purpose


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.
2



Analysis Target


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.Aarch64 Architecture SO Simulation Execution and Encryption Algorithm 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):
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis
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”.
4



ARM64 Demo Execution


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.
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis
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.
There are two solutions:
◆ 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:
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis
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.
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis
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.
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis
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:
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis
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:
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis
You can see that the next instruction is 0x748:
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis

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.
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis
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 &amp; (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:
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis
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:
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis
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:
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis
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:
Aarch64 Architecture SO Simulation Execution and Encryption Algorithm Analysis
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

Leave a Comment