linkern Emporium

Qiling & Binary Emulation for automatic unpacking

  1. Introduction
  2. Packing techniques
    1. Injection in the original binary
    2. Manual mapping of an ELF into memory
  3. Unpacking, thougths & algorithms
  4. Qiling Framework
    1. First basic analysis with qiling
  5. Hooking & unpacking
    1. Detecting suspecious memory areas
    2. Dumping pages
  6. Summary

Welcome folks, today I want to show you a few tricks about automatic unpacking on elf executables. With the usage of qiling framework .

Introduction to automatic unpacking

The automatic unpacking is the art of extracting certain informations from an obfuscated executable, to subsquently make another binary more easily readable. A native binary can be obfuscated in a lot ways, including: (virtual machine based obfuscation, opaque predicates, polymorphism ...), and we will not always be able to deobfuscate the targeted binary. But it's very hard for an obfuscator to keep the same behavior and to in the same time obfuscate directly the code of the original binary. Indeed there are many parameters which are hard to predict (indirect jmp ...). That is why quite often the obfuscator is working "around" the targeted binary. It means that instead of editing the structure of the binary, it apply certain techniques to make it very difficult to comprehend the structure of the binary, often by packing. The goal for the usage of automatically unpacking an obfuscated executable, is to search a state within the executable where the code (and all the structure of the binary if we are lucky) are unpacked. But finally what is packing / unpacking?

How does Packing works

Basically, the term "packing" refers to certain techniques used by an external program on a targeted executable for reducing it's length size, with some compression algorithms as an example. Then they will have to inject a small "stub" somewhere in the binary, which is just a piece of code within the binary, that will just decompress / unpack at runtime the packed binary and finally jumping on the Original Entry Point (OEP). The properties of such programs are very essential and useful for obfuscation, because it prevents a program from being statically analysed. However, the unpacking stub which will decipher and jump on the OEP is readable (in a disassembler). The concept of ciphering the code of an executable, injecting an unpacking stub and jump on the OEP is very simple, but how does it really work?

Code injection & common techniques

For this article, I will only be focusing on the elf executable format. So, we can distinguish a few techniques just by checking how the unpacking stub is injected. It will determine how the file is unpacked. We can distinguish two main families:

- The obfuscator keeps the structure of the original binary and inject an unpacking stub either in the code cave (memory area at the end of the executable PT_LOAD segment unused for alignment purposes) or it injects the unpacking stub at the end of the last PT_LOAD segment and edit the permissions of this segment by adding PF_X (executable) flag to its program header. In the injected code, it is only needed to be able to write on the packed data, so either the permissions of the target segment contain the PF_W flag or the injected payload edits directly the permissions at runtime with mprotect.

Basic schema of this first technique

The only drawback of injecting the unpacking stub in the code cave is has a finite length and will be different on each binaries. So the better technique would be to inject the unpacking stub at the end of the last PT_LOAD often with PF_R | PF_W permissions. The algorithm is quite simple and is explained in detail here. From a reverser point of view, we just need to understand that the EP is replaced by the address of the injected code. Of course, there are a lot of ways to inject code in an elf file, not only by expanding the length size of the last PT_LOAD. But they are similar and at a state of the execution, they will always jump at the OEP for executing the original binary. The main difference with the second technique is that the unpacking stub is just a parasite code which must not impact the execution of the original binary (if the program handles data relatively with an offset or directly a virtual address and that they are edited by the injection of the stub it will produce a crazy behavior).

- The obfuscator creates a new binary where it will keep a packed / ciphered version of the binary in a section (.data, .rodata it depends to the implementation). It's an entirely new binary and the executable code has just to: [1] unpack the ciphered binary [2] Extract informations from the executable header and from the program header table, and with these informations map the binary either by creating a new process or by mapping the binary in its own address space. Especially by using functions like mmap with MAP_FIXED argument for maping the binary at a particular location. [3] Then when the binary is completely loaded into memory, it has to jmp (often with an indirect jump) at the address of the OEP and the execution will continue without any problems. This technique is very interesting because you can heavily obfuscate the loader and don't care to the impact of the code injection. But it's a bit more complicated to implement if you want to cover all kind of binaries. Indeed when the binary isn't statically linked, you have to map the dynamic linker somewhere in the process. Finally your goal is to load the binary in the same way as the kernel so it's not always easy :) .

Basic schema of a loader

The schema is very basic and there is a lot of ways to go about loading a binary manually into memory so I will stay succinct. Firstly, the execution begins in the loader which will either unpack the header of the packed binary or directly extract the informations from the headers if it there are not ciphered. The needed informations to iterate through all the program headers and to store the original entry point are the e_phentsize, e_phnum and the e_entry fields of the elf header.


/* /usr/include/elf.h */

typedef struct {
  unsigned char e_ident[EI_NIDENT];     /* Magic number and other info */
  Elf64_Half    e_type;                 /* Object file type */
  Elf64_Half    e_machine;              /* Architecture */
  Elf64_Word    e_version;              /* Object file version */
  Elf64_Addr    e_entry;                /* Entry point virtual address */
  Elf64_Off     e_phoff;                /* Program header table file offset */
  Elf64_Off     e_shoff;                /* Section header table file offset */
  Elf64_Word    e_flags;                /* Processor-specific flags */
  Elf64_Half    e_ehsize;               /* ELF header size in bytes */
  Elf64_Half    e_phentsize;            /* Program header table entry size */
  Elf64_Half    e_phnum;                /* Program header table entry count */
  Elf64_Half    e_shentsize;            /* Section header table entry size */
  Elf64_Half    e_shnum;                /* Section header table entry count */
  Elf64_Half    e_shstrndx;             /* Section header string table index */
} Elf64_Ehdr;

/* Structure of the executable header */

Differences between the binary on disk and at runtime source: here

Then while iterating through all the program headers, it will proceed to check if p_type is equal to PT_LOAD if it is, it will make a mmap(p_vaddr, p_memsz, p_flags, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0x0); in order to setup a memory area for each segment. The PT_LOAD segments are the segments which are needed for the execution, they group the different sections / segments in the binary with the same permissions. That's why the sections with the same access permissions are next to each other in an ELF. On a pie based binary (random base address) the p_vaddr (virtual address) field in the program header of a PT_LOAD segment is equivalent to its offset in the file and all the jmp & data handling are done relatively by using rip (the instruction pointer), the binary can therefore be mmaped everywhere. For the others executables which have a defined base address it must mmap the segments of the binary from a particular base address often 0x400000. We can then use the MAP_FIXED argument to mmap a segment at a particular location. Next the mmap it has just to make a write(mmap_addr, .rodata + p_offset, p_filesz); (assuming the binary is located in the .rodata). And if the binary is dynamically linked it will take a look at the the PT_INTERP segment for extracting the pathname of the dynamic linker in order to parse its headers and to mmap all the segments in the same way that the original binary. When it's done, the loader will just edit a few fields on the stack : argv[0] *(rsp+8) field by the pathname of the original binary, and the following auxiliary vectors on the stack AT_PHDR, AT_PHENT, AT_PHNUM, AT_BASE (base address of interpreter), AT_ENTRY (OEP) according to the informations in the original executable header. The auxiliary vectors are just structures on the stack which display informations about the program, the system and certain fields that are mandatory for the dynamic linkers. Finally, if the binary is statically linked it will jump at the OEP or at the entry point of the dynamic linker with all the registers set to 0x0 except rsp & rip :)


/* /usr/include/elf.h */

typedef struct {
  Elf64_Word    p_type;                 /* Segment type */
  Elf64_Word    p_flags;                /* Segment flags */
  Elf64_Off     p_offset;               /* Segment file offset */
  Elf64_Addr    p_vaddr;                /* Segment virtual address */
  Elf64_Addr    p_paddr;                /* Segment physical address */
  Elf64_Xword   p_filesz;               /* Segment size in file */
  Elf64_Xword   p_memsz;                /* Segment size in memory */
  Elf64_Xword   p_align;                /* Segment alignment */
} Elf64_Phdr;

/* Structure of the program header */


  /* /usr/include/elf.h */

typedef struct {
  uint32_t a_type;              /* Entry type */
  union {
      uint32_t a_val;           /* Integer value */
    } a_un;
} Elf32_auxv_t;

  /* Elf32_auxv_t; */

  /* /usr/include/auxvec.h */

#define AT_NULL   0	/* end of vector */
#define AT_IGNORE 1	/* entry should be ignored */
#define AT_EXECFD 2	/* file descriptor of program */
#define AT_PHDR   3	/* program headers for program */
#define AT_PHENT  4	/* size of program header entry */
#define AT_PHNUM  5	/* number of program headers */
#define AT_PAGESZ 6	/* system page size */
#define AT_BASE   7	/* base address of interpreter */
#define AT_FLAGS  8	/* flags */
#define AT_ENTRY  9	/* entry point of program */
#define AT_NOTELF 10	/* program is not ELF */
#define AT_UID    11	/* real uid */
#define AT_EUID   12	/* effective uid */
#define AT_GID    13	/* real gid */
#define AT_EGID   14	/* effective gid */

More about auxiliary vectors and elf loading can be found here.

Now that we have a basic understanding of what are the most common techniques of packing, I think it's important to figure out and to keep in consideration that in reality we will be dealing often with some variants of these techniques. For example, an obfuscator can implement a few layers of packing by unpacking a binary which will unpack another binary ... Or unpack a part of the unpacking stub. Furthermore, packing is almost always associated with anti-debug/anti-disassembly techniques. Finally, the weak point of packing / unpacking is that it's crucial that at runtime the whole or a big part of the original binary is in clear in memory. But we can imagine that if the binary is instrumented, only a small part is deciphered at runtime. For example, the injected payload can unpack only the first few bytes of the target executable, evaluate the first instruction by disassembling it, inserting a hook just after and save the bytes erased by the hook. Next it has to jump at the OEP, execute the first instruction, reach the hook and go back to repeat cycle for the next instruction. So even if most of the time, packing & polymorphism are very easy to implement and to analyse, you can make things a bit more complicated to detect and to analyse for a Reverse Engineer. The only limit is your imagination 🙃.

Unpacking, thougths & algorithms

Now that we know the behavior of most of polymorphic engines, we can proceed to establish a few conditions on which we can automatically detect a polymorphic behavior:

- A write in a writable and executable area and potentially a jump in this area (page granularity, length size of a page: 0x1000). We can conclude that the other executable pages next to this page which have been written are suspicious.

- A call to mprotect, with the exec | write and read permissions are as seen above to trace and potentially to dump at a particular state of the execution.

- A call to mmap with PROT_EXEC | PROT_WRITE | PROT_READ protections are also interesting and as seen in the manual mapping part the mapping of the target binary implies always the usage of mmap syscall.

- A jmp <_reg_> instruction where reg contains the adress of an area either mprotect-ed with the PROT_EXEC | PROT_WRITE | PROT_READ flags or a mmaped area with the same protections. But it can be summed up by one rule: an indirect jmp in an executable area mapped by our program are suspecious and should be considerably acknowledge.

Now that we know exactly how to detect most of the packing techniques, we have another problem, the more complete the analysis, the more false-positive we get. Unavoidably, the analyse will take a looooooot of time. That's why for our automatic unpacker, we will just check that the execution was in an executable area mmaped by the program 👨🏻‍💻. And if it is the case we will either looking for an elf header or just dump the whole page.

Qiling, the future of binary analysis

The Qiling framework is an awesome binary emulation / instrumentation framework ! It's based on the Unicorn engine and groups all the qualities of a virtual machine based sandbox but without any iso . Indeed, it emulates directly the kernel of many operating systems ( Windows X86 32/64, Linux X86 32/64, ARM, AARCH64, MIPS, MacOS X86 32/64, FreeBSD X86 32/64 and even UEFI !! ). A big part of the syscalls are reimplemented and it deludes the executable into thinking it's actually being executing into a real operating system. It's based on a virtual file system called rootfs which is easy to handle. As an example, if a library (.so or .dll) is missed you will have just to append it in the corresponding rootfs. The framework is developped in python(3), it's a very young and nice project, so feel free to contribute. Furthermore, there is a lot of unimplemented functions and to reimplement these you have just to understand how they work, develop your implementation in python and hook the call of these functions. If it works great, integrate them to the core and make a pull request 🧠. On this side, qiling is very interesting because it lets you to have a better understanding of the kernel. With qiling you can litterally hook everythings according to the documentation. Very useful for automatic unpacking !!

First approach with qiling

To install the qiling framework you can either directly use the docker image developped by qiling here or install my docker image which groups an environment oriented RE / vuln research by typing docker pull nasmre/rebox:latest. For the needs of the article I will work on a crackme developped by @aassfxxx ! The structure of program using qiling looks like this:

# author: nasm

import sys

sys.path.append("../")
# Specification of the qiling's directory (mandatory)

from qiling import Qiling
# [1]

if __name__ == "__main__":
    _ql_ = Qiling(["rootfs/x8664_linux/bin/leetness"], "rootfs/x8664_linux/", output="debug")
    # [2]

    _ql_.run()
    # [3]

In [1] we import the main part of the qiling engine which will return in [2] a ql object. This object is the base of all the qiling engine. It has especial run method which will just launch the emulation on the executable gived to Qiling in [2] with the file system specified in second argument. For this article, we will work on a real world crackme which is using composed of a loader and a ciphered binary. And as shown below the basic qiling's analysis does not work properly.

[+] load 0x400000 - 0x401000
[+] load 0x7ff13371000 - 0x7ff1337e000
[+] load 0x7ff1337e000 - 0x7ff13383000
[+] load 0x7ff13383000 - 0x7ff13385000
[+] mem_start: 0x400000 mem_end: 0x7ff13385000
[+] mmap_address is : 0x7fffb7dd6000
open(/proc/self/exe, 0x0, 0o0) = -2
[+] open(/proc/self/exe, O_RDONLY, 0o0) = -2
[!] File Not Found /proc/self/exe
lseek(4294967294, 0x0, 0x2) = 0
[!] Syscall ERROR: ql_syscall_lseek DEBUG: list index out of range
Traceback (most recent call last):
  File "article_qiling.py", line 15, in module
    ql.run()
  File "/usr/local/lib/python3.7/dist-packages/qiling-1.1a1-py3.7.egg/qiling/core.py", line 197, in run
    self.os.run()
  File "/usr/local/lib/python3.7/dist-packages/qiling-1.1a1-py3.7.egg/qiling/os/linux/linux.py", line 119, in run
    raise self.ql.internal_exception
  File "/usr/local/lib/python3.7/dist-packages/qiling-1.1a1-py3.7.egg/qiling/utils.py", line 19, in wrapper
    return func(*args, **kw)
  File "/usr/local/lib/python3.7/dist-packages/qiling-1.1a1-py3.7.egg/qiling/core_hooks.py", line 125, in _hook_insn_cb
    ret = h.call(ql, *args[ : -1])
  File "/usr/local/lib/python3.7/dist-packages/qiling-1.1a1-py3.7.egg/qiling/core_hooks.py", line 34, in call
    return self.callback(ql, *args)
  File "/usr/local/lib/python3.7/dist-packages/qiling-1.1a1-py3.7.egg/qiling/os/linux/linux.py", line 65, in hook_syscall
    return self.load_syscall(intno)
  File "/usr/local/lib/python3.7/dist-packages/qiling-1.1a1-py3.7.egg/qiling/os/posix/posix.py", line 94, in load_syscall
    self.syscall_map(self.ql, self.get_func_arg()[0], self.get_func_arg()[1], self.get_func_arg()[2], self.get_func_arg()[3], self.get_func_arg()[4], self.get_func_arg()[5])
  File "/usr/local/lib/python3.7/dist-packages/qiling-1.1a1-py3.7.egg/qiling/os/posix/syscall/unistd.py", line 142, in ql_syscall_lseek
    regreturn = ql.os.file_des[lseek_fd].lseek(lseek_ofset, lseek_origin)

Indeed, the executable is trying to open in O_RDONLY /proc/self/exe which corresponds to a symlink on itself. Being already mmaped, it can be opened only in O_RDONLY. For now qiling has not yet implemented the /proc interface, so we need to do it by hand. We can create the setup_self_proc function which will copy and rename the binary at the right location. We can't make a symlink because the open implementation of qiling cannot open the symbolic links.

import sys
sys.path.append("/home/n4sm/Downloads/qiling")

from qiling import Qiling

def setup_self_proc(exe_path : str, rootfs : str) -> None:
  with open(rootfs + "/proc/self/exe", "wb") as file_created:
      file_created.seek(0x0)

      with open(exe_path, "rb") as exe_file:
          data_exe = exe_file.read()

          file_created.write(data_exe)

if __name__ == "__main__":
    ql = Qiling(["/mnt/p/leetness"], "/home/n4sm/Downloads/qiling/examples/rootfs/x8664_linux", output="debug")
      
    setup_self_proc("/mnt/p/leetness", "/home/n4sm/Downloads/qiling/examples/rootfs/x8664_linux")
      
    ql.run()

Now that it's fixed, let's launch again the script:

[+] load 0x400000 - 0x401000
[+] load 0x7ff13371000 - 0x7ff1337e000
[+] load 0x7ff1337e000 - 0x7ff13383000
[+] load 0x7ff13383000 - 0x7ff13385000
[+] mem_start: 0x400000 mem_end: 0x7ff13385000
[+] mmap_address is : 0x7fffb7dd6000
open(/proc/self/exe, 0x0, 0o0) = 3
[+] open(/proc/self/exe, O_RDONLY, 0o0) = 3
[+] File Found: /proc/self/exe
lseek(3, 0x0, 0x2) = 0
lseek(3, 0x0, 0x2) = 79292
[+] log mmap2 - mmap2(0x0, 0x1e000, 0x3, 0x22, 4294967295, 0)
[+] log mmap2 - mmap2(0x0, 0x1e000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 4294967295, 0)
[+] log mmap2 - return addr : 0x7fffb7dd6000
[+] log mmap2 - addr range  : 0x7fffb7dd6000 - 0x7fffb7df4000
[+] log mmap2 - mapping needed
mmap2(0x0, 0x1e000, 0x3, 0x22, 4294967295, 0) = 0x7fffb7dd6000
[+] mmap2_base is 0x7fffb7dd6000
lseek(3, 0x0, 0x0) = 0
lseek(3, 0x0, 0x0) = 0
read(3, 0x7fffb7dd6028, 0x135bc) = 79292
[+] log mmap2 - mmap2(0x0, 0xc01000, 0x3, 0x22, 4294967295, 0)
[+] log mmap2 - mmap2(0x0, 0xc01000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 4294967295, 0)
[+] log mmap2 - return addr : 0x7fffb7df4000
[+] log mmap2 - addr range  : 0x7fffb7df4000 - 0x7fffb89f5000
[+] log mmap2 - mapping needed
mmap2(0x0, 0xc01000, 0x3, 0x22, 4294967295, 0) = 0x7fffb7df4000
[+] mmap2_base is 0x7fffb7df4000
[+] log mmap2 - mmap2(0x400000, 0x1000, 0x7, 0x32, 4294967295, 0)
[+] log mmap2 - mmap2(0x400000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, 4294967295, 0)
[+] log mmap2 - return addr : 0x400000
[+] log mmap2 - addr range  : 0x400000 - 0x401000
mmap2(0x400000, 0x1000, 0x7, 0x32, 4294967295, 0) = 0x400000
[+] mmap2_base is 0x400000
[!] Emulation Error

[-] rax :        0x400000
[-] rbx :        0x7fffb7df398c
[-] rcx :        0x0
[-] rdx :        0x17c
[-] rsi :        0x7fffb85f4040
[-] rdi :        0x400000
[-] rbp :        0x400000
[-] rsp :        0x80000000dc38
[-] r8  :        0x7f
[-] r9  :        0x0
[-] r10 :        0x32
[-] r11 :        0xbcf65a4b
[-] r12 :        0x80000000dd50
[-] r13 :        0x0
[-] r14 :        0x17c
[-] r15 :        0x80000000dca0
[-] rip :        0x7ff133725d8
[-] ef  :        0x0
[-] cs  :        0x1b
[-] ss  :        0x28
[-] ds  :        0x28
[-] es  :        0x28
[-] fs  :        0x0
[-] gs  :        0x0
[-] st0 :        0x0
[-] st1 :        0x0
[-] st2 :        0x0
[-] st3 :        0x0
[-] st4 :        0x0
[-] st5 :        0x0
[-] st6 :        0x0
[-] st7 :        0x0

[+] PC = 0x7ff133725d8
[+] Start      End        Perm.  Path
[+] 00003000 - 00004000 - rwx    [GDT]
[+] 00400000 - 00401000 - r--    /mnt/p/leetness
[+] 7ff13371000 - 7ff1337e000 - r-x    /mnt/p/leetness
[+] 7ff1337e000 - 7ff13383000 - r--    /mnt/p/leetness
[+] 7ff13383000 - 7ff13385000 - rw-    /mnt/p/leetness
[+] 7ff13385000 - 7ff13387000 - rwx    hook mem
[+] 7fffb7dd6000 - 7fffb7df4000 - rwx    [mapped]
[+] 7fffb7df4000 - 7fffb89f5000 - rwx    [mapped]
[+] 7ffffffde000 - 80000000e000 - rwx    [stack]

[+] ['0x31', '0xc9', '0x66', '0xf', '0x1f', '0x44', '0x0', '0x0']

[+] 0x7ff133725d8      31 c9 66 0f 1f 44 00 00 44 0f b6 04 0e 44 88 04 08 48 83 c1 01 48 39 ca 75 ee c3 66 66 2e 0f 1f 84 00 00 00 00 00 66 90 48 89 f8 48 89 f9 4c 8d 04 3a 48 85 d2 74 0d 90 40 88 31 48 83 c1 01 49 
xor ecx, ecx
nop word ptr [rax + rax]
movzx r8d, byte ptr [rsi + rcx]
mov byte ptr [rax + rcx], r8b
add rcx, 1
cmp rdx, rcx
jne 0x7ff133725e0
ret
nop word ptr cs:[rax + rax]
nop
mov rax, rdi
mov rcx, rdi
lea r8, [rdx + rdi]
test rdx, rdx
je 0x7ff1337261c
nop
mov byte ptr [rcx], sil
add rcx, 1
Traceback (most recent call last):
  File "article_qiling.py", line 26, in module
    ql.run()
  File "/usr/local/lib/python3.7/dist-packages/qiling-1.1a1-py3.7.egg/qiling/core.py", line 197, in run
    self.os.run()
  File "/usr/local/lib/python3.7/dist-packages/qiling-1.1a1-py3.7.egg/qiling/os/linux/linux.py", line 112, in run
    self.ql.emu_start(self.ql.loader.elf_entry, self.exit_point, self.ql.timeout, self.ql.count)
  File "/usr/local/lib/python3.7/dist-packages/qiling-1.1a1-py3.7.egg/qiling/core.py", line 248, in emu_start
    self.uc.emu_start(begin, end, timeout, count)
  File "/usr/local/lib/python3.7/dist-packages/unicorn/unicorn.py", line 317, in emu_start
    raise UcError(status)
unicorn.unicorn.UcError: Write to write-protected memory (UC_ERR_WRITE_PROT)

We are getting another crash because of the mmap implementation of mmap by qiling. In its implementation the requested blocks of memory (aligned on a page) are contiguous and each time a new block is requested a "mmap base" variable is incremented. Furthermore, It does not honor the permissions and is basically allocating all the blocks in RWX. Finally, the MAP_FIXED argument of mmap which is asking for a page at a particular virtual address is not supported. That's why we need to edit a little bit of the mmap implementation.

# https://github.com/qilingframework/qiling/blob/master/qiling/os/posix/syscall/mman.py

# Actual implementation

# =-=-=-=-=-=-=-=-=-=-=-=-=

# author: nasm

import sys

sys.path.append("/home/n4sm/Downloads/qiling")
# Specification of the qiling's directory (mandatory)

from qiling import *
# [1]
from qiling.const import *
# QL_ARCH
from qiling.os.posix.const_mapping import *
# mmap_prot_mapping

import random
# radom.choice

from unicorn import *
# UC_PROT_ALL

def is_available(ql, addr, size):
    '''
    The main function of is_available is to determine 
    whether the memory starting with addr and having a size of length can be used for allocation.
    If it can be allocated, returns True.
    If it cannot be allocated, it returns False.
    '''
    try:
        ql.mem.map(addr, size)
    except:
        return False    

    ql.mem.unmap(addr, size)

    return True


def mmap_c(ql, mmap2_addr, mmap2_length, mmap2_prot, mmap2_flags, mmap2_fd, mmap2_pgoffset):

    mmap2_length_align = ((mmap2_length + 0x1000 - 1) // 0x1000) * 0x1000
    mmap2_addr_align = ((mmap2_addr + 0x1000 - 1) // 0x1000) * 0x1000

    ret = False

    # this is ugly patch, we might need to get value from elf parse,
    # is32bit or is64bit value not by arch

    MAP_ANONYMOUS=32

    if (ql.archtype== QL_ARCH.ARM64) or (ql.archtype== QL_ARCH.X8664):
        mmap2_fd = ql.unpack64(ql.pack64(mmap2_fd))

    elif (ql.archtype== QL_ARCH.MIPS):
        mmap2_fd = ql.unpack32s(ql.mem.read(mmap2_fd, 4))
        mmap2_pgoffset = ql.unpack32(ql.mem.read(mmap2_pgoffset, 4)) * 4096
        MAP_ANONYMOUS=2048
    else:
        mmap2_fd = ql.unpack32s(ql.pack32(mmap2_fd))
        mmap2_pgoffset = mmap2_pgoffset * 4096

    if mmap2_addr != 0 and ql.mem.is_mapped(mmap2_addr, mmap2_length):
        ql.dprint(D_INFO, "[+] log mmap2 - mmap2(0x%x, 0x%x, 0x%x, 0x%x, %x, %d)" % (
        mmap2_addr, mmap2_length, mmap2_prot, mmap2_flags, mmap2_fd, mmap2_pgoffset))
        ql.dprint(D_INFO, "[+] log mmap2 - mmap2(0x%x, 0x%x, %s, %s, %x, %d)" % (
        mmap2_addr, mmap2_length, mmap_prot_mapping(mmap2_prot), mmap_flag_mapping(mmap2_flags), mmap2_fd, mmap2_pgoffset))
        ql.dprint(D_INFO, "[+] log mmap2 - return addr : " + hex(0x0))

        ql.uc.mem_protect(mmap2_addr_align, mmap2_length_align, mmap2_prot)

        local_hook(ql, mmap2_addr, mmap2_length_align, mmap2_prot)

        ql.mem.add_mapinfo(mmap2_addr, mmap2_addr + mmap2_length_align, mem_p = mmap2_prot, mem_info = "[mapped]")
        return 0x0

    if mmap2_addr == 0:

        while ret == False:
            random_base = random.randint(0x0, 0x00007fffffffffff)

            random_base_align = ((random_base + 0x1000 - 1) // 0x1000) * 0x1000

            ret = is_available(ql, random_base_align, mmap2_length_align)

    else:
        random_base_align = mmap2_addr_align

    ql.dprint(D_INFO, "[+] log mmap2 - mmap2(0x%x, 0x%x, 0x%x, 0x%x, %x, %d)" % (
    mmap2_addr, mmap2_length, mmap2_prot, mmap2_flags, mmap2_fd, mmap2_pgoffset))
    ql.dprint(D_INFO, "[+] log mmap2 - mmap2(0x%x, 0x%x, %s, %s, %x, %d)" % (
    mmap2_addr, mmap2_length, mmap_prot_mapping(mmap2_prot), mmap_flag_mapping(mmap2_flags), mmap2_fd, mmap2_pgoffset))
    ql.dprint(D_INFO, "[+] log mmap2 - return addr : " + hex(random_base_align))
    ql.dprint(D_INFO, "[+] log mmap2 - addr range  : " + hex(random_base_align) + ' - ' + hex(
        random_base_align + mmap2_length_align))

    try:
        ql.mem.map(random_base_align, mmap2_length_align)
    except:
        ql.mem.show_mapinfo()
        raise

    ql.uc.mem_protect(random_base_align, mmap2_length_align, UC_PROT_ALL)
    # To avoid an invalid memory write

    ql.mem.write(random_base_align, b'\x00' * mmap2_length_align)

    if ((mmap2_flags & MAP_ANONYMOUS) == 0) and mmap2_fd < 256 and ql.os.file_des[mmap2_fd] != 0:
        ql.os.file_des[mmap2_fd].lseek(mmap2_pgoffset)
        data = ql.os.file_des[mmap2_fd].read(mmap2_length)
        mem_info = str(ql.os.file_des[mmap2_fd].name)

        ql.dprint(D_INFO, "[+] log mem write : " + hex(len(data)))
        ql.dprint(D_INFO, "[+] log mem mmap2  : " + mem_info)
        ql.mem.add_mapinfo(random_base_align, random_base_align + (len(data)), mem_p = UC_PROT_ALL, mem_info = mem_info)
        ql.mem.write(random_base_align, data)


    ql.nprint("mmap2(0x%x, 0x%x, 0x%x, 0x%x, %d, %d) = 0x%x" % (mmap2_addr, mmap2_length, mmap2_prot, mmap2_flags, mmap2_fd, mmap2_pgoffset, random_base_align))

    regreturn = random_base_align
    ql.dprint(D_INFO, "[+] mmap2_block is at 0x%x with a length of 0x%x" % (regreturn, mmap2_length_align))

    ql.uc.mem_protect(random_base_align, mmap2_length_align, mmap2_prot)

    ql.mem.add_mapinfo(random_base_align, random_base_align + mmap2_length_align, mem_p = mmap2_prot, mem_info="[mapped]")
    ql.mem.show_mapinfo()

    ql.os.definesyscall_return(regreturn)


def setup_self_proc(exe_path : str, rootfs : str) -> None:
  with open(rootfs + "/proc/self/exe", "wb") as file_created:
      file_created.seek(0x0)

      with open(exe_path, "rb") as exe_file:
          data_exe = exe_file.read()

          file_created.write(data_exe)

if __name__ == "__main__":
    ql = Qiling(["/mnt/p/leetness"], "/home/n4sm/Downloads/qiling/examples/rootfs/x8664_linux", output="debug")

    setup_self_proc("/mnt/p/leetness", "/home/n4sm/Downloads/qiling/examples/rootfs/x8664_linux")

    ql.set_syscall(0x9, mmap_c)
    # [1]

    ql.run()

In [1] we replace the syscall corresponding to the 0x9 syscall number (sys_mmap) by our implementation. The implementation is a bit different; however, the code is reasonably understandable with the comments. If you are not familiar with the mmap syscall I advise you to read either man or directly the implementation in the linux kernel . And you can see that with this version, the emulation works perfectly 👌:

[+] load 0x400000 - 0x401000
[+] load 0x7ff13371000 - 0x7ff1337e000
[+] load 0x7ff1337e000 - 0x7ff13383000
[+] load 0x7ff13383000 - 0x7ff13385000
[+] mem_start: 0x400000 mem_end: 0x7ff13385000
[+] mmap_address is : 0x7fffb7dd6000
open(/proc/self/exe, 0x0, 0o0) = 3
[+] open(/proc/self/exe, O_RDONLY, 0o0) = 3
[+] File Found: /proc/self/exe
lseek(3, 0x0, 0x2) = 0
lseek(3, 0x0, 0x2) = 79292
[+] log mmap2 - mmap2(0x0, 0x1e000, 0x3, 0x22, ffffffff, 0)
[+] log mmap2 - mmap2(0x0, 0x1e000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, ffffffff, 0)
[+] log mmap2 - return addr : 0x345e6a56c000
[+] log mmap2 - addr range  : 0x345e6a56c000 - 0x345e6a58a000
mmap2(0x0, 0x1e000, 0x3, 0x22, 4294967295, 0) = 0x345e6a56c000
[+] mmap2_block is at 0x345e6a56c000 with a length of 0x1e000
[+] Start      End        Perm.  Path
[+] 00003000 - 00004000 - rwx    [GDT]
[+] 00400000 - 00401000 - r--    /mnt/p/leetness
[+] 7ff13371000 - 7ff1337e000 - r-x    /mnt/p/leetness
[+] 7ff1337e000 - 7ff13383000 - r--    /mnt/p/leetness
[+] 7ff13383000 - 7ff13385000 - rw-    /mnt/p/leetness
[+] 7ff13385000 - 7ff13387000 - rwx    hook mem
[+] 345e6a56c000 - 345e6a58a000 - rw-    [mapped]
[+] 7ffffffde000 - 80000000e000 - rwx    [stack]
lseek(3, 0x0, 0x0) = 0
lseek(3, 0x0, 0x0) = 0
read(3, 0x345e6a56c028, 0x135bc) = 79292
[+] log mmap2 - mmap2(0x0, 0xc01000, 0x3, 0x22, ffffffff, 0)
[+] log mmap2 - mmap2(0x0, 0xc01000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, ffffffff, 0)
[+] log mmap2 - return addr : 0x3b475f9b2000
[+] log mmap2 - addr range  : 0x3b475f9b2000 - 0x3b47605b3000
mmap2(0x0, 0xc01000, 0x3, 0x22, 4294967295, 0) = 0x3b475f9b2000
[+] mmap2_block is at 0x3b475f9b2000 with a length of 0xc01000
[+] Start      End        Perm.  Path
[+] 00003000 - 00004000 - rwx    [GDT]
[+] 00400000 - 00401000 - r--    /mnt/p/leetness
[+] 7ff13371000 - 7ff1337e000 - r-x    /mnt/p/leetness
[+] 7ff1337e000 - 7ff13383000 - r--    /mnt/p/leetness
[+] 7ff13383000 - 7ff13385000 - rw-    /mnt/p/leetness
[+] 7ff13385000 - 7ff13387000 - rwx    hook mem
[+] 345e6a56c000 - 345e6a58a000 - rw-    [mapped]
[+] 3b475f9b2000 - 3b47605b3000 - rw-    [mapped]
[+] 7ffffffde000 - 80000000e000 - rwx    [stack]
[+] log mmap2 - mmap2(0x400000, 0x1000, 0x7, 0x32, ffffffff, 0)
[+] log mmap2 - mmap2(0x400000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, ffffffff, 0)
[+] log mmap2 - return addr : 0x0
[+] log mmap2 - mmap2(0x401000, 0x1000, 0x7, 0x32, ffffffff, 0)
[+] log mmap2 - mmap2(0x401000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, ffffffff, 0)
[+] log mmap2 - return addr : 0x401000
[+] log mmap2 - addr range  : 0x401000 - 0x402000
mmap2(0x401000, 0x1000, 0x7, 0x32, 4294967295, 0) = 0x401000
[+] mmap2_block is at 0x401000 with a length of 0x1000
[+] Start      End        Perm.  Path
[+] 00003000 - 00004000 - rwx    [GDT]
[+] 00400000 - 00401000 - rwx    [mapped]
[+] 00401000 - 00402000 - rwx    [mapped]
[+] 7ff13371000 - 7ff1337e000 - r-x    /mnt/p/leetness
[+] 7ff1337e000 - 7ff13383000 - r--    /mnt/p/leetness
[+] 7ff13383000 - 7ff13385000 - rw-    /mnt/p/leetness
[+] 7ff13385000 - 7ff13387000 - rwx    hook mem
[+] 345e6a56c000 - 345e6a58a000 - rw-    [mapped]
[+] 3b475f9b2000 - 3b47605b3000 - rw-    [mapped]
[+] 7ffffffde000 - 80000000e000 - rwx    [stack]
[+] log mmap2 - mmap2(0x402000, 0x1000, 0x7, 0x32, ffffffff, 0)
[+] log mmap2 - mmap2(0x402000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, ffffffff, 0)
[+] log mmap2 - return addr : 0x402000
[+] log mmap2 - addr range  : 0x402000 - 0x403000
mmap2(0x402000, 0x1000, 0x7, 0x32, 4294967295, 0) = 0x402000
[+] mmap2_block is at 0x402000 with a length of 0x1000
[+] Start      End        Perm.  Path
[+] 00003000 - 00004000 - rwx    [GDT]
[+] 00400000 - 00401000 - rwx    [mapped]
[+] 00401000 - 00402000 - rwx    [mapped]
[+] 00402000 - 00403000 - rwx    [mapped]
[+] 7ff13371000 - 7ff1337e000 - r-x    /mnt/p/leetness
[+] 7ff1337e000 - 7ff13383000 - r--    /mnt/p/leetness
[+] 7ff13383000 - 7ff13385000 - rw-    /mnt/p/leetness
[+] 7ff13385000 - 7ff13387000 - rwx    hook mem
[+] 345e6a56c000 - 345e6a58a000 - rw-    [mapped]
[+] 3b475f9b2000 - 3b47605b3000 - rw-    [mapped]
[+] 7ffffffde000 - 80000000e000 - rwx    [stack]
write(1,402000,25) = 0
Could you get the flag ?
kurisu
read(0, 0x80000000de78, 0x1f) = 7
write(1,40203c,18) = 0
You lost THE GAME
exit(0) = 0

The binary is basically opening /proc/self/exe, getting the length of itself with lseek, mapping a lot of things and then is asking for a flag. And when the flag is bad it makes us loose to the game .

Hooking & Unpacking

Now that we have the basics on qiling, we will begin to implement some unpacking techniques by hooking certain functions / syscall.

Firstly, according to the first rule described previously in Unpacking, thougths & algorithms, we will record all the executable memory areas to add them in a list. It can be done by hooking syscalls like mprotect & mmap. We hook these syscall like we've done previously with sys_mmap. And each time we have to mmap / mprotect a memory area with the bit PROT_EXEC we save its length and it base address in a list. So we can get all the pages mmaped with the rights permissions.

mem_regions_exec = [] # list of dic
mem_regions = []

def is_available(ql, addr, size):
    '''
    The main function of is_available is to determine 
    whether the memory starting with addr and having a size of length can be used for allocation.
    If it can be allocated, returns True.
    If it cannot be allocated, it returns False.
    '''
    try:
        ql.mem.map(addr, size)
    except:
        return False    

    ql.mem.unmap(addr, size)

    return True

def local_hook(ql, addr, size, prot) -> int:
    """
    Records all the memory area mmaped / mprotected with the rights permissions
    If the area is executable it's added to mem_regions_exec
    """

    tmp_lst = [addr, size, prot]

    s = True

    if prot & UC_PROT_EXEC:

        print("[+] mapping with PROT_EXEC prot 0x%x - 0x%x" % (addr, addr + size))

        for i in range(len(mem_regions_exec)):
            if tmp_lst[0] == mem_regions_exec[i][0]:
                
                mem_regions_exec[i] = tmp_lst
                # Get base address and sz

                s = False
                break

        if s:
            mem_regions_exec.append(tmp_lst)

    for i in range(len(mem_regions)):
        if tmp_lst[0] == mem_regions[i][0]:
            
            mem_regions[i] = tmp_lst
            # Update permissions

            return 0x0

    mem_regions.append(tmp_lst)

    return 0x0

# ...

def mmap_c(ql, mmap2_addr, mmap2_length, mmap2_prot, mmap2_flags, mmap2_fd, mmap2_pgoffset):

    # ...

    local_hook(ql, random_base_align, mmap2_length_align, mmap2_prot)

    # ...

# ...

def ql_syscall_mprotect(ql, mprotect_start, mprotect_len, mprotect_prot, *args, **kw):
    regreturn = 0
    ql.nprint("mprotect(0x%x, 0x%x, 0x%x) = %d" % (mprotect_start, mprotect_len, mprotect_prot, regreturn))
    ql.dprint(D_INFO, "[+] mprotect(0x%x, 0x%x, %s) = %d" % (
    mprotect_start, mprotect_len, mmap_prot_mapping(mprotect_prot), regreturn))

    base_align = ((mprotect_start + 0x1000 - 1) // 0x1000) * 0x1000
    length_align = ((mprotect_len + 0x1000 - 1) // 0x1000) * 0x1000

    ql.uc.mem_protect(base_align, length_align, mprotect_prot)
    ql.mem.add_mapinfo(base_align, base_align+length_align, mem_p=mprotect_prot, mem_info="[mprotect]")

    local_hook(ql, base_align, length_align, mprotect_prot)

    ql.os.definesyscall_return(regreturn)

Now we have an exec_memory_area which contains all the executable areas. One thing we can do is dump the page each time it's written. It can be interesting in the case where the binary is unpacking something, jumping on, and rewrite a part of the same page with another shellcode to jump on this shellcode later. But if we do that we will dump a loooot of pages so it's not convenient. For our scenario, we will dump the executable pages mapped by the program each time the execution is transfered there. More precisely, each time the instruction pointer is in an executable memory area, we will print the instruction executed an add it in a trace. And the first time the execution jump on one of those pages, we will look for the most closest elf header in the memory compared to the target page. It will be effective only for the most basic packed binaries because of an elf header is useless for the execution it's often deleted. It's however mandatory for the parsing & mapping part in the case described here. For unpacking stubs it stays effective because most of the time the unpacking stub is just basically unpacking the packed code and jump on, without unmapping the elf header.

To do that, we need to hook each instruction to check if the execution is in one of our memory area that we've registered as being mapped with executable permissions by the program. Qiling provide the hook_code(callback) function, which will call callback for each instruction executed with these arguments: callback(ql: object, addr: int, size: int). ql is the qiling instance, addr is the address of the instruction and size the length of this one. I've also implemented a small cache for performances purposes. It looks like this:


def is_in_range(addr) -> bool:
    """
    Checks if an address is in the range of executable memeory area mapped by the process
    """
    for area in mem_regions_exec:
            if addr >= area[0] and addr <= area[0] + area[1]:
                return True
    return False

def dump(ql, addr):
    matchs = ql.mem.search(b"\x7fELF")
    # All the matchs in the current virtual memory

    average = {}

    for ar in matchs:
        if addr > ar:
            # If the address is next the match because it cannot be lower (it's the header) 
            
            average[ar] = addr - ar
            # Get the offset

    m_sorted = sorted(average, key=lambda x: average.keys())
    # Get the matchs the most closer to addr

    addr_dmp = m_sorted[0]

    # Here is a very quick parsing of the header to know what is the length of the binary 
    # (the offset of the section header table plus its length)

    shoff = int.from_bytes(ql.mem.read(addr_dmp + 40, 0x8), "little")
    print("[+] Section header offset : 0x%x" % shoff)

    len_section_headers = int.from_bytes(ql.mem.read(addr_dmp + 58, 0x2), "little") * int.from_bytes(ql.mem.read(addr_dmp + 60, 0x2), "little")
    print("[+] length of the section headers : 0x%x" % len_section_headers)

    len_dmp = shoff + len_section_headers
    print("[+] Final binary dumped : 0x%x - 0x%x" % (addr_dmp, addr_dmp + len_dmp))

    dmp = ql.mem.read(addr_dmp, len_dmp)

    with open("bin.u", "wb+") as file_unpacked:
        # Dump on the disk
        file_unpacked.write(dmp)

def _callback_(ql, addr, size):
    global md, idx, pinst

    # (addr >> 12) << 12) is the base address of the page to which belongs addr
    if ((addr >> 12) << 12) in c_m or addr in cache:
        # If the address is already in the cache miss or in the cache it returns
        return 0

    elif not is_in_range(((addr >> 12) << 12)):
        # If it is not yet in the cache miss and that addr is not in a mapped area in PF_X
        print("{} append to the cache".format(hex(((addr >> 12) << 12))))
        c_m.append(((addr >> 12) << 12))
        return 0

    if len(cache) < 1024:
        # cache for obfuscated instrcutions (to avoid loop repetitions)
        cache.append(addr)
    else:
        cache[idx % 1024] = addr
        idx += 1

    pinst += 1
    # Number of unpacked instructions executed 

    if pinst == 0x1:
        dump(ql, addr)

    ins = ql.mem.read(addr, size)
    # We read the raw bytes for the instruction

    for i in md.disasm(ins, addr):
        # We disassemble it
        insnt = "{} : {} {}".format(hex(i.address), i.mnemonic, i.op_str)

        print(insnt)

        if insnt not in trace:
            # append meta datas for the instruction in the trace
            trace.append(insnt)

    return 0

if __name__ == "__main__":

    # ...

    ql_.hook_code(_callback_)

    # ...


I think that the comments are clear enough so now let's put all together and launch it !!

[+] load 0x400000 - 0x401000
[+] load 0x7ff13371000 - 0x7ff1337e000
[+] load 0x7ff1337e000 - 0x7ff13383000
[+] load 0x7ff13383000 - 0x7ff13385000
[+] mem_start: 0x400000 mem_end: 0x7ff13385000
[+] mmap_address is : 0x7fffb7dd6000
0x7ff13371000 append to the cache
0x7ff13372000 append to the cache
open(/proc/self/exe, 0x0, 0o0) = 3
[+] open(/proc/self/exe, O_RDONLY, 0o0) = 3
[+] File Found: /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/proc/self/exe
lseek(3, 0x0, 0x2) = 0
lseek(3, 0x0, 0x2) = 79292
[+] log mmap2 - mmap2(0x0, 0x1e000, 0x3, 0x22, ffffffff, 0)
[+] log mmap2 - mmap2(0x0, 0x1e000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, ffffffff, 0)
[+] log mmap2 - return addr : 0x2489f63bf000
[+] log mmap2 - addr range  : 0x2489f63bf000 - 0x2489f63dd000
mmap2(0x0, 0x1e000, 0x3, 0x22, 4294967295, 0) = 0x2489f63bf000
[+] mmap2_block is at 0x2489f63bf000 with a length of 0x1e000
[+] Start      End        Perm.  Path
[+] 00003000 - 00004000 - rwx    [GDT] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 00400000 - 00401000 - r--    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff13371000 - 7ff1337e000 - r-x    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff1337e000 - 7ff13383000 - r--    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff13383000 - 7ff13385000 - rw-    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff13385000 - 7ff13387000 - rwx    [hook_mem] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 2489f63bf000 - 2489f63dd000 - rw-    [mapped]
[+] 7ffffffde000 - 80000000e000 - rwx    [stack]
[+] ffffffffff600000 - ffffffffff601000 - rwx    [vsyscall]
lseek(3, 0x0, 0x0) = 0
lseek(3, 0x0, 0x0) = 0
read(3, 0x2489f63bf028, 0x135bc) = 79292
0x7ff13373000 append to the cache
0x7ff13375000 append to the cache
0x7ff13374000 append to the cache
0x7ff13376000 append to the cache
0x7ff1337c000 append to the cache
0x7ff1337a000 append to the cache
0x7ff1337d000 append to the cache
[+] log mmap2 - mmap2(0x0, 0xc01000, 0x3, 0x22, ffffffff, 0)
[+] log mmap2 - mmap2(0x0, 0xc01000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, ffffffff, 0)
[+] log mmap2 - return addr : 0x19f699ed4000
[+] log mmap2 - addr range  : 0x19f699ed4000 - 0x19f69aad5000
mmap2(0x0, 0xc01000, 0x3, 0x22, 4294967295, 0) = 0x19f699ed4000
[+] mmap2_block is at 0x19f699ed4000 with a length of 0xc01000
[+] Start      End        Perm.  Path
[+] 00003000 - 00004000 - rwx    [GDT] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 00400000 - 00401000 - r--    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff13371000 - 7ff1337e000 - r-x    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff1337e000 - 7ff13383000 - r--    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff13383000 - 7ff13385000 - rw-    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff13385000 - 7ff13387000 - rwx    [hook_mem] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 19f699ed4000 - 19f69aad5000 - rw-    [mapped]
[+] 2489f63bf000 - 2489f63dd000 - rw-    [mapped]
[+] 7ffffffde000 - 80000000e000 - rwx    [stack]
[+] ffffffffff600000 - ffffffffff601000 - rwx    [vsyscall]
0x7ff13377000 append to the cache
0x7ff13378000 append to the cache
0x7ff13379000 append to the cache
[+] log mmap2 - mmap2(0x400000, 0x1000, 0x7, 0x32, ffffffff, 0)
[+] log mmap2 - mmap2(0x400000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, ffffffff, 0)
[+] log mmap2 - return addr : 0x0
[+] mapping with PROT_EXEC prot 0x400000 - 0x401000
[+] log mmap2 - mmap2(0x401000, 0x1000, 0x7, 0x32, ffffffff, 0)
[+] log mmap2 - mmap2(0x401000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, ffffffff, 0)
[+] log mmap2 - return addr : 0x401000
[+] log mmap2 - addr range  : 0x401000 - 0x402000
mmap2(0x401000, 0x1000, 0x7, 0x32, 4294967295, 0) = 0x401000
[+] mmap2_block is at 0x401000 with a length of 0x1000
[+] Start      End        Perm.  Path
[+] 00003000 - 00004000 - rwx    [GDT] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 00400000 - 00401000 - rwx    [mapped] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 00401000 - 00402000 - rwx    [mapped] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff13371000 - 7ff1337e000 - r-x    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff1337e000 - 7ff13383000 - r--    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff13383000 - 7ff13385000 - rw-    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff13385000 - 7ff13387000 - rwx    [hook_mem] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 19f699ed4000 - 19f69aad5000 - rw-    [mapped]
[+] 2489f63bf000 - 2489f63dd000 - rw-    [mapped]
[+] 7ffffffde000 - 80000000e000 - rwx    [stack]
[+] ffffffffff600000 - ffffffffff601000 - rwx    [vsyscall]
[+] mapping with PROT_EXEC prot 0x401000 - 0x402000
[+] log mmap2 - mmap2(0x402000, 0x1000, 0x7, 0x32, ffffffff, 0)
[+] log mmap2 - mmap2(0x402000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, ffffffff, 0)
[+] log mmap2 - return addr : 0x402000
[+] log mmap2 - addr range  : 0x402000 - 0x403000
mmap2(0x402000, 0x1000, 0x7, 0x32, 4294967295, 0) = 0x402000
[+] mmap2_block is at 0x402000 with a length of 0x1000
[+] Start      End        Perm.  Path
[+] 00003000 - 00004000 - rwx    [GDT] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 00400000 - 00401000 - rwx    [mapped] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 00401000 - 00402000 - rwx    [mapped] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 00402000 - 00403000 - rwx    [mapped] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff13371000 - 7ff1337e000 - r-x    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff1337e000 - 7ff13383000 - r--    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff13383000 - 7ff13385000 - rw-    /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 7ff13385000 - 7ff13387000 - rwx    [hook_mem] (/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/qiling/examples/rootfs/x8664_linux/bin/leetness)
[+] 19f699ed4000 - 19f69aad5000 - rw-    [mapped]
[+] 2489f63bf000 - 2489f63dd000 - rw-    [mapped]
[+] 7ffffffde000 - 80000000e000 - rwx    [stack]
[+] ffffffffff600000 - ffffffffff601000 - rwx    [vsyscall]
[+] mapping with PROT_EXEC prot 0x402000 - 0x403000
[+] Section header offset : 0x20e8
[+] length of the section headers : 0x1c0
[+] Final binary dumped : 0x400000 - 0x4022a8
0x401000 : push rbx
0x401001 : mov edx, 0x19
0x401006 : mov edi, 1
0x40100b : lea rsi, [rip + 0xfee]
0x401012 : sub rsp, 0x20
0x401016 : call 0x4010f0
0x4010f0 : mov eax, 1
0x4010f5 : syscall 
write(1,402000,25) = 0
Could you get the flag?
0x4010f7 : ret 
0x40101b : mov rbx, rsp
0x40101e : mov edx, 0x1f
0x401023 : xor edi, edi
0x401025 : mov rsi, rbx
0x401028 : call 0x4010d0
0x4010d0 : xor eax, eax
0x4010d2 : syscall 
kurisu
read(0, 0x80000000de68, 0x1f) = 7
0x4010d4 : ret 
0x40102d : sub rax, 1
0x401031 : mov byte ptr [rsp + rax], 0
0x401035 : test rax, rax
0x401038 : jle 0x40108d
0x40103a : xor edx, edx
0x40103c : mov ecx, 1
0x401041 : lea rdi, [rip + 0xfd2]
0x401048 : nop dword ptr [rax + rax]
0x401050 : movzx esi, byte ptr [rdi + rdx]
0x401054 : cmp byte ptr [rbx + rdx], sil
0x401058 : sete sil
0x40105c : add rdx, 1
0x401060 : movzx esi, sil
0x401064 : and ecx, esi
0x401066 : cmp rdx, rax
0x401069 : jne 0x401050
0x40106b : cmp ecx, 1
0x40106e : je 0x40108d
0x401070 : mov edx, 0x12
0x401075 : lea rsi, [rip + 0xfc0]
0x40107c : mov edi, 1
0x401081 : call 0x4010f0
write(1,40203c,18) = 0
You lost THE GAME
0x401086 : xor edi, edi
0x401088 : call 0x4010e0
0x4010e0 : mov rax, 0x3c
0x4010e7 : syscall 
exit(0) = 0
$ file bin.u
bin.u: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
$ strings bin.u
ijZ7K
Could you get the flag ?
flag{unp4ck_1s_fun}
Gr33tz d00dz
You lost THE GAME
$ chmod +x bin.u && ./bin.u < <(python -c 'print "flag{unp4ck_1s_fun}"')
Could you get the flag ?
Gr33tz d00dz

Awesome !!!! We've finished to develop our automatic unpacker with qiling ! And we found the password by using strings on the unpacked binary 🙂. The final code looks like this:

The code is available on github gist. And you can get the binary here.

Summary

To summarize, I've talked about a few techniques of packing and we've seen how to fight against them. The reliability of the PoC that I've developped depends on your target. But in all the cases, I think that the better technique stays to hook sys_mmap and sys_mprotect to record all the memory areas mapped / protected by the program in PROT_EXEC. Next the question is what to do with that (looking for an elf header in the memory, dump the page each time an executable page is written ...).

Thanks for reading and I hope that it has been interesting ;) . If you have any questions dm me on twitter or ask on my discord server.

~ cheers, nasm