# xxxswf.py was created by alexander dot hanel at gmail dot com # version 0.1 # Date - 12-07-2011 # To do list # - Tag Parser # - ActionScript Decompiler # 2016-11-01 PL: - A few changes for Python 2+3 compatibility import fnmatch import hashlib import math import os import re import struct import sys import time from io import BytesIO from optparse import OptionParser import zlib def checkMD5(md5): # checks if MD5 has been seen in MD5 Dictionary # MD5Dict contains the MD5 and the CVE # For { 'MD5':'CVE', 'MD5-1':'CVE-1', 'MD5-2':'CVE-2'} MD5Dict = {'c46299a5015c6d31ad5766cb49e4ab4b':'CVE-XXXX-XXXX'} if MD5Dict.get(md5): print('\t[BAD] MD5 Match on', MD5Dict.get(md5)) return def bad(f): for idx, x in enumerate(findSWF(f)): tmp = verifySWF(f,x) if tmp != None: yaraScan(tmp) checkMD5(hashBuff(tmp)) return def yaraScan(d): # d = buffer of the read file # Scans SWF using Yara # test if yara module is installed # if not Yara can be downloaded from http://code.google.com/p/yara-project/ try: import yara except ImportError: print('\t[ERROR] Yara module not installed - aborting scan') return # test for yara compile errors try: r = yara.compile(r'rules.yar') except: pass print('\t[ERROR] Yara compile error - aborting scan') return # get matches m = r.match(data=d) # print matches for X in m: print('\t[BAD] Yara Signature Hit: %s' % X) return def findSWF(d): # d = buffer of the read file # Search for SWF Header Sigs in files return [tmp.start() for tmp in re.finditer(b'CWS|FWS', d.read())] def hashBuff(d): # d = buffer of the read file # This function hashes the buffer # source: http://stackoverflow.com/q/5853830 if type(d) is str: d = BytesIO(d) md5 = hashlib.md5() while True: data = d.read(128) if not data: break md5.update(data) return md5.hexdigest() def verifySWF(f,addr): # Start of SWF f.seek(addr) # Read Header header = f.read(3) # Read Version ver = struct.unpack('<b', f.read(1))[0] # Read SWF Size size = struct.unpack('<i', f.read(4))[0] # Start of SWF f.seek(addr) try: # Read SWF into buffer. If compressed read uncompressed size. t = f.read(size) except: pass # Error check for invalid SWF print(' - [ERROR] Invalid SWF Size') return None if type(t) is str: f = BytesIO(t) # Error check for version above 20 if ver > 20: print(' - [ERROR] Invalid SWF Version') return None if b'CWS' in header: try: f.read(3) tmp = b'FWS' + f.read(5) + zlib.decompress(f.read()) print(' - CWS Header') return tmp except: pass print('- [ERROR]: Zlib decompression error. Invalid CWS SWF') return None elif b'FWS' in header: try: tmp = f.read(size) print(' - FWS Header') return tmp except: pass print(' - [ERROR] Invalid SWF Size') return None else: print(' - [Error] Logic Error Blame Programmer') return None def headerInfo(f): # f is the already opended file handle # Yes, the format is is a rip off SWFDump. Can you blame me? Their tool is awesome. # SWFDump FORMAT # [HEADER] File version: 8 # [HEADER] File is zlib compressed. Ratio: 52% # [HEADER] File size: 37536 # [HEADER] Frame rate: 18.000000 # [HEADER] Frame count: 323 # [HEADER] Movie width: 217.00 # [HEADER] Movie height: 85.00 if type(f) is str: f = BytesIO(f) sig = f.read(3) print('\t[HEADER] File header: %s' % sig) if b'C' in sig: print('\t[HEADER] File is zlib compressed.') version = struct.unpack('<b', f.read(1))[0] print('\t[HEADER] File version: %d' % version) size = struct.unpack('<i', f.read(4))[0] print('\t[HEADER] File size: %d' % size) # deflate compressed SWF if b'C' in sig: f = verifySWF(f,0) if type(f) is str: f = BytesIO(f) f.seek(0, 0) x = f.read(8) ta = f.tell() tmp = struct.unpack('<b', f.read(1))[0] nbit = tmp >> 3 print('\t[HEADER] Rect Nbit: %d' % nbit) # Curretely the nbit is static at 15. This could be modified in the # future. If larger than 9 this will break the struct unpack. Will have # to revist must be a more effective way to deal with bits. Tried to keep # the algo but damn this is ugly... f.seek(ta) rect = struct.unpack('>Q', f.read(int(math.ceil((nbit*4)/8.0))))[0] tmp = struct.unpack('<b', f.read(1))[0] tmp = bin(tmp>>7)[2:].zfill(1) # bin requires Python 2.6 or higher # skips string '0b' and the nbit rect = bin(rect)[7:] xmin = int(rect[0:nbit-1],2) print('\t[HEADER] Rect Xmin: %d' % xmin) xmax = int(rect[nbit:(nbit*2)-1],2) print('\t[HEADER] Rect Xmax: %d' % xmax) ymin = int(rect[nbit*2:(nbit*3)-1],2) print('\t[HEADER] Rect Ymin: %d' % ymin) # one bit needs to be added, my math might be off here ymax = int(rect[nbit*3:(nbit*4)-1] + str(tmp) ,2) print('\t[HEADER] Rect Ymax: %d' % ymax) framerate = struct.unpack('<H', f.read(2))[0] print('\t[HEADER] Frame Rate: %d' % framerate) framecount = struct.unpack('<H', f.read(2))[0] print('\t[HEADER] Frame Count: %d' % framecount) def walk4SWF(path): # returns a list of [folder-path, [addr1,addrw2]] # Don't ask, will come back to this code. p = ['',[]] r = p*0 if os.path.isdir(path) != True and path != '': print('\t[ERROR] walk4SWF path must be a dir.') return for root, dirs, files in os.walk(path): for name in files: try: x = open(os.path.join(root, name), 'rb') except: pass break y = findSWF(x) if len(y) != 0: # Path of file SWF p[0] = os.path.join(root, name) # contains list of the file offset of SWF header p[1] = y r.insert(len(r),p) p = ['',[]] y = '' x.close() return r def tagsInfo(f): return def fileExist(n, ext): # Checks the working dir to see if the file is # already in the dir. If exists the file will # be named name.count.ext (n.c.ext). No more than # 50 matching MD5s will be written to the dir. if os.path.exists( n + '.' + ext): c = 2 while os.path.exists(n + '.' + str(c) + '.' + ext): c = c + 1 if c == 50: print('\t[ERROR] Skipped 50 Matching MD5 SWFs') break n = n + '.' + str(c) return n + '.' + ext def CWSize(f): # The file size in the header is of the uncompressed SWF. # To estimate the size of the compressed data, we can grab # the length, read that amount, deflate the data, then # compress the data again, and then call len(). This will # give us the length of the compressed SWF. return def compressSWF(f): if type(f) is str: f = BytesIO(f) try: f.read(3) tmp = b'CWS' + f.read(5) + zlib.compress(f.read()) return tmp except: pass print('\t[ERROR] SWF Zlib Compression Failed') return None def disneyland(f,filename, options): # because this is where the magic happens # but seriously I did the recursion part last.. retfindSWF = findSWF(f) f.seek(0) print('\n[SUMMARY] %d SWF(s) in MD5:%s:%s' % ( len(retfindSWF),hashBuff(f), filename )) # for each SWF in file for idx, x in enumerate(retfindSWF): print('\t[ADDR] SWF %d at %s' % (idx+1, hex(x))) f.seek(x) h = f.read(1) f.seek(x) swf = verifySWF(f,x) if swf == None: continue if options.extract != None: name = fileExist(hashBuff(swf), 'swf') print('\t\t[FILE] Carved SWF MD5: %s' % name) try: o = open(name, 'wb+') except IOError as e: print('\t[ERROR] Could Not Create %s ' % e) continue o.write(swf) o.close() if options.yara != None: yaraScan(swf) if options.md5scan != None: checkMD5(hashBuff(swf)) if options.decompress != None: name = fileExist(hashBuff(swf), 'swf') print('\t\t[FILE] Carved SWF MD5: %s' % name) try: o = open(name, 'wb+') except IOError as e: print('\t[ERROR] Could Not Create %s ' % e) continue o.write(swf) o.close() if options.header != None: headerInfo(swf) if options.compress != None: swf = compressSWF(swf) if swf == None: continue name = fileExist(hashBuff(swf), 'swf') print('\t\t[FILE] Compressed SWF MD5: %s' % name) try: o = open(name, 'wb+') except IOError as e: print('\t[ERROR] Could Not Create %s ' % e) continue o.write(swf) o.close() def main(): # Scenarios: # Scan file for SWF(s) # Scan file for SWF(s) and extract them # Scan file for SWF(s) and scan them with Yara # Scan file for SWF(s), extract them and scan with Yara # Scan directory recursively for files that contain SWF(s) # Scan directory recursively for files that contain SWF(s) and extract them parser = OptionParser() usage = 'usage: %prog [options] <file.bad>' parser = OptionParser(usage=usage) parser.add_option('-x', '--extract', action='store_true', dest='extract', help='Extracts the embedded SWF(s), names it MD5HASH.swf & saves it in the working dir. No addition args needed') parser.add_option('-y', '--yara', action='store_true', dest='yara', help='Scans the SWF(s) with yara. If the SWF(s) is compressed it will be deflated. No addition args needed') parser.add_option('-s', '--md5scan', action='store_true', dest='md5scan', help='Scans the SWF(s) for MD5 signatures. Please see func checkMD5 to define hashes. No addition args needed') parser.add_option('-H', '--header', action='store_true', dest='header', help='Displays the SWFs file header. No addition args needed') parser.add_option('-d', '--decompress', action='store_true', dest='decompress', help='Deflates compressed SWFS(s)') parser.add_option('-r', '--recdir', dest='PATH', type='string', help='Will recursively scan a directory for files that contain SWFs. Must provide path in quotes') parser.add_option('-c', '--compress', action='store_true', dest='compress', help='Compresses the SWF using Zlib') (options, args) = parser.parse_args() # Print help if no argurments are passed if len(sys.argv) < 2: parser.print_help() return # Note files can't start with '-' if '-' in sys.argv[len(sys.argv)-1][0] and options.PATH == None: parser.print_help() return # Recusive Search if options.PATH != None: paths = walk4SWF(options.PATH) for y in paths: #if sys.argv[0] not in y[0]: try: t = open(y[0], 'rb+') disneyland(t, y[0],options) except IOError: pass return # try to open file try: f = open(sys.argv[len(sys.argv)-1],'rb+') filename = sys.argv[len(sys.argv)-1] except Exception: print('[ERROR] File can not be opended/accessed') return disneyland(f,filename,options) f.close() return if __name__ == '__main__': main()
Memory