Quick Glance At Lazarus Group MATA APT On Linux (PART2) Using Ghidra
We begin start off by analyzing main()
in the decompiler view where we see the LIBC function int daemon(int nochdir, int noclose)
.
This function carries out daemonization of a process as its name suggests. A Ghidra Function ID signature was not matched for this particular function
but it was fairly simple to recognize the routine as daemonization because for POSIX its performed as a series syscalls. If you are familiar with the programmatic steps to daemonize in a POSIX environment it’s easy to recognize. The EAX
register is used to index the syscall lookup table, from there you can google the syscall number to see what syscall it correlates to (check reference section for lookup table).
I’ll go over quickly on my process for identifying a function like this as LIBC is loaded with functions that wrap system calls. Below is the decompiler view in Ghidra of LIBC daemon()
already filled with function signatures and type information from prior analysis, but ofcourse this was not the case on my initial sweep of the function.
We take a closer look at fork()
in the disassembly view where we see the syscall
instruction which allows applications to access kernel level functionality from userspace on x86_64 Linux (analagous to int 0x80
on x86 Linux). As mentioned before, the EAX
register will hold a numerical value that correlates to the desired syscall.
The hex value 0x38
is equivalent to decimal 56
. In the snippet of the lookup table we see that this is syscall clone()
.
Now this might seem a bit confusing but the clone()
syscall is very similar to fork()
. The difference between the two being that clone()
in short allows some sharing of the execution context of the child process with the parent. What is happening here is that the fork()
we see in daemon()
is a LIBC function wrapper that calls clone()
syscall. From inside daemon()
no arguments are supplied prior to the call to fork()
which is consistent with its function prototype. We can further verify this behavior with a quick look at the LIBC source code for the fork()
implementation
The ARCH_FORK()
macro above is defined as an inline syscall to clone()
source code link here
Now that we’ve resolved that slight nuance to our methodology we can take a look at our other finds for fingerprinting the other functions.
Below we have the exit()
function. Following previous methods 0x3c
is equivalent to decimal 60
, we see that ESI
is loaded with 0x3c
then further down the listing just before the syscall
instruction we have MOV EAX, ESI
.
The same methods were done to fingerprint __setsid()
, __dup2()
, __fxstat64()
and others. From here we have enough information to feel confident that
this is infact LIBC implementation of daemon and we can take time to supply type information for functions with multiple references as we might see them again during further analysis.
Now back to main()
we examine the way the sample handles command line arguments after daemonization. The malware looks for a command line flag /pro
if any arguments are supplied. If supplied (with /pro
flag) multiple instances of this malware will run when deployed in its intended environment, we will take a closer look at that functionality when we examine FUN_0040e4ea
labeled as IS_RUNNING
in the snapshot below.
Taking a close look at IS_RUNNING
we see its part of daemonization. That is it checks for the presents of a pidfile stored in /var/run/init.pid
(attempting to hide as the pidfile for /var/run/inetd.pid
a legitimate network service daemon on some unix systems) with the call to fopen64()
in reading mode. The return value is checked for NULL
and in such a case the statement in the if-block would call FUN_0040e413
aka write_to_pid_file
. The variable named running_status
is set to 0
, its returned to the calling function – main()
. A return value of 0
signifies that this is the first instance of the malware running and 1
there is another instance.
The code in the else-block executes when a /var/run/init.pid
is present on the system. However the malware takes care to validate that the file contains a PID that belongs to an instance of it. First by checking to make sure the file isn’t empty by taking a look at __fread()
return value andthe PID is non-zero. Should any of those checks fail, then the function calls write_to_pid_file()
and sets return_status
to 0
.
Then it reads the command line arguments associated with the PID via /proc/<PID>/cmdline
. If the path to the binary on disk associated with the PID is not equal to /flash/bin/mountd
(this is retrieved from /proc/<PID>/cmdline
) then its again calls write_to_pid_file()
and sets return_status
appropriately. Failure to read /proc/<PID>/cmdline
due to an invalid PID also results in the same behavior.
References:
MATA sample hosted on VX-UNDERGROUND (Caution donot run outside of VM)