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 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
Not completely knowing, what I'm doing here but let me explain, why I chose:
-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 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”