Another scriptjob virus caught in the wild, and this one ** IS NOT CAUGHT ** by summer 2020 Autodesk plugin.
Bummer.
Another scriptjob virus caught in the wild, and this one ** IS NOT CAUGHT ** by summer 2020 Autodesk plugin.
Bummer.
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
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.
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)
cmds.optionVar(iv= ("fileExecuteSN", 0))
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)
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.
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.
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!
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.
Cheers.
Liam.
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')
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
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
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
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.