Another Maya malware in the wild

Another scriptjob virus caught in the wild, and this one ** IS NOT CAUGHT ** by summer 2020 Autodesk plugin.

Bummer.

2 Likes

Here is a solution from Chinese community. Hope it can be added to Security Tools soon:

You can use auto translate to get the idea

1 Like

thanks for the heads up, I’ve also added this to our Malware scanner in ProPack just in case.

Just had this come in from an external developer at the studio I work for.

Looks like the same asset a week prior they sent us was clean, so its freshly hitting them.

We notified the external dev’s.

For treatment we have a simple python line scanning files, looking for keywords and flagging them to be cleaned, and then we are manually removing the userSetup, and vaccine.py it creates.

Any other suggestions?

Looks like I’ll be writing a script node scanner of our own that we can manually run on incoming files.

Another way to approach it is to start an offline maya with both the script node execution disabled and MAYA_SKIP_USER_SETUP = 1, then batch through all of your maya files looking for scriptNodes of any description, since changing the signatures of these is trivial. The author of the virus screwed up by leaving import os in plain text but Python gives you an infinity of ways to obfuscate a red flag command like that.

In general I’d suggest banning scriptnodes completely until ADSK get serious about security for them.

Also – you’ll need to look for unknown py and mel files in all of the maya script exec locations.

2 Likes

Oh, lots of questions Theodox!

Is script node execution disabled a standard option in “offline maya” mode?

And what is offline maya? that’s maya prompt -batch mode? Have a autodesk link for documented support?

where is that MAYA_SKIP_USER_SETUP = 1 going? That an env Var?

(going back 15 years of memory since I used maya prompt, and google is just returning piracy questions for using maya offline. I recall one of the modes to be stripped down, and one had to manually run what scripts were needed)

  1. MAYA_SKIP_USER_SETUP=1 will disable usersetup.py
  2. To disable script node execution, you want cmds.optionVar(iv= ("fileExecuteSN", 0))
  3. I should have said “mayapy” – i was thinking you’d make a tool which fired up mayapy (in a process with MAYA_SKIP_USER_SETUP=1), used the optionVar to disable script node execution, and then looped through all the files you care about cataloging and/or deleting their script node contents.

Here’s the shell of a script you could use, though it needs to be beefed up – it just audits for fiiles with any script nodes other than the two standard ones (sceneConfigurationScriptNode and uiConfigurationScriptNode). A clever attacker could hijack those, so this is NOT production quality security – but it could be turned into such with some attention.

# usage :
# mayapy.exe  quick_scan.py   path/to/directory/to/scan
# recursively scanes all maya files in path/to/directory/to/scan 

import sys
import logging
logger = logging.getLogger("mayascan")
logger.setLevel(logging.INFO)

import os
os.environ['MAYA_SKIP_USER_SETUP'] = "1"
logger.info("usersetup disabled")

import maya.standalone
maya.standalone.initialize()
logger.info("maya initialized")

import maya.cmds as cmds
cmds.optionVar(iv= ("fileExecuteSN", 0))
logger.info("scriptnodes disabled")

file_list = []
counter = 0

for root, _, files in os.walk(sys.argv[-1]):
    for mayafile in files:
        lower = mayafile.lower()
        if lower.endswith(".ma") or lower.endswith(".mb"):
            counter += 1
            abspath = os.path.join(root, mayafile)
            logger.info("scanning {}".format(abspath))
            cmds.file(abspath, open=True)
            scriptnodes = cmds.ls(type='script')
            # almost all Maya files will contain two nodes named
            # 'sceneConfigurationScriptNode' and 'uiConfigurationScriptNode'
            # a proper job wouldd make sure that they contained only trivial MEL 
            # but youd have to really inspect the contents to make sure
            # a smart attacker hadn't hidden inside those nodes.  For demo purposes
            # I'm just ignoring them but that is a clear vulnerability

            if len(scriptnodes) > 2:
                # here's where you'd want to nuke and resave the file if you were really cleaning house,
                # or you could loop through them applying your own safety test
                logger.warning("file {} contains {} scriptnodes".format(abspath, len(scriptnodes) - 2 ))
                file_list.append(abspath)


logger.info("scanned {} files".format(counter))
if file_list:
    logger.warning ("=" * 72)
    logger.warning ("filenodes found in:")
    for f in file_list:
        logger.warning(f)


4 Likes

Thanks so much!

Maybe what I’m thinking is overkill but for long term, I was thinking about registering the script nodes with a custom attr stored in an asset db*, or external file, and then scanning for un-registered nodes and flagging files / automatically cleaning out the nodes. Would have to find a way to keep our “tag” of the asset hidden and not go out in any files to external developers.

At least, I have a way to let the producers scan the incoming files for suspicious nodes now.

We do have python distributed environments, and we only work in ascii so I wasn’t limited to mayapy, and just did a fast ascii script search.

For others, simple ascii scanner for maya script nodes, puts all nodes in a dictionary to do with as you please: (os walk file method not included)

# go get script node code blocks
for ma in maya_ascii_files:
    script_nodes = dict()
    script_node_block = None
    f = open(ma, 'r')
    for line in f:
        if line.startswith('createNode script'):
            script_node_block = line.rsplit('"')[-2]
            script_nodes[script_node_block] = [line]
        elif script_node_block is not None and line.startswith('\t'):
            script_nodes[script_node_block].append(line)
        else:
            script_node_block = None
    f.close()

As a note in another train of thought, one machine got a full vaccine.py file, many didn’t, and only got the userSetup. I’m not sure where/when it gets created yet. I didn’t look closely at the opening script node code posted in the original post, but I wonder about not keeping that code public. ¯\(ツ)/¯

I think what I’d do for internal security is to have a “secret” value like you would for a web app:

1 generate a checksum value for the script contents of the node .
2. combine it with the secret value to generate a final checksum
3. Store the result in an attribute
4. When scanning, first check for the presence of your attribute – if it’s not present the node is not one of your in-house ones
5. finally, check the crc against a crc of the current script to guarantee it has not been tampered with.

Here’s an example module that uses a sha512 hash:

import maya.cmds as cmds
import hashlib

# YOUR SECRET NOT BE IN CODE THAT IS
# AVAILABLE TO POTENTIAL ATTACKERS!
SECRET = '1234567890987654321'  


def generate_hash(node, secret):
    """
    Generates a sha512 hash for <node> as a string, keyed with <secret>
    """
    hasher = hashlib.sha512()
    hasher.update(str(cmds.getAttr(node +  ".scriptType")).encode('utf-8'))
    hasher.update(str(cmds.getAttr(node + ".sourceType")).encode('utf-8'))
    before = cmds.getAttr(node +  ".before") or 'empty before'
    hasher.update(before.encode('utf-8'))
    after = cmds.getAttr(node + ".after") or 'empty_after'
    hasher.update(after.encode('utf-8'))
    hasher.update(str(secret).encode('utf-8'))
    return hasher.hexdigest()
    
def update_hash(node, secret):
    """
    Updates scriptNode <node> with a correct hash attribute using <secret>
    """
    
    if not cmds.ls(node + ".hash"):
        cmds.addAttr(node, ln = "hash", dt='string', hidden=True)
    new_hash = generate_hash(node, secret)
    cmds.setAttr(node + ".hash", new_hash, type='string')

def validate_hash(node, secret):
    """
    Ensures that scriptNode <node> has a valid hash for the supplied secret
    """
    if not cmds.ls(node + ".hash"):
         return False
   
    return cmds.getAttr(node + ".hash") == generate_hash(node, secret)

def scan_script_nodes(secret):
    """
    Yields a tuple of <node>, <valid> for all scriptNodes in the scene,
    where "valid" is true if the node has a valid hash attribute.

    This is code you'd want to include in a scanner or in a file-open callback.
    """
    for item in cmds.ls(type='script', recursive=True):
        yield item, validate_hash(item, secret)
        
def update_all_script_nodes(secret):
    """
    Updates all the script nodes in the scene so their hash values represent
    their current contents and the supplied secret.

    You'd probably want to run this on file save, or ask the user on save.
    """
    for item in cmds.ls(type='script', recursive=True):
        update_hash(item, secret)

This is still not bank level security but it’s better than nothing. Whatever you do, let the secret value show up in code an attacker could see – you probably want to install it an an env var or a text file you load at startup. It’s essentially the “password” for scripts you want to allows so it’s important not to allow it to float free. An internal-only file share or an environment variable on internal machines is a good choice since they are harder to leak; so would an internal webserver which only gave out the value to approved IP addresses.

1 Like

Is the same risk not posed by any program that can execute arbitrary code contained within it’s scene files (i.e. any DCC app I can think of has some mechanism for this). Install a security check on maya scriptjobs, sure, but theres a bigger probem which to me feels is most effectively solved by quarantining 3rd-party files that may contain malicious code. However you do it, this seems like a wakeup call make sure you’re thoroughly checking your maya/houdini/3ds-max/etc files before loading them in an app which has full network access.

You’re correct - any program that executes code on load is vulnerable , this goes all the way back to activeX in MS Word docs. I’d always recommend quarantining and then scanning the incoming files.

IMPORTANT

As we’ve been looking at security issues in the Maya Security Project it’s become clear that scriptNodes are NOT the only easy attack vector — while the code above is useful it is also nothing like a guarantee. Please be careful!

TLDR :DO NOT ACCEPT .MA FILES FROM UNTRUSTED SOURCES.

Hello,
For anyone still finding this thread from a google search, and wondering for a quick fix solution, made a script with a quick interface to fix this issue.

tool window screenshot

Cheers.
Liam.

1 Like

Had the same “malware” spread among the many files we had, so had to cobble up the following python code. Contains project specific parts, also can check all .ma files in the current path (and lower) recursively and cleans them (non-destructive, keeps the original file with a timestamp).

Maya Scanner by Autodesk does detect the malware, but is unable to clean it, especially if exists in one of the numerous external references. This one does, since it checks every file individually. Run it from your favorite terminal.

usage: cleaner.py 41

(“41” refers to a non-existing episode number, which tells the script to check current dir and below)

import os
import sys
import re
import glob
import ntpath
import datetime
from datetime import datetime
import time
import argparse

version = "1.81"

print("\n\nRecursive .ma cleaner - Tankut, version " + version + "\n\n")

#signature = r'createNode script -n "vaccine_gene";.*createNode script -n "breed_gene";.*setAttr ".stp" 1;'
signature = r'createNode script -n "vaccine_gene.";.*\]"\);\ncreateNode script -n "breed_gene?";.*setAttr ".stp" 1;'
signature_a = r'createNode script -n "vaccine_gene.{7129}'
signature_b = r'createNode script -n "breed_gene.{405}'
innocent = r'createNode script -n ".*sceneConfigurationScriptNode.*"'
suspicious = r'createNode script -n'
remnant =  r'connectAttr "(breed|vaccine)_gene.msg" .*dn"'


number = 0
checked = 0
suspected = 0
red_flag = 0
folderindex = 0
episodeflag = 0
lastcheck = 0
logexists = 0

currentpath = os.getcwd()

def truncate(n, decimals=0):
    multiplier = 10 ** decimals
    return int(n * multiplier) / multiplier


def quitapp(errorcode):
    if errorcode == 1:
        print ("\nenter an episode number  ( 01 - 40 )   \n")
    if errorcode == 2:
        print ("\nchecking current dir and below for malware \n")
    exit()
    return

def noepisode():
    print("no episode number, checking current dir : ")
    return





episodelist = [".", "e01_oyuncakMuhendisi", "e02_dagDustu", "e03_CokSoguk", "e04_calisirsanOlur", "e05_bulutTamircisi", "e06_bitmeyenKale", "e07_BeniUnutmaNiloya", "e08_aksamOldu", "e09_tospikAraKurtar", "e10_NiloyaHaritasi", "e11_kucukBilimInsanlari", "e12_hayalOyunu", "e13_elimYuzumSobe", "e14_uzaydanSinyal", "e15_yuruyenKutuphane", "e16_temizlikSagliktir", "e17_magaradakiAyi", "e18_goool", "e19_denizciMete", "e20_masaCadiri", "e21_salyangozGibiSakin", "e22_kucukSaglikcilar", "e23_cekirgeOgretmeni", "e24_oyuncakBrosuru", "e25_canimSikiliyor", "e26_koydenHaberler", "e27_sporHerkesIcin", "e28_kurbagaSarkisi", "e29_neseliParkur", "e30_ataTohumlari", "e31_akilliCobanKopegi", "e32_sekerimYok", "e33_haylazSular", "e34_benimGuzelDenizim", "e35_dahaOzel", "e36_disariCikalim", "e37_elifinTeleskobu", "e38_evcilKarinca", "e39_miniklerinParki", "e40_baharTelasi"]

try:
    argument = sys.argv[1]
except:
    noepisode()
    argument =""

try:
    force = sys.argv[2]
except:
    force = ""

if argument != "":
    try:
        folderindex = int(argument)
    except ValueError:
        noepisode()


if folderindex >=0 and folderindex < 41:
    episode = episodelist [folderindex]
    projectpath = "D:\\Dropbox"
    if currentpath.find ("D:\\Dropbox") == -1:
        projectpath = "M:\\PROJECTS"
    currentpath = projectpath + "\\Niloya_2019\\scenes\\s07\\" + episode
    episodeflag = 1
    if episode == ".":
        episode = "ALL"
    print("Checking episode: " + episode + "\n" + currentpath + "\n")



else:
    noepisode()
    print(currentpath + "\n")



try:
    log = open(currentpath + "\\vircheck.ini",'r')
    logexists = 1

except FileNotFoundError:
    print ("no checkpoint data. skipping..")

if logexists == 1 :
    lastcheck = int (log.read())
    print ("last checkpoint: " + str(lastcheck))
    log.close()

if logexists == 1 and force == "f":
    print ("ignoring checkpoint")
    lastcheck = 0


## mainloop

filelist = glob.glob(currentpath + '\**\*.ma', recursive=True)

for filename in filelist:
    if int(os.path.getmtime(filename)) > lastcheck:

        suspectfile = open(filename, "r")
        try: 
            infile = suspectfile.read()
            checked = checked + 1
            print('\r', str(checked).zfill(6), end = ' ')
        except UnicodeDecodeError: 
            print ("Unicode read error, skipping: "+filename)
            infile = ""
        except FileNotFoundError:
            print ("File not found (moved?) : "+ filename)
            infile = ""
        except MemoryError:
            print ("Mem error (size = "+ str(truncate(os.path.getsize(filename)/1024**2,3)) + " MB) : "+ filename)
            infile = ""


        suspectfile.close()

        red_flag = 0

        if re.search (suspicious, infile, flags=re.DOTALL):
            red_flag = red_flag + 1
            if re.search (innocent, infile, flags=re.DOTALL):
                red_flag = red_flag - 1
            if red_flag >> 0:
                print (str(red_flag) + " unknown scriptnode(s) in " + filename)

        

        if re.search ('vaccine_gene', infile, flags=re.DOTALL):
            number = number + 1
            currenttime = datetime.now()
            datestamp = str(currenttime.year).zfill(4)+"-"+str(currenttime.month).zfill(2)+"-"+str(currenttime.day).zfill(2)+"_"+str(currenttime.hour)+str(currenttime.minute)+str(currenttime.second)

            try:
                os.rename (filename, filename+"_old_"+datestamp )
            except FileExistsError:
                os.rename (filename, filename+"_old_"+ datestamp + "_2")

            clean = infile

            item = 0
    
            while item<4:          
                clean = re.sub(signature_a, ' ', clean, flags=re.DOTALL)
                clean = re.sub(signature_b, ' ', clean, flags=re.DOTALL)
                item += 1

            oldsize = sys.getsizeof(infile)
            newsize = sys.getsizeof(clean)

            clean = re.sub(remnant, ' ', clean, flags=re.DOTALL)
            print (filename + " " + f'{oldsize:,}' + " " + f'{newsize:,}')

            outfile = open(filename, "w+")
            outfile.write(clean)
            outfile.close()
        

print ("\nTotal checked: "+ str(checked) + " of " + str(len(filelist)) + " - cleaned : " + str(number) + "\n")

log = open(currentpath + "\\vircheck.ini",'w')
log.write(str(int(time.time())))
log.close()

# requires pip3 install psutil, not standard library
#
#running_from = psutil.Process(os.getpid()).parent().name()
#if running_from == 'explorer.exe':
#    input('Press Enter to Exit..')

if episodeflag == 0:
    input('Press Enter to Exit..\n')


2 Likes

Could you upload a zipped copy of an infected file? It’d be good to ensure that the Manik MA scanner finds it correctly.

Sure, here are a few random infected files from the archive:

https://www.dropbox.com/s/arrhezisfdppy41/infected_vacc_examples.zip?dl=1

1 Like

Thanks, helped me catch a bug in the scanner. Otherwise it correctly identified the infected files, if that’s useful ping me and I can get your the code

1 Like

Glad it was useful. And yes, I’d be interested in your MA scanner.

This link goes to a Maya plugin (tested on Maya 2022, should be OK at least as far back as 2020 or 2019). If you unzip into a folder on the your MAYA_MODULE_PATH (usually Documents /Maya/2022/Modules) it will scan MA files as you open them and warn you if the files are suspicious.

Note that the “Maya security plugin” does NOT catch all of the (many!) zero-day exploits that are trivially easy to do in MA files. It’s only really able to catch scriptnode hacks.

If you catch any holes in this please ping me. If you want to join the Github group that created the plugin, also reach out via DM

3 Likes

Thank you very much.

It would be nice if the plugin could also (without duplication of code) scan existing files on the server, without loading them up in Maya.