├── LICENSE ├── setup.py ├── README.md └── punbup.py /LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='punbup', 5 | version='0.0.1', 6 | url='https://github.com/herrcore/punbup', 7 | author='@herrcore, setup @seanmw', 8 | description='Python unbup script for McAfee .bup files - with some additional fun features. \ 9 | Simple usage will extract all files from a .bup to a directory with the same name as the bup file.', 10 | install_requires=["olefile"], 11 | py_modules=['punbup'], 12 | entry_points={'console_scripts': ['punbup=punbup:main']} 13 | ) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [punbup](https://github.com/herrcore/punbup) 2 | 3 | Python unbup script for McAfee .bup files - with some additional fun features. Simple usage will extract all files from a .bup to a directory with the same name as the bup file. 4 | 5 | What makes this script unique is that it is fully implemented in python it's not just another wrapper around 7zip! This means that you are free to run this with any non-python dependencies. Download, install the dependencies from your favourite python package manager and get un-bupping! 6 | 7 | 8 | ## Dependencies 9 | 10 | Before you can use the script you will need to install the [olefile](https://github.com/decalage2/olefile). You can use a package manager such as easy_install or pip (ie. pip install olefile). Or, you can just download and install the library directly from the [project site](https://github.com/decalage2/olefile). 11 | 12 | ## Quick Start 13 | 14 | * Extract all files from 7dea15dd393591.bup to folder 7dea15dd393591/ 15 | ``` 16 | ./punbup.py 7dea15dd393591.bup 17 | ``` 18 | 19 | * Extract all files from 7dea15dd393591.bup to folder 7dea15dd393591/ and rename files to their original names (their file names as noted when they were quarantined). 20 | 21 | ``` 22 | ./punbup.py -o 7dea15dd393591.bup 23 | ``` 24 | 25 | * Print the contents of the Details file to stdout. Don't extract any files (disk won't be written to). 26 | 27 | ``` 28 | ./punbup.py -d 7dea15dd393591.bup 29 | ``` 30 | 31 | ## Usage 32 | 33 | ``` 34 | usage: punbup.py [-h] [-d] [-o] infile 35 | 36 | This script can be used to extract quarantined files from a McAfee .bup file. 37 | If run with no additional options the script will extract all files from the 38 | .bup and place them in a folder with the same name as the supplied .bup file. 39 | 40 | positional arguments: 41 | infile The file that you wish to un-bup. 42 | 43 | optional arguments: 44 | -h, --help show this help message and exit 45 | -d, --details Only print the contents of the Details file. Don't extract 46 | any files. 47 | -o, --original Rename all quarantine files to their original names as noted 48 | in the Details file. Some assumptions have been made for 49 | this to feature to work. Use at your own risk. 50 | -c {md5,sha1,sha256}, --hash {md5,sha1,sha256} 51 | Calculates the hash for all of the files in the bup. 52 | ``` 53 | 54 | ## Features 55 | 56 | ### Fully Implemented 57 | In addition to extracting files from a .bup file the script has the option to rename the files to their original name (instead of File_0, File_1, etc). 58 | 59 | The script also provides an option to just print the Details file and not extract any files. This allows an analyst to quickly investigate a bup file without having to extract anything to disk (very helpful in some environments). 60 | 61 | The script should be fully platform independent. It has been tested and confirmed on some versions of Linux, Windows, OSX. 62 | 63 | ### Future 64 | If you take a look at the script you will see that there is a Details file parser that can be used to extract the .bup Details file into a dictionary. This dictionary is used to implement some features in the script but it has real potential to be extended. Stay tuned! 65 | 66 | ## History 67 | Just to set the record straight .bup files have nothing to do with the "7-zip" file format (LZMA). It is a mystery why there are tons of "unbup" scripts that all just wrap 7zip. The .bup file is actually a Compound File Binary File Format file. There is no need to bring 7zip into the picture, CFB/OLE files are well understood and can be parsed by the OleFileIO_PL library. So, hopefully, after many years of 7zip dependency pain we finally have a dependency-less unbup script. 68 | 69 | ## Support 70 | For questions, suggestions, collaborations, or if you just want to complain you can hit me up on twitter [@herrcore](https://twitter.com/herrcore). 71 | -------------------------------------------------------------------------------- /punbup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import os 4 | import errno 5 | import argparse 6 | import re 7 | import hashlib 8 | 9 | try: 10 | import olefile 11 | except Exception as e: 12 | print >>sys.stderr, 'Error - Please ensure you install the olefile library before running this script (https://github.com/decalage2/olefile): %s' % e 13 | sys.exit(1) 14 | 15 | def extract(infile, dirname=None): 16 | if dirname is None: 17 | dirname = os.getcwd() 18 | try: 19 | if olefile.isOleFile(infile) is not True: 20 | print >>sys.stderr, 'Error - %s is not a valid OLE file.' % infile 21 | sys.exit(1) 22 | 23 | ole = olefile.OleFileIO(infile) 24 | filelist = ole.listdir() 25 | for fname in filelist: 26 | if not ole.get_size(fname[0]): 27 | print 'Warning: The "%s" stream reports a size of 0. Possibly a corrupt bup.' % fname[0] 28 | data = ole.openstream(fname[0]).read() 29 | fp = open(os.path.join(dirname, fname[0]),'wb') 30 | fp.write(data) 31 | fp.close() 32 | ole.close() 33 | return filelist 34 | except Exception as e: 35 | print >>sys.stderr, 'Error - %s' % e 36 | sys.exit(1) 37 | 38 | def decryptStream(data): 39 | ptext='' 40 | for b in data: 41 | ptext+= chr(ord(b) ^ ord('\x6A')) 42 | return ptext 43 | 44 | def decryptFile(fname): 45 | try: 46 | bfile = open(fname, "rb").read() 47 | ptext=decryptStream(bfile) 48 | fp = open(fname, "wb") 49 | fp.write(ptext) 50 | fp.close() 51 | except Exception as e: 52 | print>>sys.stderr, "Error - %s" % e 53 | sys.exit(1) 54 | 55 | def extractAll(bupname, original=False): 56 | dirname = bupname.split('.bup')[0] 57 | #create directory to store extracted files 58 | try: 59 | os.makedirs(dirname) 60 | except OSError as e: 61 | if e.errno != errno.EEXIST: 62 | raise 63 | 64 | filelist = extract(bupname, dirname) 65 | for fname in filelist: 66 | decryptFile(os.path.join(dirname, fname[0])) 67 | 68 | if original: 69 | #rename all extracted file to their original names as noted in Details file 70 | data = getDetails(bupname) 71 | details = parseDetails(data) 72 | for fname in filelist: 73 | if fname[0] == 'Details': 74 | continue 75 | try: 76 | newName = details.get(fname[0]).get("OriginalName").split('\\')[-1].split('/')[-1] 77 | if newName: 78 | os.rename(os.path.join(dirname, fname[0]), os.path.join(dirname, newName)) 79 | else: 80 | print>>sys.stderr, "Error - Could not rename %s: original name not found in Details file." % fname 81 | except Exception as e: 82 | print>>sys.stderr, "Error - Could not rename %s: %s" % (fname, e) 83 | 84 | print>>sys.stdout, "Successfully extracted all files to %s." % dirname 85 | 86 | def getDetails(bupname): 87 | try: 88 | if olefile.isOleFile(bupname) is not True: 89 | print >>sys.stderr, 'Error - %s is not a valid OLE file.' % bupname 90 | sys.exit(1) 91 | 92 | ole = olefile.OleFileIO(bupname) 93 | #clean this up later by catching exception 94 | data = ole.openstream("Details").read() 95 | ptext=decryptStream(data) 96 | ole.close() 97 | return ptext 98 | except Exception as e: 99 | print >>sys.stderr, 'Error - %s' % e 100 | sys.exit(1) 101 | 102 | def parseDetails(data): 103 | #stack based parser for McAfee .bup Details file 104 | #this will fail spectacularly if the file contains keywords that are not accounted for in the keyword list 105 | #use at your own risk 106 | keywords=[re.compile("\[Details\]"), re.compile("\[File_[0-9]+\]"), re.compile("\[Value_[0-9]+\]")] 107 | arrData = data.split("\r\n") 108 | details = {} 109 | tmp={} 110 | for line in reversed(arrData): 111 | #if the line is blank skip it 112 | if line == '': 113 | continue 114 | #if the line is a keyword add its dictionary to the details dictionary and reset the tmp dictionary 115 | keyfound=False 116 | for parse in keywords: 117 | if parse.match(line): 118 | details[line.replace('[','').replace(']','')] = tmp 119 | tmp={} 120 | keyfound=True 121 | continue 122 | #if the line is not a keyword assume it is a key value pair and add it to the tmp dictionary 123 | if not keyfound: 124 | tmp[line.split('=', 1)[0]] = line.split('=', 1)[1] 125 | return details 126 | 127 | def getHashes(bupname,htype): 128 | # 129 | #Return a dictionary of stream name and hash. 130 | # 131 | try: 132 | if olefile.isOleFile(bupname) is not True: 133 | print >>sys.stderr, 'Error - %s is not a valid OLE file.' % bupname 134 | sys.exit(1) 135 | 136 | ole = olefile.OleFileIO(bupname) 137 | hashes = {} 138 | for entry in ole.listdir(): 139 | if entry[0] != "Details": 140 | fdata = ole.openstream(entry[0]).read() 141 | ptext = decryptStream(fdata) 142 | if htype == 'md5': 143 | m = hashlib.md5() 144 | elif htype == 'sha1': 145 | m = hashlib.sha1() 146 | elif htype == 'sha256': 147 | m = hashlib.sha256() 148 | m.update(ptext) 149 | hashes[entry[0]] = m.hexdigest() 150 | ole.close() 151 | return hashes 152 | except Exception as e: 153 | print >>sys.stderr, 'Error - %s' % e 154 | sys.exit(1) 155 | 156 | dumplinelength = 16 157 | 158 | # CIC: Call If Callable 159 | def CIC(expression): 160 | if callable(expression): 161 | return expression() 162 | else: 163 | return expression 164 | 165 | # IFF: IF Function 166 | def IFF(expression, valueTrue, valueFalse): 167 | if expression: 168 | return CIC(valueTrue) 169 | else: 170 | return CIC(valueFalse) 171 | 172 | class cDumpStream(): 173 | def __init__(self): 174 | self.text = '' 175 | 176 | def Addline(self, line): 177 | if line != '': 178 | self.text += line + '\n' 179 | 180 | def Content(self): 181 | return self.text 182 | 183 | def HexDump(data): 184 | oDumpStream = cDumpStream() 185 | hexDump = '' 186 | for i, b in enumerate(data): 187 | if i % dumplinelength == 0 and hexDump != '': 188 | oDumpStream.Addline(hexDump) 189 | hexDump = '' 190 | hexDump += IFF(hexDump == '', '', ' ') + '%02X' % ord(b) 191 | oDumpStream.Addline(hexDump) 192 | return oDumpStream.Content() 193 | 194 | def CombineHexAscii(hexDump, asciiDump): 195 | if hexDump == '': 196 | return '' 197 | return hexDump + ' ' + (' ' * (3 * (16 - len(asciiDump)))) + asciiDump 198 | 199 | def HexAsciiDump(data): 200 | oDumpStream = cDumpStream() 201 | hexDump = '' 202 | asciiDump = '' 203 | for i, b in enumerate(data): 204 | if i % dumplinelength == 0: 205 | if hexDump != '': 206 | oDumpStream.Addline(CombineHexAscii(hexDump, asciiDump)) 207 | hexDump = '%08X:' % i 208 | asciiDump = '' 209 | hexDump+= ' %02X' % ord(b) 210 | asciiDump += IFF(ord(b) >= 32 and ord(b), b, '.') 211 | oDumpStream.Addline(CombineHexAscii(hexDump, asciiDump)) 212 | return oDumpStream.Content() 213 | 214 | def IdentityFunction(data): 215 | return data 216 | 217 | #Fix for http://bugs.python.org/issue11395 218 | def StdoutWriteChunked(data): 219 | while data != '': 220 | sys.stdout.write(data[0:10000]) 221 | sys.stdout.flush() 222 | data = data[10000:] 223 | 224 | def printDump(bupname, DumpFunction=IdentityFunction, allfiles=False): 225 | # 226 | #Print Hex dump/Hex-ASCII dump of first or all streams 227 | # 228 | if sys.platform == 'win32' and DumpFunction == IdentityFunction: 229 | import msvcrt 230 | msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) 231 | try: 232 | if olefile.isOleFile(bupname) is not True: 233 | print >>sys.stderr, 'Error - %s is not a valid OLE file.' % bupname 234 | sys.exit(1) 235 | 236 | ole = olefile.OleFileIO(bupname) 237 | printNewline = False 238 | for entry in ole.listdir(): 239 | if entry[0] != "Details": 240 | if printNewline: 241 | print 242 | printNewline = True 243 | StdoutWriteChunked(DumpFunction(decryptStream(ole.openstream(entry[0]).read()))) 244 | if not allfiles: 245 | break 246 | ole.close() 247 | except Exception as e: 248 | print >>sys.stderr, 'Error - %s' % e 249 | sys.exit(1) 250 | 251 | def main(): 252 | parser = argparse.ArgumentParser(description="This script can be used to extract quarantined files from a McAfee .bup file. If run with no additional options the script will extract all files from the .bup and place them in a folder with the same name as the supplied .bup file.") 253 | parser.add_argument("infile", help="The file that you wish to un-bup.") 254 | parser.add_argument('-d','--details',dest="print_details",action='store_true',default=False,help="Only print the contents of the Details file. Don't extract any files.") 255 | parser.add_argument('-o','--original',dest="rename_original",action='store_true',default=False,help="Rename all quarantine files to their original names as noted in the Details file. Some assumptions have been made for this to feature to work. Use at your own risk.") 256 | parser.add_argument('-c','--hash',dest="hash",choices=('md5','sha1','sha256'),help="Calculates the hash for all of the files in the bup. ") 257 | parser.add_argument('-f','--firstfile',dest="print_firstfile",action='store_true',default=False,help="Output the first quarantined file.") 258 | parser.add_argument('-x','--hexdumpfirst',dest="print_hexdumpfirst",action='store_true',default=False,help="Perform a hexdump of the first quarantined file.") 259 | parser.add_argument('-X','--hexdumpall',dest="print_hexdumpall",action='store_true',default=False,help="Perform a hexdump of all quarantined files.") 260 | parser.add_argument('-a','--hexasciidumpfirst',dest="print_hexasciidumpfirst",action='store_true',default=False,help="Perform a hex & ASCII dump of the first quarantined file.") 261 | parser.add_argument('-A','--hexasciidumpall',dest="print_hexasciidumpall",action='store_true',default=False,help="Perform a hex & ASCII dump of all quarantined files.") 262 | args = parser.parse_args() 263 | 264 | bupname = args.infile 265 | #sanity check make sure .bup exists 266 | if not os.path.exists(bupname): 267 | print>>sys.stderr, "Error - the .bup file %s does not exist.\n" % bupname 268 | parser.print_help() 269 | sys.exit(1) 270 | if args.print_details: 271 | print getDetails(bupname) 272 | elif args.hash: 273 | hashes = getHashes(bupname,args.hash) 274 | for name in hashes: 275 | print "%s hash for %s: %s" % (args.hash,name,hashes[name]) 276 | elif args.rename_original: 277 | extractAll(bupname,original=True) 278 | elif args.print_firstfile: 279 | printDump(bupname) 280 | elif args.print_hexdumpfirst: 281 | printDump(bupname, HexDump) 282 | elif args.print_hexdumpall: 283 | printDump(bupname, HexDump, True) 284 | elif args.print_hexasciidumpfirst: 285 | printDump(bupname, HexAsciiDump) 286 | elif args.print_hexasciidumpall: 287 | printDump(bupname, HexAsciiDump, True) 288 | else: 289 | extractAll(bupname) 290 | 291 | if __name__ == '__main__': 292 | main() 293 | --------------------------------------------------------------------------------