Everyone who has delved a bit into malware analysis knows that you don’t actually need much: a PC, a suitably configured VM, and the necessary analysis tools – and, of course, the malware itself. This is a simplified representation, but it captures the essence of the process. This approach is effective because the malware we typically analyze is compiled for the x86/x86_64 architecture.
However, how do we handle the dynamic analysis of malware designed for a different CPU architecture? Especially in the world of IoT, where a variety of processor architectures prevails, this poses a challenge. The most commonly used architectures include those of ARM processors, known for their cost efficiency and energy efficiency – a reason why these processors are frequently used in smartphones, wearables, and embedded devices. Another example is the MIPS architecture, which is found primarily in routers, media players, and old gaming consoles. There are many others, such as PowerPC and SuperH/SH.
A feasible solution is the emulation of the respective CPU architecture. A widely recognized emulation tool is QEMU. It allows us to emulate in full system mode and debug the IoT malware for dynamic analysis. However, this is not our desired approach. Another option is to work with the Qiling framework. Qiling, built on Unicorn, stands out by providing comprehensive cross-platform emulation. In this case, we choose user mode for several reasons. First, we want an environment that is easy and fast to reuse, and second – most importantly – this allows us to instrument the malware most effectively. It enables the execution of Linux ELF executables on any system where Python runs and ensures a comprehensive emulation of syscalls, thereby effectively isolating the emulated binary from the host system. Qiling is distinguished by its emulation of IOCTL system controls and offers dynamic linkers, an I/O handler, and support for developing your own tools in Python. Additionally, it has features for dynamic hot patching and hooking, as well as snapshot functionalities. However, it’s important to note that not every system call in Qiling is an exact replica of the real kernel system calls.
As a corresponding sample, we chose a destructive IoT malware compiled for the MIPS architecture. It is the Wiper malware AcidRain, which overwrites and deletes files and folders as well as storage devices attached to the system.
To additionally protect the file system and especially to restore the manipulated file system after a successful emulation, we integrate an additional layer using Docker. The entire construct is intended to look as follows:
While Docker provides file system isolation, it does not offer full system virtualization due to its reliance on the host system’s kernel. Therefore, when we create devices within a Dockerfile, Docker itself applies cgroup rules, which allow the creation of a few standard devices. Thus, when we aim to create a device with mknod
, we can reference it to the /dev/null
device using the magic numbers 1, 3, as defined here: https://www.kernel.org/doc/Documentation/admin-guide/devices.txt.
With the parameter --device-cgroup-rule
, we gain the ability to create other device types. Every device type is a reference to, or linked with, the host system devices. However, this approach does not align with our objective, neither intentionally nor accidentally, especially in the context of AcidRain, which aims to wipe various device types. This is why we create a device referenced to the /dev/null
device with the command mknod /home/sda c 1 3
, allowing AcidRain to overwrite everything on the discovered devices without any effect.
This solution might sound pretty cool and easy, but it’s tricky in cases where the malware attempts to determine the size of the device and acts based on this information. This issue occurs in the emulation, causing AcidRain to enter a never-ending overwrite routine. To resolve this, we need to implement a hook to stop the overwrite function. We’ll explain this process in part 2 of this blog. Until then, we’ll comment this out in our Dockerfile.
All required files, the Python script in which Qiling is started, AcidRain, and a root directory named rootfs, which Qiling expects for emulating a file system, we add through the Dockerfile to our Docker container.
FROM python:3.10RUN apt-get update && apt-get install -y cmake patch\
&& pip install qiling==1.4.6 pefile==2023.2.7
RUN mkdir /home/qiling/ && mkdir /home/logs/
COPY qiling/ /home/qiling/
COPY rootfs /home/rootfs
COPY qiling/run_qiliot.sh /home/qiling/
RUN chmod +x /home/qiling/run_qiliot.sh
RUN mknod /home/sda c 1 3 && \
mknod /home/mtd0 c 1 3 && \
chmod 666 /home/sda && \
chmod 666 /home/mtd0
Note: If you don’t have a root file system for the emulation, Qiling offers different rootfs folders: Qilling rootfs collection .
It’s also worth mentioning that Qiling offers the option not just to attach devices statically to the rootfs system but also dynamically, as described in the documentation: https://docs.qiling.io/en/latest/hijack/#hijacking-vfs-objects. AcidRain generates thousands of device names. To trigger AcidRain’s functions, we simply mock the SDA Device and the MTD0 Device and dynamically map the mock devices into the emulation file system:
# Dynamically add files to the file system
ql.add_fs_mapper("/dev/null", "/dev/null")
ql.add_fs_mapper("/dev/sda", "/home/sda")
ql.add_fs_mapper("/dev/mtd0", "/home/mtd0")
AcidRain also attempts to open the /dev/null
file and overwrite the three standard file descriptors with the the new file descriptor. To achieve this, we dynamically mapped the /dev/null
file from the docker container to the emulation.
Docker Compose can also be used to mount volumes for logs or code coverage and extract results easily.
version: '3'
services:
qiling:
build:
context: .
dockerfile: Dockerfile
volumes:
- ./logs:/home/logs/
working_dir: /home
command: ./qiling/run_qiliot.sh
For the initial emulation of the Wiper IoT malware, only minimal input is required. In the Python script, Qiling can be initialized with two essential things: the path to the IoT malware sample and the path of the rootfs folder. Optionally, as in the example, a log file can also be specified.
#!/usr/local/bin/python
from qiling import Qilingif __name__ == "__main__":
ql = Qiling(["./acidrain"],rootfs="../rootfs",log_file="emulation.log")
# Dynamically add files to the file system
ql.add_fs_mapper("/dev/null", "/dev/null")
ql.add_fs_mapper("/dev/sda", "/home/sda")
ql.add_fs_mapper("/dev/mtd0", "/home/mtd0")
ql.run()
With the initial execution, we can emulate AcidRain from start to finish, as evidenced by the characteristic “Look out!” print in the terminal. Additionally, AcidRain opens the /dev/sda
device and begins overwriting the file.
[=] write(fd = 0x1, buf = Look out!\n, buf = 0x404684, count = 0xa) = 0xa
[=] fork() = 0xf
[=] fork() = 0x0
[=] Simultates exit and do nothing.
[=] exit(code = 0x0) = ?
[=] Simultates setsid and do nothing.
[=] setsid() = 0xf
[=] open(filename = /dev/null, filename = 0x404690, flags = 0x1, mode = 0x0) = 0x3
[=] dup2(oldfd = 0x3, newfd = 0x0) = 0x0
[=] dup2(oldfd = 0x3, newfd = 0x1) = 0x1
[=] dup2(oldfd = 0x3, newfd = 0x2) = 0x2
[=] close(fd = 0x3) = 0x0
[=] brk(inp = 0x0) = 0x447000
[=] brk(inp = 0x488000) = 0x488000
[=] getuid() = 0x0
[=] open(filename = /dev/sda, filename = 0x7ff3cd98, flags = 0x1, mode = 0x0) = 0x3
[=] ioctl(fd = 0x3, request = BLKGETSIZE64, pointer = 0x7ff3cd58) = 0x0
[=] lseek(fd = 0x3, offset = 0x0, origin = 0x0) = 0x0
[=] write(fd = 0x3, buf = bytearray(b'\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff') ... [more data], buf = 0x447004, count = 0x40000) = 0x40000
[=] write(fd = 0x3, buf = bytearray(b'\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff') ... [more data], buf = 0x447004, count = 0x40000) = 0x40000
[ ... ]
Even if the initial emulation runs through without obvious hard crashes, it’s important to note that not all functions are triggered. Appropriate syscall hooks must be implemented for the sample. It can also happen that the execution of certain syscalls leads to crashes, which then have to be fixed within the framework. We will cover that in the next part of this blog series.
AcidRain daemonizes itself by forking and using the setsid
syscall, which complicates the emulation in a container. This is because with the termination of the parent process, the Docker container and thus the emulation also ends. Tools like tini
cannot cover this special case. Therefore, to start our Python script, a simple bash script is used. The script keeps the Docker container running until no emulation process is running, which in this case must be less than 2 processes (our bash script plus at least one python process). All files can be found in our GitHub organization under the project called Qiliot.
Thus, we have established a secure analysis environment for destructive IoT malware on our system. In the next post, we’ll explain what we had to do to trigger all functionalities of AcidRain. Stay tuned.