UPX packed ELF binaries of the Peer-to-Peer Botnet Family Mozi



Found another rabbit hole! While reading my daily digest of tubes [an article about the Mozi bot](https://blog.netlab.360.com/mozi-another-botnet-using-dht/) sparked my interest. Peer-to-peer (P2P) botnets are always cool and this one has some worm-like capabilities and seems to hide its traffic within bittorrent communications. Naturally I wanted to take a look at the sample. # Intro The [sample from the netlab blog post](https://malshare.com/sample.php?action=detail&hash=9a111588a7db15b796421bd13a949cd4) - [and many more](https://malshare.com/search.php?query=Mozi.m&private=on) - are readily available on MalShare. [Mad reverse engineering skills](https://en.wikipedia.org/wiki/Strings_(Unix)) lead me to believe that this initial sample is packed with UPX. We need to unpack it before we can look at the malware. This post will describe my journy of unpacking the sample mentioned in this blog post without the help of any dynamic analysis. # Context UPX is a publicly available tool. It's purpose is not to obfuscate malware but to reduce the file size of legitimate executables. This is done similarly to an SFX archive: the target file is compressed and a small stub is prepended that takes care of reverting the compression process and then handing over execution to the original executable. Apart from reducing the file size (and hence also often the loading time), this also _obfuscates_ the original executable, which makes UPX feasible for malware. This is especially true since there is not a large additional risk for the malware author of increasing detection: UPX is trivial to detect but as opposed to packers with the primary purpose to obfuscate malware, it is a bad idea for an anti-virus solution to flag executables like this as malicious solely on the basis of UPX-usage. On the other hand, packing with UPX also has not that many advantages for the malware author: it is trivial to unpack with the -d switch of the upx tool. That is, if the upx tool and the unpacker stub behave exactly the same. # Hanc marginis exiguitas non caperet The author of the netlab blogpost mentioned the following: > But instead of using the common upx magic number to defeat unpacking, it used a novel method, to erase the value of p_filesize & p_blocksize to zero, with that change, researcher need to patch the upx source code then unpacking is possible. And indeed, this is correct: upx -d only left me with the disdainful error message
> upx -d 9a111588a7db15b796421bd13a949cd4

                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2018
UPX 3.95w       Markus Oberhumer, Laszlo Molnar & John Reiser   Aug 26th 2018

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
upx: 9a111588a7db15b796421bd13a949cd4: CantUnpackException: p_info corrupted

Unpacked 1 file: 0 ok, 1 error.
Assuming that the sample is able to run on targeted systems, the unpacker stub will probably not run into the same problem. Hence it is plausible, this was intentionally patched by the malware author to exploit differences in behavior between the upx tool and the unpacker stub prepended to the file. # Patch Everything After "quickly" installing Visual Studio 2019, I found a [random and totally trustworthy repository containing a VS2019 project of UPX](https://github.com/james34602/UPX-Visual-Studio). Luckily there were only two appearences of the string p_info corrupted in the source code: one in the function PackLinuxElf32 and one in PackLinuxElf64. So let's patch in some debugging output:
@@ -4707,8 +4715,15 @@ void PackLinuxElf32::unpack(OutputFile *fo)
     unsigned orig_file_size = get_te32(&hbuf.p_filesize);
     blocksize = get_te32(&hbuf.p_blocksize);
     if (file_size > (off_t)orig_file_size || blocksize > orig_file_size
-        || !mem_size_valid(1, blocksize, OVERHEAD))
-        throwCantUnpack("p_info corrupted");
+        || !mem_size_valid(1, blocksize, OVERHEAD)) {
+        char msg[128];
+        snprintf(
+            msg, 128,
+            "p_info corrupted: file_size=%u, blocksize=%u orig_file_size=%u",
+            file_size, blocksize, orig_file_size
+        );
+        throwCantUnpack(msg);
+    }
 
     ibuf.alloc(blocksize + OVERHEAD);
     b_info bhdr; memset(&bhdr, 0, sizeof(bhdr));
This leads to the more descriptive message p_info corrupted: file_size=95268, blocksize=0 orig_file_size=0. So something called blocksize and something called original_file_size is zero. The names suggest that these values _should_ not be zero: block sizes of zero lead to quite boring algorithms and the original file size was probably also not zero, otherwise the malware would be [pretty harmless](http://www.ioccc.org/1994/smr.c). I decided not to understand the UPX decompression algorithm but instead backtrack where these values came from: both are read from a struct of type p_info. I also noted that adjacent to p_info, UPX expects a struct of type l_info. Their definitions are as follows:
struct l_info {
    unsigned char l_checksum[4];
    unsigned char l_magic[4];
    unsigned char l_lsize[2];
    unsigned char l_version;
    unsigned char l_format;
};

struct p_info {
    unsigned int p_progid;
    unsigned int p_filesize; // later copied to original_file_size
    unsigned int p_blocksize; // later copied to blocksize
};
The field l_magic is especially interesting because the code compares its with 0x21585055, which is UPX! in ASCII. This is good news if you want to be as lazy as I am safe some time and [watch the English version of the most successful German math meme Internet personality](https://www.youtube.com/watch?v=8KJtazJMyl0) instead: it means that eight bytes after UPX! there are two times four bytes representing the file size and the block size respectively. So let's fire up a hex editor and patch these damn bytes:
Not completely knowing, what I'm doing here but let me explain, why I chose: FF FF F0 00: It is larger than the original file size - which was 95268 or 24 74 01 00 - and at the same time not too large to look improbable for an unpacked file. # Da Capo Sadly, this just lead to another error message: EOFException: premature end of file. Let's not give up and grep for it in the source code, because there is hope: the error message suggests that we are near the end of the algorithm and that the packed file just was not long enough for the expected (patched) original file size. Maybe we can somehow guess the actual size. The string premature end of file appears only in this function:
void throwEOFException(const char *msg, int e)
{
    if (msg == NULL && e == 0)
        msg = "premature end of file";
    throw EOFException(msg, e);
}
But this function is called in multiple places. Inserting debugging output in all of them lead me to understand, where to look at and finally to the following patch:
@@ -4987,8 +5003,15 @@ void PackLinuxElf32::unpack(OutputFile *fo)
     ph.u_len = total_out;
 
     // all bytes must be written
-    if (total_out != orig_file_size)
-        throwEOFException();
+    if (total_out != orig_file_size) {
+        char msg[128];
+        snprintf(
+            msg, 128,
+            "EOFException total_out=%u, orig_file_size=%u",
+            total_out, orig_file_size
+        );
+        throwEOFException(msg);
+    } 
# The End is Near This results in the error message EOFException: total_out=212464, orig_file_size=15794175. Hence, I expect the actual original file size to be 212464, or f0 3d 03 00. Let's quickly patch it in and voila:
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2018
UPX 3.95w       Markus Oberhumer, Laszlo Molnar & John Reiser   Aug 26th 2018
Built with Visual Studio 2017 compiled by James34602   Build date:Dec 25 2019

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
    212464 <-     95268   44.84%    linux/arm    upx-patched2

Unpacked 1 file.
Interestingly [MalShare observed the exact same (unpacked) sample a few days ago](https://malshare.com/sample.php?action=detail&hash=dd4b6f3216709e193ed9f06c37bcc389) being served by an URL that looks very similar to other URLs, the malware used. Potentially, operators have moved away from using UPX for obfuscation or this was just uploaded without being packed by mistake. # Summary The original file had a SHA256 hash of e15e93db3ce3a8a22adb4b18e0e37b93f39c495e4a97008f9b1a9a42e1fac2b0, it was downloaded by MalShare from http[:]//176.113.161[.]131:44031/i on 2019-12-05 and from a lot of other places in the following days. It is packed with UPX and the resulting binary was probably patched afterwards to hinder easy unpacking: two header fields that seem to be ignored by the unpacker stub, but are necessary for the upx tool to work, have been overwritten with zeros. Restoring the original values results in an unpacked file with SHA256 hash 83441d77abb6cf328e77e372dc17c607fb9c4a261722ae80d83708ae3865053d. Here is a YARA rule to detect this kind of anti-analysis technique:
rule PatchedUpx_01
{
    strings:
        $upx_magic_with_zero_sizes = {
            55 50 58 21
            ?? ?? ?? ?? ?? ?? ?? ??
            00 00 00 00 00 00 00 00
        }
    condition:
        all of them
}

One Reply to “UPX packed ELF binaries of the Peer-to-Peer Botnet Family Mozi”

  1. What are the chances. I pulled this sample from VT this morning and randomly found your blog post after fixing the UPX header size stomp. Good write up BTW! Guess I was 1 day too late lol.

Leave a Reply

Your email address will not be published. Required fields are marked *