# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. # # This software is provided under under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file # for more information. # # [C706] Transfer NDR Syntax implementation # # Author: Alberto Solino (@agsolino) # # ToDo: # [X] Unions and rest of the structured types # [ ] Documentation for this library, especially the support for Arrays # from __future__ import division from __future__ import print_function import random import inspect from struct import pack, unpack_from, calcsize from six import with_metaclass, PY3 from impacket import LOG from impacket.dcerpc.v5.enum import Enum from impacket.uuid import uuidtup_to_bin # Something important to have in mind: # Diagrams do not depict the specified alignment gaps, which can appear in the octet stream # before an item (see Section 14.2.2 on page 620.) # Where necessary, an alignment gap, consisting of octets of unspecified value, *precedes* the # representation of a primitive. The gap is of the smallest size sufficient to align the primitive class NDR(object): """ This will be the base class for all DCERPC NDR Types and represents a NDR Primitive Type """ referent = () commonHdr = () commonHdr64 = () structure = () structure64 = () align = 4 item = None _isNDR64 = False def __init__(self, data = None, isNDR64 = False): object.__init__(self) self._isNDR64 = isNDR64 self.fields = {} if isNDR64 is True: if self.commonHdr64 != (): self.commonHdr = self.commonHdr64 if self.structure64 != (): self.structure = self.structure64 if hasattr(self, 'align64'): self.align = self.align64 for fieldName, fieldTypeOrClass in self.commonHdr+self.structure+self.referent: if self.isNDR(fieldTypeOrClass): self.fields[fieldName] = fieldTypeOrClass(isNDR64 = self._isNDR64) elif fieldTypeOrClass == ':': self.fields[fieldName] = b'' elif len(fieldTypeOrClass.split('=')) == 2: try: self.fields[fieldName] = eval(fieldTypeOrClass.split('=')[1]) except: self.fields[fieldName] = None else: self.fields[fieldName] = [] if data is not None: self.fromString(data) def changeTransferSyntax(self, newSyntax): NDR64Syntax = uuidtup_to_bin(('71710533-BEBA-4937-8319-B5DBEF9CCC36', '1.0')) if newSyntax == NDR64Syntax: if self._isNDR64 is False: # Ok, let's change everything self._isNDR64 = True for fieldName in list(self.fields.keys()): if isinstance(self.fields[fieldName], NDR): self.fields[fieldName].changeTransferSyntax(newSyntax) # Finally, I change myself if self.commonHdr64 != (): self.commonHdr = self.commonHdr64 if self.structure64 != (): self.structure = self.structure64 if hasattr(self, 'align64'): self.align = self.align64 # And check whether the changes changed the data types # if so, I need to instantiate the new ones and copy the # old values for fieldName, fieldTypeOrClass in self.commonHdr+self.structure+self.referent: if isinstance(self.fields[fieldName], NDR): if fieldTypeOrClass != self.fields[fieldName].__class__ and isinstance(self.fields[fieldName], NDRPOINTERNULL) is False: backupData = self[fieldName] self.fields[fieldName] = fieldTypeOrClass(isNDR64 = self._isNDR64) if 'Data' in self.fields[fieldName].fields: self.fields[fieldName].fields['Data'] = backupData else: self[fieldName] = backupData else: if self._isNDR64 is True: # Ok, nothing for now raise Exception('Shouldn\'t be here') def __setitem__(self, key, value): if isinstance(value, NDRPOINTERNULL): value = NDRPOINTERNULL(isNDR64 = self._isNDR64) if isinstance(self.fields[key], NDRPOINTER): self.fields[key] = value elif 'Data' in self.fields[key].fields: if isinstance(self.fields[key].fields['Data'], NDRPOINTER): self.fields[key].fields['Data'] = value elif isinstance(value, NDR): # It's not a null pointer, ok. Another NDR type, but it # must be the same same as the iteam already in place if self.fields[key].__class__.__name__ == value.__class__.__name__: self.fields[key] = value elif isinstance(self.fields[key]['Data'], NDR): if self.fields[key]['Data'].__class__.__name__ == value.__class__.__name__: self.fields[key]['Data'] = value else: LOG.error("Can't setitem with class specified, should be %s" % self.fields[key]['Data'].__class__.__name__) else: LOG.error("Can't setitem with class specified, should be %s" % self.fields[key].__class__.__name__) elif isinstance(self.fields[key], NDR): self.fields[key]['Data'] = value else: self.fields[key] = value def __getitem__(self, key): if isinstance(self.fields[key], NDR): if 'Data' in self.fields[key].fields: return self.fields[key]['Data'] return self.fields[key] def __str__(self): return self.getData() def __len__(self): # XXX: improve return len(self.getData()) def getDataLen(self, data, offset=0): return len(data) - offset @staticmethod def isNDR(field): if inspect.isclass(field): myClass = field if issubclass(myClass, NDR): return True return False def dumpRaw(self, msg = None, indent = 0): if msg is None: msg = self.__class__.__name__ ind = ' '*indent print("\n%s" % msg) for field in self.commonHdr+self.structure+self.referent: i = field[0] if i in self.fields: if isinstance(self.fields[i], NDR): self.fields[i].dumpRaw('%s%s:{' % (ind,i), indent = indent + 4) print("%s}" % ind) elif isinstance(self.fields[i], list): print("%s[" % ind) for num,j in enumerate(self.fields[i]): if isinstance(j, NDR): j.dumpRaw('%s%s:' % (ind,i), indent = indent + 4) print("%s," % ind) else: print("%s%s: {%r}," % (ind, i, j)) print("%s]" % ind) else: print("%s%s: {%r}" % (ind,i,self[i])) def dump(self, msg = None, indent = 0): if msg is None: msg = self.__class__.__name__ ind = ' '*indent if msg != '': print("%s" % msg, end=' ') for fieldName, fieldType in self.commonHdr+self.structure+self.referent: if fieldName in self.fields: if isinstance(self.fields[fieldName], NDR): self.fields[fieldName].dump('\n%s%-31s' % (ind, fieldName+':'), indent = indent + 4), else: print(" %r" % (self[fieldName]), end=' ') def getAlignment(self): return self.align @staticmethod def calculatePad(fieldType, soFar): if isinstance(fieldType, str): try: alignment = calcsize(fieldType.split('=')[0]) except: alignment = 0 else: alignment = 0 if alignment > 0: pad = (alignment - (soFar % alignment)) % alignment else: pad = 0 return pad def getData(self, soFar = 0): data = b'' for fieldName, fieldTypeOrClass in self.commonHdr+self.structure: try: # Alignment of Primitive Types # NDR enforces NDR alignment of primitive data; that is, any primitive of size n # octets is aligned at a octet stream index that is a multiple of n. # (In this version of NDR, n is one of {1, 2, 4, 8}.) An octet stream index indicates # the number of an octet in an octet stream when octets are numbered, beginning with 0, # from the first octet in the stream. Where necessary, an alignment gap, consisting of # octets of unspecified value, precedes the representation of a primitive. The gap is # of the smallest size sufficient to align the primitive. pad = self.calculatePad(fieldTypeOrClass, soFar) if pad > 0: soFar += pad data += b'\xbf'*pad res = self.pack(fieldName, fieldTypeOrClass, soFar) data += res soFar += len(res) except Exception as e: LOG.error(str(e)) LOG.error("Error packing field '%s | %s' in %s" % (fieldName, fieldTypeOrClass, self.__class__)) raise return data def fromString(self, data, offset=0): offset0 = offset for fieldName, fieldTypeOrClass in self.commonHdr+self.structure: try: # Alignment of Primitive Types # NDR enforces NDR alignment of primitive data; that is, any primitive of size n # octets is aligned at a octet stream index that is a multiple of n. # (In this version of NDR, n is one of {1, 2, 4, 8}.) An octet stream index indicates # the number of an octet in an octet stream when octets are numbered, beginning with 0, # from the first octet in the stream. Where necessary, an alignment gap, consisting of # octets of unspecified value, precedes the representation of a primitive. The gap is # of the smallest size sufficient to align the primitive. offset += self.calculatePad(fieldTypeOrClass, offset) offset += self.unpack(fieldName, fieldTypeOrClass, data, offset) except Exception as e: LOG.error(str(e)) LOG.error("Error unpacking field '%s | %s | %r'" % (fieldName, fieldTypeOrClass, data[offset:offset+256])) raise return offset - offset0 def pack(self, fieldName, fieldTypeOrClass, soFar = 0): if isinstance(self.fields[fieldName], NDR): return self.fields[fieldName].getData(soFar) data = self.fields[fieldName] # void specifier if fieldTypeOrClass[:1] == '_': return b'' # code specifier two = fieldTypeOrClass.split('=') if len(two) >= 2: try: return self.pack(fieldName, two[0], soFar) except: self.fields[fieldName] = eval(two[1], {}, self.fields) return self.pack(fieldName, two[0], soFar) if data is None: raise Exception('Trying to pack None') # literal specifier if fieldTypeOrClass[:1] == ':': if hasattr(data, 'getData'): return data.getData() return data # struct like specifier return pack(fieldTypeOrClass, data) def unpack(self, fieldName, fieldTypeOrClass, data, offset=0): if isinstance(self.fields[fieldName], NDR): return self.fields[fieldName].fromString(data, offset) # code specifier two = fieldTypeOrClass.split('=') if len(two) >= 2: return self.unpack(fieldName, two[0], data, offset) # literal specifier if fieldTypeOrClass == ':': if isinstance(fieldTypeOrClass, NDR): return self.fields[fieldName].fromString(data, offset) else: dataLen = self.getDataLen(data, offset) self.fields[fieldName] = data[offset:offset+dataLen] return dataLen # struct like specifier self.fields[fieldName] = unpack_from(fieldTypeOrClass, data, offset)[0] return calcsize(fieldTypeOrClass) def calcPackSize(self, fieldTypeOrClass, data): if isinstance(fieldTypeOrClass, str) is False: return len(data) # code specifier two = fieldTypeOrClass.split('=') if len(two) >= 2: return self.calcPackSize(two[0], data) # literal specifier if fieldTypeOrClass[:1] == ':': return len(data) # struct like specifier return calcsize(fieldTypeOrClass) def calcUnPackSize(self, fieldTypeOrClass, data, offset=0): if isinstance(fieldTypeOrClass, str) is False: return len(data) - offset # code specifier two = fieldTypeOrClass.split('=') if len(two) >= 2: return self.calcUnPackSize(two[0], data, offset) # array specifier two = fieldTypeOrClass.split('*') if len(two) == 2: return len(data) - offset # literal specifier if fieldTypeOrClass[:1] == ':': return len(data) - offset # struct like specifier return calcsize(fieldTypeOrClass) # NDR Primitives class NDRSMALL(NDR): align = 1 structure = ( ('Data', 'b=0'), ) class NDRUSMALL(NDR): align = 1 structure = ( ('Data', 'B=0'), ) class NDRBOOLEAN(NDRSMALL): def dump(self, msg = None, indent = 0): if msg is None: msg = self.__class__.__name__ if msg != '': print(msg, end=' ') if self['Data'] > 0: print(" TRUE") else: print(" FALSE") class NDRCHAR(NDR): align = 1 structure = ( ('Data', 'c'), ) class NDRSHORT(NDR): align = 2 structure = ( ('Data', ' 0: soFar += pad0 arrayPadding = b'\xef'*pad0 else: arrayPadding = b'' # And now, let's pretend we put the item in soFar += arrayItemSize data = self.fields[fieldName].getData(soFar) data = arrayPadding + pack(arrayPackStr, self.getArrayMaximumSize(fieldName)) + data else: pad = self.calculatePad(fieldTypeOrClass, soFar) if pad > 0: soFar += pad data += b'\xcc'*pad data += self.pack(fieldName, fieldTypeOrClass, soFar) # Any referent information to pack? if isinstance(self.fields[fieldName], NDRCONSTRUCTEDTYPE): data += self.fields[fieldName].getDataReferents(soFar0 + len(data)) data += self.fields[fieldName].getDataReferent(soFar0 + len(data)) soFar = soFar0 + len(data) except Exception as e: LOG.error(str(e)) LOG.error("Error packing field '%s | %s' in %s" % (fieldName, fieldTypeOrClass, self.__class__)) raise return data def calcPackSize(self, fieldTypeOrClass, data): if isinstance(fieldTypeOrClass, str) is False: return len(data) # array specifier two = fieldTypeOrClass.split('*') if len(two) == 2: answer = 0 for each in data: if self.isNDR(self.item): item = ':' else: item = self.item answer += self.calcPackSize(item, each) return answer else: return NDR.calcPackSize(self, fieldTypeOrClass, data) def getArrayMaximumSize(self, fieldName): if self.fields[fieldName].fields['MaximumCount'] is not None and self.fields[fieldName].fields['MaximumCount'] > 0: return self.fields[fieldName].fields['MaximumCount'] else: return self.fields[fieldName].getArraySize() def getArraySize(self, fieldName, data, offset=0): if self._isNDR64: arrayItemSize = 8 arrayUnPackStr = ' align: align = tmpAlign return align def getData(self, soFar = 0): data = b'' soFar0 = soFar for fieldName, fieldTypeOrClass in self.structure: try: if self.isNDR(fieldTypeOrClass) is False: # If the item is not NDR (e.g. ('MaximumCount', ' 0: soFar += pad data += b'\xca'*pad res = self.pack(fieldName, fieldTypeOrClass, soFar) data += res soFar = soFar0 + len(data) except Exception as e: LOG.error(str(e)) LOG.error("Error packing field '%s | %s' in %s" % (fieldName, fieldTypeOrClass, self.__class__)) raise return data def pack(self, fieldName, fieldTypeOrClass, soFar = 0): # array specifier two = fieldTypeOrClass.split('*') if len(two) == 2: answer = b'' if self.isNDR(self.item): item = ':' dataClass = self.item self.fields['_tmpItem'] = dataClass(isNDR64=self._isNDR64) else: item = self.item dataClass = None self.fields['_tmpItem'] = item for each in (self.fields[fieldName]): pad = self.calculatePad(self.item, len(answer)+soFar) if pad > 0: answer += b'\xdd' * pad if dataClass is None: if item == 'c' and PY3 and isinstance(each, int): # Special case when dealing with PY3, here we have an integer we need to convert each = bytes([each]) answer += pack(item, each) else: answer += each.getData(len(answer)+soFar) if dataClass is not None: for each in self.fields[fieldName]: if isinstance(each, NDRCONSTRUCTEDTYPE): answer += each.getDataReferents(len(answer)+soFar) answer += each.getDataReferent(len(answer)+soFar) del(self.fields['_tmpItem']) if isinstance(self, NDRUniConformantArray) or isinstance(self, NDRUniConformantVaryingArray): # First field points to a field with the amount of items self.setArraySize(len(self.fields[fieldName])) else: self.fields[two[1]] = len(self.fields[fieldName]) return answer else: return NDRCONSTRUCTEDTYPE.pack(self, fieldName, fieldTypeOrClass, soFar) def fromString(self, data, offset=0): offset0 = offset for fieldName, fieldTypeOrClass in self.commonHdr+self.structure: try: if self.isNDR(fieldTypeOrClass) is False: # If the item is not NDR (e.g. ('MaximumCount', ' 0: soFarItems +=pad if dataClassOrCode is None: nsofar = soFarItems + calcsize(item) answer.append(unpack_from(item, data, offset+soFarItems)[0]) else: itemn = dataClassOrCode(isNDR64=self._isNDR64) size = itemn.fromString(data, offset+soFarItems) answer.append(itemn) nsofar += size + pad numItems -= 1 soFarItems = nsofar if dataClassOrCode is not None and isinstance(dataClassOrCode(), NDRCONSTRUCTEDTYPE): # We gotta go over again, asking for the referents answer2 = [] for itemn in answer: size = itemn.fromStringReferents(data, soFarItems+offset) soFarItems += size size = itemn.fromStringReferent(data, soFarItems+offset) soFarItems += size answer2.append(itemn) answer = answer2 del answer2 del(self.fields['_tmpItem']) self.fields[fieldName] = answer return soFarItems + offset - offset0 else: return NDRCONSTRUCTEDTYPE.unpack(self, fieldName, fieldTypeOrClass, data, offset) class NDRUniFixedArray(NDRArray): structure = ( ('Data',':'), ) # Uni-dimensional Conformant Arrays class NDRUniConformantArray(NDRArray): item = 'c' structure = ( #('MaximumCount', '