from __future__ import print_function # Copyright (c) 2003-2016 CORE Security Technologies # # This software is provided under under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file # for more information. # from struct import pack, unpack, calcsize class Structure: """ sublcasses can define commonHdr and/or structure. each of them is an tuple of either two: (fieldName, format) or three: (fieldName, ':', class) fields. [it can't be a dictionary, because order is important] where format specifies how the data in the field will be converted to/from bytes (string) class is the class to use when unpacking ':' fields. each field can only contain one value (or an array of values for *) i.e. struct.pack('Hl',1,2) is valid, but format specifier 'Hl' is not (you must use 2 dfferent fields) format specifiers: specifiers from module pack can be used with the same format see struct.__doc__ (pack/unpack is finally called) x [padding byte] c [character] b [signed byte] B [unsigned byte] h [signed short] H [unsigned short] l [signed long] L [unsigned long] i [signed integer] I [unsigned integer] q [signed long long (quad)] Q [unsigned long long (quad)] s [string (array of chars), must be preceded with length in format specifier, padded with zeros] p [pascal string (includes byte count), must be preceded with length in format specifier, padded with zeros] f [float] d [double] = [native byte ordering, size and alignment] @ [native byte ordering, standard size and alignment] ! [network byte ordering] < [little endian] > [big endian] usual printf like specifiers can be used (if started with %) [not recommeneded, there is no why to unpack this] %08x will output an 8 bytes hex %s will output a string %s\\x00 will output a NUL terminated string %d%d will output 2 decimal digits (against the very same specification of Structure) ... some additional format specifiers: : just copy the bytes from the field into the output string (input may be string, other structure, or anything responding to __str__()) (for unpacking, all what's left is returned) z same as :, but adds a NUL byte at the end (asciiz) (for unpacking the first NUL byte is used as terminator) [asciiz string] u same as z, but adds two NUL bytes at the end (after padding to an even size with NULs). (same for unpacking) [unicode string] w DCE-RPC/NDR string (it's a macro for [ ' 2: dataClassOrCode = field[2] try: self[field[0]] = self.unpack(field[1], data[:size], dataClassOrCode = dataClassOrCode, field = field[0]) except Exception as e: e.args += ("When unpacking field '%s | %s | %r[:%d]'" % (field[0], field[1], data, size),) raise size = self.calcPackSize(field[1], self[field[0]], field[0]) if self.alignment and size % self.alignment: size += self.alignment - (size % self.alignment) data = data[size:] return self def __setitem__(self, key, value): self.fields[key] = value self.data = None # force recompute def __getitem__(self, key): return self.fields[key] def __delitem__(self, key): del self.fields[key] def __str__(self): return self.getData() def __len__(self): # XXX: improve return len(self.getData()) def pack(self, format, data, field = None): if self.debug: print(" pack( %s | %r | %s)" % (format, data, field)) if field: addressField = self.findAddressFieldFor(field) if (addressField is not None) and (data is None): return '' # void specifier if format[:1] == '_': return '' # quote specifier if format[:1] == "'" or format[:1] == '"': return format[1:] # code specifier two = format.split('=') if len(two) >= 2: try: return self.pack(two[0], data) except: fields = {'self':self} fields.update(self.fields) return self.pack(two[0], eval(two[1], {}, fields)) # address specifier two = format.split('&') if len(two) == 2: try: return self.pack(two[0], data) except: if (two[1] in self.fields) and (self[two[1]] is not None): return self.pack(two[0], id(self[two[1]]) & ((1<<(calcsize(two[0])*8))-1) ) else: return self.pack(two[0], 0) # length specifier two = format.split('-') if len(two) == 2: try: return self.pack(two[0],data) except: return self.pack(two[0], self.calcPackFieldSize(two[1])) # array specifier two = format.split('*') if len(two) == 2: answer = '' for each in data: answer += self.pack(two[1], each) if two[0]: if two[0].isdigit(): if int(two[0]) != len(data): raise Exception("Array field has a constant size, and it doesn't match the actual value") else: return self.pack(two[0], len(data))+answer return answer # "printf" string specifier if format[:1] == '%': # format string like specifier return format % data # asciiz specifier if format[:1] == 'z': return str(data)+'\0' # unicode specifier if format[:1] == 'u': return str(data)+'\0\0' + (len(data) & 1 and '\0' or '') # DCE-RPC/NDR string specifier if format[:1] == 'w': if len(data) == 0: data = '\0\0' elif len(data) % 2: data += '\0' l = pack('= 2: return self.unpack(two[0],data) # length specifier two = format.split('-') if len(two) == 2: return self.unpack(two[0],data) # array specifier two = format.split('*') if len(two) == 2: answer = [] sofar = 0 if two[0].isdigit(): number = int(two[0]) elif two[0]: sofar += self.calcUnpackSize(two[0], data) number = self.unpack(two[0], data[:sofar]) else: number = -1 while number and sofar < len(data): nsofar = sofar + self.calcUnpackSize(two[1],data[sofar:]) answer.append(self.unpack(two[1], data[sofar:nsofar], dataClassOrCode)) number -= 1 sofar = nsofar return answer # "printf" string specifier if format[:1] == '%': # format string like specifier return format % data # asciiz specifier if format == 'z': if data[-1] != '\x00': raise Exception("%s 'z' field is not NUL terminated: %r" % (field, data)) return data[:-1] # remove trailing NUL # unicode specifier if format == 'u': if data[-2:] != '\x00\x00': raise Exception("%s 'u' field is not NUL-NUL terminated: %r" % (field, data)) return data[:-2] # remove trailing NUL # DCE-RPC/NDR string specifier if format == 'w': l = unpack('= 2: return self.calcPackSize(two[0], data) # length specifier two = format.split('-') if len(two) == 2: return self.calcPackSize(two[0], data) # array specifier two = format.split('*') if len(two) == 2: answer = 0 if two[0].isdigit(): if int(two[0]) != len(data): raise Exception("Array field has a constant size, and it doesn't match the actual value") elif two[0]: answer += self.calcPackSize(two[0], len(data)) for each in data: answer += self.calcPackSize(two[1], each) return answer # "printf" string specifier if format[:1] == '%': # format string like specifier return len(format % data) # asciiz specifier if format[:1] == 'z': return len(data)+1 # asciiz specifier if format[:1] == 'u': l = len(data) return l + (l & 1 and 3 or 2) # DCE-RPC/NDR string specifier if format[:1] == 'w': l = len(data) return 12+l+l % 2 # literal specifier if format[:1] == ':': return len(data) # struct like specifier return calcsize(format) def calcUnpackSize(self, format, data, field = None): if self.debug: print(" calcUnpackSize( %s | %s | %r)" % (field, format, data)) # void specifier if format[:1] == '_': return 0 addressField = self.findAddressFieldFor(field) if addressField is not None: if not self[addressField]: return 0 try: lengthField = self.findLengthFieldFor(field) return self[lengthField] except: pass # XXX: Try to match to actual values, raise if no match # quote specifier if format[:1] == "'" or format[:1] == '"': return len(format)-1 # address specifier two = format.split('&') if len(two) == 2: return self.calcUnpackSize(two[0], data) # code specifier two = format.split('=') if len(two) >= 2: return self.calcUnpackSize(two[0], data) # length specifier two = format.split('-') if len(two) == 2: return self.calcUnpackSize(two[0], data) # array specifier two = format.split('*') if len(two) == 2: answer = 0 if two[0]: if two[0].isdigit(): number = int(two[0]) else: answer += self.calcUnpackSize(two[0], data) number = self.unpack(two[0], data[:answer]) while number: number -= 1 answer += self.calcUnpackSize(two[1], data[answer:]) else: while answer < len(data): answer += self.calcUnpackSize(two[1], data[answer:]) return answer # "printf" string specifier if format[:1] == '%': raise Exception("Can't guess the size of a printf like specifier for unpacking") # asciiz specifier if format[:1] == 'z': return data.index('\x00')+1 # asciiz specifier if format[:1] == 'u': l = data.index('\x00\x00') return l + (l & 1 and 3 or 2) # DCE-RPC/NDR string specifier if format[:1] == 'w': l = unpack('L'), ('code1','>L=len(arr1)*2+0x1000'), ) def populate(self, a): a['default'] = 'hola' a['int1'] = 0x3131 a['int3'] = 0x45444342 a['z1'] = 'hola' a['u1'] = 'hola'.encode('utf_16_le') a[':1'] = ':1234:' a['arr1'] = (0x12341234,0x88990077,0x41414141) # a['len1'] = 0x42424242 class _Test_fixedLength(_Test_simple): def populate(self, a): _Test_simple.populate(self, a) a['len1'] = 0x42424242 class _Test_simple_aligned4(_Test_simple): alignment = 4 class _Test_nested(_StructureTest): class theClass(Structure): class _Inner(Structure): structure = (('data', 'z'),) structure = ( ('nest1', ':', _Inner), ('nest2', ':', _Inner), ('int', '> 8)'), ('pad', '_','((iv >>2) & 0x3F)'), ('keyid', '_','( iv & 0x03 )'), ('dataLen', '_-data', 'len(inputDataLeft)-4'), ('data',':'), ('icv','>L'), ) def populate(self, a): a['init_vector']=0x01020304 #a['pad']=int('01010101',2) a['pad']=int('010101',2) a['keyid']=0x07 a['data']="\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9" a['icv'] = 0x05060708 #a['iv'] = 0x01020304 if __name__ == '__main__': _Test_simple().run() try: _Test_fixedLength().run() except: print("cannot repack because length is bogus") _Test_simple_aligned4().run() _Test_nested().run() _Test_Optional().run() _Test_Optional_sparse().run() _Test_AsciiZArray().run() _Test_UnpackCode().run() _Test_AAA().run()