mirror of
https://github.com/dobin/avred
synced 2026-06-08 13:54:13 +00:00
1166 lines
44 KiB
Python
1166 lines
44 KiB
Python
"""
|
|
Author: Dominik Reichel - Palo Alto Networks (2021-2022)
|
|
|
|
dotnetfile - Interface library for the CLR header parser library for Windows .NET assemblies.
|
|
|
|
The following references were used:
|
|
CLI specification (ECMA-335 standard)
|
|
https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf
|
|
Microsoft .NET metadata documentation
|
|
https://docs.microsoft.com/en-us/dotnet/api/system.reflection.metadata.ecma335?view=net-5.0
|
|
.NET runtime code
|
|
https://github.com/dotnet/runtime
|
|
|
|
Thanks to the authors of the following tools and libraries:
|
|
- dnSpy + dnlib
|
|
- dotPeek
|
|
- ILSpy
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from hashlib import md5, sha1, sha256
|
|
from enum import Enum, IntEnum, IntFlag, auto
|
|
from typing import List, Dict, Optional, Union
|
|
|
|
from .parser import DotNetPEParser
|
|
from .constants import METADATA_TABLE_INDEXES
|
|
|
|
|
|
METADATA_TABLES = {}
|
|
|
|
|
|
def metatable(cls):
|
|
METADATA_TABLES[cls.__name__] = cls
|
|
return cls
|
|
|
|
|
|
class Type:
|
|
class MethodDefMask(IntEnum):
|
|
"""
|
|
Sources:
|
|
https://www.ecma-international.org/publications-and-standards/standards/ecma-335/
|
|
https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodattributes?view=net-5.0
|
|
"""
|
|
MEMBERACCESS = 7
|
|
UNMANAGEDEXPORT = 8
|
|
STATIC = 16
|
|
FINAL = 32
|
|
VIRTUAL = 64
|
|
HIDEBYSIG = 128
|
|
VTABLELAYOUTMASK = 256
|
|
STRICT = 512
|
|
ABSTRACT = 1024
|
|
SPECIALNAME = 2048
|
|
RTSPECIALNAME = 4096
|
|
PINVOKEIMPL = 8192
|
|
HASSECURITY = 16384
|
|
REQUIRESECOBJECT = 32768
|
|
|
|
class MethodDefMemberAccess(IntFlag):
|
|
"""
|
|
Sources:
|
|
https://www.ecma-international.org/publications-and-standards/standards/ecma-335/
|
|
https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodattributes?view=net-5.0
|
|
"""
|
|
COMPILERCONTROLLED = auto()
|
|
PRIVATE = auto()
|
|
FAMANDASSEM = auto()
|
|
ASSEM = auto()
|
|
FAMILY = auto()
|
|
FAMORASSEM = auto()
|
|
PUBLIC = auto()
|
|
ANY = auto()
|
|
|
|
class TypeDefMask(IntEnum):
|
|
"""
|
|
Sources:
|
|
https://www.ecma-international.org/publications-and-standards/standards/ecma-335/
|
|
https://docs.microsoft.com/en-us/dotnet/api/system.reflection.typeattributes?view=net-5.0
|
|
"""
|
|
VISIBILITY = 7
|
|
LAYOUT = 24
|
|
CLASSSEMANTIC = 32
|
|
|
|
class TypeDefVisibility(IntFlag):
|
|
"""
|
|
Sources:
|
|
https://www.ecma-international.org/publications-and-standards/standards/ecma-335/
|
|
https://docs.microsoft.com/en-us/dotnet/api/system.reflection.typeattributes?view=net-5.0
|
|
"""
|
|
NOTPUBLIC = auto()
|
|
PUBLIC = auto()
|
|
NESTEDPUBLIC = auto()
|
|
NESTEDPRIVATE = auto()
|
|
NESTEDFAMILY = auto()
|
|
NESTEDASSEMBLY = auto()
|
|
NESTEDFAMANDASSEM = auto()
|
|
NESTEDFAMORASSEM = auto()
|
|
ANY = auto()
|
|
|
|
class UnmanagedFunctions(IntEnum):
|
|
RAW = 0
|
|
CHARSET = 1
|
|
NOSET = 2
|
|
|
|
class UnmanagedModules(IntEnum):
|
|
RAW = 0
|
|
NORMALIZED = 1
|
|
|
|
class Hash(IntEnum):
|
|
MD5 = 0
|
|
SHA1 = 1
|
|
SHA256 = 2
|
|
|
|
class EntryPoint(Enum):
|
|
NATIVE = 'Native'
|
|
MANAGED = 'Managed'
|
|
|
|
|
|
class Struct:
|
|
@dataclass
|
|
class NativeEntryPoint:
|
|
EntryPointType: str
|
|
Address: str
|
|
|
|
@dataclass
|
|
class ManagedEntryPoint:
|
|
EntryPointType: str
|
|
Method: str
|
|
Type: Optional[str] = ''
|
|
Namespace: Optional[str] = ''
|
|
Signature: Optional[Dict] = ''
|
|
|
|
@dataclass
|
|
class TypesMethods:
|
|
Type: str
|
|
Namespace: str
|
|
Methods: List["Methods"]
|
|
Flags: int
|
|
|
|
@dataclass
|
|
class Methods:
|
|
Name: str
|
|
Signature: Dict
|
|
Flags: int
|
|
|
|
@dataclass
|
|
class EntryPoint:
|
|
Method: str
|
|
Signature: Dict
|
|
Type: str
|
|
Namespace: str
|
|
|
|
@dataclass
|
|
class AssemblyInfo:
|
|
MajorVersion: int
|
|
MinorVersion: int
|
|
BuildNumber: int
|
|
RevisionNumber: int
|
|
|
|
|
|
class DotNetPE(DotNetPEParser):
|
|
def __init__(self, path):
|
|
super().__init__(path)
|
|
self.AntiMetadataAnalysis = AntiMetadataAnalysis(self)
|
|
self.Cor20Header = Cor20Header(self)
|
|
self.Type = Type()
|
|
|
|
for table_name, table_obj in METADATA_TABLES.items():
|
|
if self.metadata_table_exists(table_name):
|
|
table_instance = table_obj(self)
|
|
setattr(self, table_name, table_instance)
|
|
|
|
def metadata_table_exists(self, name: str) -> bool:
|
|
"""
|
|
Check if table exists in metadata tables.
|
|
"""
|
|
result = False
|
|
|
|
if name in self.metadata_tables_lookup.keys():
|
|
result = True
|
|
|
|
return result
|
|
|
|
def existent_metadata_tables(self) -> List[str]:
|
|
"""
|
|
Get all metadata tables that exist in the file.
|
|
"""
|
|
return [*self.metadata_tables_lookup]
|
|
|
|
def is_mixed_assembly(self) -> bool:
|
|
"""
|
|
Check if the file is a mixed assembly that contains managed + native code.
|
|
"""
|
|
result = False
|
|
|
|
mixed_assembly_namespaces = [
|
|
'<CppImplementationDetails>',
|
|
'<CrtImplementationDetails>'
|
|
]
|
|
|
|
# Check if the common mixed assembly namespaces '<CppImplementationDetails>' and '<CrtImplementationDetails>'
|
|
# are referenced in the TypeDef table. We could also check for presence of these strings in the MethodDef table
|
|
# or the #Strings stream.
|
|
if self.metadata_table_exists('TypeDef'):
|
|
namespaces = []
|
|
for table_row in self.metadata_tables_lookup['TypeDef'].table_rows:
|
|
namespace_string_address = table_row.string_stream_references['TypeNamespace']
|
|
namespaces.append(self.get_string(namespace_string_address))
|
|
|
|
if all(x in namespaces for x in mixed_assembly_namespaces):
|
|
result = True
|
|
|
|
return result
|
|
|
|
def has_native_entry_point(self) -> bool:
|
|
"""
|
|
Check if the file has a native entry point (and thus is also a mixed assembly).
|
|
"""
|
|
result = False
|
|
|
|
# Check if the file has the native entry point value set in the Cor20 header's flags. If the value 0x10 is set
|
|
# (COMIMAGE_FLAGS_NATIVE_ENTRYPOINT), the file is also automatically a mixed assembly. However, not all mixed
|
|
# assemblies have a native entry point. Additionally, we check if the EntryPointToken field that contains the
|
|
# entry point address is not zero.
|
|
if (self.clr_header.Flags.value & 0x10) == 0x10 and self.clr_header.EntryPointToken.value:
|
|
result = True
|
|
|
|
return result
|
|
|
|
def is_native_image(self) -> bool:
|
|
"""
|
|
Check if the file is a native image generated by the Ngen tool ngen.exe.
|
|
"""
|
|
result = False
|
|
|
|
# Check if the file has the IL Library value set in the Cor20 header's flags. If the value 0x4 is set
|
|
# (COMIMAGE_FLAGS_IL_LIBRARY), the file is a native image created by Ngen. Additionally, we check if the
|
|
# ManagedNativeHeader address field contains a value that points to the native image header.
|
|
if (self.clr_header.Flags.value & 0x4) == 0x4 and self.clr_header.ManagedNativeHeaderAddress.value:
|
|
result = True
|
|
|
|
return result
|
|
|
|
def is_windows_forms_app(self) -> bool:
|
|
"""
|
|
Check if the file is a Windows Forms app.
|
|
"""
|
|
result = False
|
|
|
|
if 'System.Windows.Forms' in self.get_all_references():
|
|
if self.metadata_table_exists('TypeRef'):
|
|
for table_row in self.metadata_tables_lookup['TypeRef'].table_rows:
|
|
type_name_address = table_row.string_stream_references['TypeName']
|
|
if self.get_string(type_name_address) == 'STAThreadAttribute':
|
|
result = True
|
|
|
|
return result
|
|
|
|
def has_resources(self) -> bool:
|
|
"""
|
|
Check if .NET resources exist.
|
|
"""
|
|
result = False
|
|
|
|
# Check if the Resources RVA value in the Cor20 header isn't empty. If positive, the file has at least one
|
|
# resource. Additionally, check if the assembly has a ManifestResource table that contains resource information.
|
|
if self.clr_header.ResourcesDirectoryAddress.value and self.metadata_table_exists('ManifestResource'):
|
|
result = True
|
|
|
|
return result
|
|
|
|
def get_string(self, string_address: int) -> str:
|
|
"""
|
|
Get string from the #Strings normal stream lookup dictionary or the overlap strings list
|
|
"""
|
|
try:
|
|
result = self.dotnet_string_lookup[string_address].string_representation
|
|
except KeyError:
|
|
result = self.dotnet_overlap_string_lookup[string_address].string_representation
|
|
|
|
return result
|
|
|
|
@staticmethod
|
|
def get_hash(hash_type: Type.Hash, string_list: List) -> str:
|
|
"""
|
|
Convert list of strings to single string and calculate hash.
|
|
"""
|
|
result = ''
|
|
|
|
if hash_type == Type.Hash.MD5:
|
|
result = md5(','.join(string_list).encode())
|
|
elif hash_type == Type.Hash.SHA1:
|
|
result = sha1(','.join(string_list).encode())
|
|
elif hash_type == Type.Hash.SHA256:
|
|
result = sha256(','.join(string_list).encode())
|
|
|
|
return result.hexdigest()
|
|
|
|
def get_all_references(self) -> List[str]:
|
|
"""
|
|
Get a list of all referenced libraries.
|
|
"""
|
|
result = []
|
|
|
|
if self.metadata_table_exists('AssemblyRef'):
|
|
result = self.AssemblyRef.get_assemblyref_names(deduplicate=True)
|
|
|
|
if self.metadata_table_exists('ModuleRef'):
|
|
result += self.ModuleRef.get_unmanaged_module_names(Type.UnmanagedModules.NORMALIZED)
|
|
|
|
return result
|
|
|
|
def get_strings_stream_strings(self) -> List[str]:
|
|
"""
|
|
Get all strings of the #Strings stream.
|
|
"""
|
|
result = []
|
|
|
|
for string in self.dotnet_string_lookup.values():
|
|
result.append(string.string_representation)
|
|
|
|
return result
|
|
|
|
def get_user_stream_strings(self) -> List[str]:
|
|
"""
|
|
Get all strings of the #US stream.
|
|
"""
|
|
result = []
|
|
|
|
for string in self.user_string_stream_strings:
|
|
result.append(string.string_representation)
|
|
|
|
return result
|
|
|
|
def get_stream_names(self) -> List[str]:
|
|
"""
|
|
Get a list of stream names.
|
|
"""
|
|
result = []
|
|
|
|
for stream in self.dotnet_streams:
|
|
result.append(stream.string_representation)
|
|
|
|
return result
|
|
|
|
def get_resources(self) -> List[Dict]:
|
|
"""
|
|
Get .NET resources with additional information.
|
|
"""
|
|
if not self.has_resources():
|
|
self.logger.debug('File does not have .NET resources.')
|
|
return []
|
|
|
|
return self.dotnet_resources
|
|
|
|
def get_runtime_target_version(self) -> str:
|
|
"""
|
|
Get file's .NET runtime target version.
|
|
"""
|
|
return self.dotnet_metadata_header.VersionString.field_text
|
|
|
|
def get_number_of_streams(self) -> int:
|
|
"""
|
|
Get number of total streams (includes fake streams).
|
|
"""
|
|
return self.dotnet_metadata_header.NumberOfStreams.value
|
|
|
|
|
|
class AntiMetadataAnalysis:
|
|
"""
|
|
Contains detected metadata anti-parsing tricks.
|
|
"""
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
@property
|
|
def is_dotnet_data_directory_hidden(self):
|
|
"""
|
|
.NET data directory in the PE header is hidden.
|
|
"""
|
|
return self.dotnetpe.dotnet_anti_metadata['data_directory_hidden']
|
|
|
|
@property
|
|
def has_metadata_table_extra_data(self):
|
|
"""
|
|
Tables header contains 4 bytes of extra data.
|
|
"""
|
|
return self.dotnetpe.dotnet_anti_metadata['metadata_table_has_extra_data']
|
|
|
|
@property
|
|
def has_self_referenced_typeref_entries(self):
|
|
"""
|
|
TypeRef table contains entries that reference each other.
|
|
"""
|
|
result = False
|
|
|
|
if not self.dotnetpe.metadata_table_exists('TypeRef'):
|
|
self.dotnetpe.logger.debug('File does not have a TypeRef table.')
|
|
return result
|
|
|
|
for current_row_index, table_row in enumerate(self.dotnetpe.metadata_tables_lookup['TypeRef'].table_rows):
|
|
type_name = table_row.table_references['ResolutionScope'][0]
|
|
|
|
if type_name == 'TypeRef':
|
|
table_row_reference = table_row.table_references['ResolutionScope'][1] - 1
|
|
|
|
# Skip invalid entries
|
|
if table_row_reference >= len(self.dotnetpe.metadata_tables_lookup['TypeRef'].table_rows):
|
|
continue
|
|
|
|
if self.dotnetpe.metadata_tables_lookup['TypeRef'].table_rows[
|
|
table_row_reference].table_references['ResolutionScope'][1] - 1 == current_row_index:
|
|
result = True
|
|
break
|
|
|
|
return result
|
|
|
|
@property
|
|
def has_invalid_typeref_entries(self):
|
|
"""
|
|
TypeRef table contains invalid entries.
|
|
"""
|
|
result = False
|
|
|
|
if not self.dotnetpe.metadata_table_exists('TypeRef'):
|
|
self.dotnetpe.logger.debug('File does not have a TypeRef table.')
|
|
return result
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['TypeRef'].table_rows:
|
|
type_name = table_row.table_references['ResolutionScope'][0]
|
|
|
|
if type_name == 'TypeRef':
|
|
table_row_reference = table_row.table_references['ResolutionScope'][1] - 1
|
|
type_name_address = table_row.string_stream_references['TypeName']
|
|
|
|
if table_row_reference > len(self.dotnetpe.metadata_tables_lookup[
|
|
'TypeRef'].table_rows) or not type_name_address:
|
|
result = True
|
|
break
|
|
|
|
return result
|
|
|
|
@property
|
|
def has_fake_data_streams(self):
|
|
"""
|
|
CLR header contains fake data streams.
|
|
"""
|
|
return self.dotnetpe.dotnet_anti_metadata['has_fake_data_streams']
|
|
|
|
@property
|
|
def module_table_has_multiple_rows(self):
|
|
"""
|
|
Module table has more than one row.
|
|
"""
|
|
result = False
|
|
|
|
if self.dotnetpe.metadata_table_exists('Module'):
|
|
if len(self.dotnetpe.metadata_tables_lookup['Module'].table_rows) > 1:
|
|
result = True
|
|
|
|
return result
|
|
|
|
@property
|
|
def assembly_table_has_multiple_rows(self):
|
|
"""
|
|
Assembly table has more than one row.
|
|
"""
|
|
result = False
|
|
|
|
if self.dotnetpe.metadata_table_exists('Assembly'):
|
|
if len(self.dotnetpe.metadata_tables_lookup['Assembly'].table_rows) > 1:
|
|
result = True
|
|
|
|
return result
|
|
|
|
@property
|
|
def has_invalid_strings_stream_entries(self):
|
|
"""
|
|
#Strings stream contains invalid entries.
|
|
"""
|
|
return self.dotnetpe.dotnet_anti_metadata['has_invalid_strings_stream_entries']
|
|
|
|
|
|
class Cor20Header:
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
def entry_point_exists(self) -> bool:
|
|
"""
|
|
Check if defined entry point exists.
|
|
"""
|
|
return True if self.dotnetpe.clr_header.EntryPointToken.value else False
|
|
|
|
def get_header_entry_point(self) -> Optional[Union[Struct.NativeEntryPoint, Struct.ManagedEntryPoint]]:
|
|
"""
|
|
Get defined entry point along with type, namespace and possible parameter(s).
|
|
|
|
TODO: Add more blob signatures or create proper blob signature decoding
|
|
"""
|
|
flag_native_entry_point = 0x10
|
|
result = None
|
|
|
|
if not self.entry_point_exists() or not self.dotnetpe.metadata_table_exists(
|
|
'TypeDef') or not self.dotnetpe.metadata_table_exists(
|
|
'MethodDef') or not self.dotnetpe.dotnet_string_lookup:
|
|
self.dotnetpe.logger.debug('Cross-reference error: File does not have a TypeDef or MethodDef table.')
|
|
return result
|
|
|
|
# Check if the file has a native entry point
|
|
if (self.dotnetpe.clr_header.Flags.value & flag_native_entry_point) == flag_native_entry_point:
|
|
return Struct.NativeEntryPoint(Type.EntryPoint.NATIVE.value,
|
|
hex(self.dotnetpe.clr_header.EntryPointToken.value))
|
|
|
|
entry_point_token = self.dotnetpe.clr_header.EntryPointToken.value
|
|
table_index = entry_point_token >> 24
|
|
table_row = entry_point_token & 0xffffff
|
|
|
|
metadata_table_row = self.dotnetpe.metadata_tables_lookup[METADATA_TABLE_INDEXES[table_index]].table_rows[
|
|
table_row - 1]
|
|
|
|
method_string_address = metadata_table_row.string_stream_references['Name']
|
|
method_name = self.dotnetpe.get_string(method_string_address)
|
|
result = Struct.ManagedEntryPoint(Type.EntryPoint.MANAGED.value, method_name)
|
|
|
|
method_signature_index = metadata_table_row.blob_stream_references['Signature']
|
|
method_signature = self.dotnetpe.dotnet_blob_lookup[method_signature_index]
|
|
|
|
types_with_methods = self.dotnetpe.TypeDef.get_type_names_with_methods()
|
|
|
|
for type_with_method in types_with_methods:
|
|
for method in type_with_method.Methods:
|
|
if method.Name == method_name and method.Signature == method_signature.string_representation:
|
|
result.Type = type_with_method.Type
|
|
result.Namespace = type_with_method.Namespace
|
|
result.Signature = method_signature.string_representation
|
|
|
|
return result
|
|
|
|
|
|
# Table 0
|
|
@metatable
|
|
class Module:
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
def get_module_name(self) -> str:
|
|
"""
|
|
Get module name.
|
|
"""
|
|
# To counteract .NET protectors like ConfuserEx that add additional entries, we skip the remaining row(s) as
|
|
# they are officially not supported and contain invalid entries
|
|
string_address = self.dotnetpe.metadata_tables_lookup['Module'].table_rows[0].string_stream_references['Name']
|
|
return self.dotnetpe.get_string(string_address)
|
|
|
|
|
|
# Table 1
|
|
@metatable
|
|
class TypeRef:
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
def get_typeref_names(self) -> List[str]:
|
|
"""
|
|
Get a list of referenced type names.
|
|
"""
|
|
result = []
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['TypeRef'].table_rows:
|
|
type_string_address = table_row.string_stream_references['TypeName']
|
|
type_name = self.dotnetpe.get_string(type_string_address)
|
|
result.append(type_name)
|
|
|
|
return result
|
|
|
|
def get_typeref_hash(self, hash_type: Type.Hash = Type.Hash.SHA256, skip_self_referenced_entries: bool = True,
|
|
strings_sorted: bool = False) -> str:
|
|
"""
|
|
TypeRefHash: Get hash of type and their corresponding resolution scope names.
|
|
|
|
In contrast to the original TypeRefHash implementation from GData, we use the resolution scope names as they're
|
|
always present compared to the namespace names. Additionally, you can skip types that reference each other as
|
|
added by some .NET protectors. Furthermore, the list of scope and type names can be sorted alphabetically after
|
|
the type names before being hashed. Strings are also used case-sensitive.
|
|
|
|
Options:
|
|
- Type.Hash.MD5, Type.Hash.SHA1, Type.Hash.SHA256
|
|
- Skip types that reference each other (added by some .NET protectors)
|
|
- Strings unsorted or sorted (after the type names)
|
|
|
|
Source:
|
|
https://www.gdatasoftware.com/blog/2020/06/36164-introducing-the-typerefhash-trh
|
|
"""
|
|
types_scopes = []
|
|
for current_row_index, table_row in enumerate(self.dotnetpe.metadata_tables_lookup['TypeRef'].table_rows):
|
|
type_string_address = table_row.string_stream_references['TypeName']
|
|
type_name = self.dotnetpe.get_string(type_string_address)
|
|
|
|
resolution_scope_table = table_row.table_references['ResolutionScope'][0]
|
|
resolution_scope_index = table_row.table_references['ResolutionScope'][1] - 1
|
|
current_type_name_address = table_row.string_stream_references['TypeName']
|
|
|
|
# Skip invalid type rows
|
|
if resolution_scope_index > len(
|
|
self.dotnetpe.metadata_tables_lookup['TypeRef'].table_rows) or not current_type_name_address:
|
|
continue
|
|
|
|
if skip_self_referenced_entries and \
|
|
resolution_scope_table == 'TypeRef' and \
|
|
self.dotnetpe.AntiMetadataAnalysis.has_self_referenced_typeref_entries and \
|
|
self.dotnetpe.metadata_tables_lookup['TypeRef'].table_rows[
|
|
resolution_scope_index].table_references['ResolutionScope'][1] - 1 == current_row_index:
|
|
continue
|
|
|
|
name = ''
|
|
if resolution_scope_table in ['Module', 'ModuleRef', 'AssemblyRef']:
|
|
name = 'Name'
|
|
elif resolution_scope_table == 'TypeRef':
|
|
name = 'TypeName'
|
|
|
|
resolution_scope_string_address = self.dotnetpe.metadata_tables_lookup[resolution_scope_table].table_rows[
|
|
resolution_scope_index].string_stream_references[name]
|
|
resolution_scope_name = self.dotnetpe.get_string(resolution_scope_string_address)
|
|
|
|
types_scopes.append(f'{resolution_scope_name}-{type_name}')
|
|
|
|
if strings_sorted:
|
|
types_scopes.sort(key=lambda x: x.split('-')[1])
|
|
|
|
return self.dotnetpe.get_hash(hash_type, types_scopes)
|
|
|
|
|
|
# Table 2
|
|
@metatable
|
|
class TypeDef:
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
def get_type_names(self, visibility: int = Type.TypeDefVisibility.ANY) -> List[str]:
|
|
"""
|
|
Get a list of defined type names.
|
|
|
|
Visibility options:
|
|
Type.TypeDefVisibility.NOTPUBLIC, Type.TypeDefVisibility.PUBLIC,
|
|
Type.TypeDefVisibility.NESTEDPUBLIC, Type.TypeDefVisibility.NESTEDPRIVATE,
|
|
Type.TypeDefVisibility.NESTEDFAMILY, Type.TypeDefVisibility.NESTEDASSEMBLY,
|
|
Type.TypeDefVisibility.NESTEDFAMANDASSEM, Type.TypeDefVisibility.NESTEDFAMORASSEM,
|
|
Type.TypeDefVisibility.ANY
|
|
"""
|
|
result = []
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['TypeDef'].table_rows:
|
|
type_string_address = table_row.string_stream_references['TypeName']
|
|
type_name = self.dotnetpe.get_string(type_string_address)
|
|
|
|
if (visibility & Type.TypeDefVisibility.ANY) == Type.TypeDefVisibility.ANY:
|
|
result.append(type_name)
|
|
continue
|
|
|
|
flag = table_row.Flags.value & Type.TypeDefMask.VISIBILITY
|
|
|
|
if (visibility & Type.TypeDefVisibility.NOTPUBLIC) == Type.TypeDefVisibility.NOTPUBLIC \
|
|
and flag == 0:
|
|
result.append(type_name)
|
|
elif (visibility & Type.TypeDefVisibility.PUBLIC) == Type.TypeDefVisibility.PUBLIC \
|
|
and flag == 1:
|
|
result.append(type_name)
|
|
elif (visibility & Type.TypeDefVisibility.NESTEDPUBLIC) == Type.TypeDefVisibility.NESTEDPUBLIC \
|
|
and flag == 2:
|
|
result.append(type_name)
|
|
elif (visibility & Type.TypeDefVisibility.NESTEDPRIVATE) == Type.TypeDefVisibility.NESTEDPRIVATE \
|
|
and flag == 3:
|
|
result.append(type_name)
|
|
elif (visibility & Type.TypeDefVisibility.NESTEDFAMILY) == Type.TypeDefVisibility.NESTEDFAMILY \
|
|
and flag == 4:
|
|
result.append(type_name)
|
|
elif (visibility & Type.TypeDefVisibility.NESTEDASSEMBLY) == Type.TypeDefVisibility.NESTEDASSEMBLY \
|
|
and flag == 5:
|
|
result.append(type_name)
|
|
elif (visibility & Type.TypeDefVisibility.NESTEDFAMANDASSEM) == Type.TypeDefVisibility.NESTEDFAMANDASSEM \
|
|
and flag == 6:
|
|
result.append(type_name)
|
|
elif (visibility & Type.TypeDefVisibility.NESTEDFAMORASSEM) == Type.TypeDefVisibility.NESTEDFAMORASSEM \
|
|
and flag == 7:
|
|
result.append(type_name)
|
|
|
|
return result
|
|
|
|
def get_type_names_with_methods(self) -> List[Struct.TypesMethods]:
|
|
"""
|
|
Get type names with all corresponding methods.
|
|
"""
|
|
result = []
|
|
|
|
if not self.dotnetpe.metadata_table_exists('MethodDef'):
|
|
self.dotnetpe.logger.debug('Cross-reference error: File does not have a TypeDef or MethodDef table.')
|
|
return result
|
|
|
|
typedef_table_rows = self.dotnetpe.metadata_tables_lookup['TypeDef'].table_rows
|
|
methoddef_table_rows = self.dotnetpe.metadata_tables_lookup['MethodDef'].table_rows
|
|
|
|
current_method_index = 0
|
|
for i, typedef_table_row in enumerate(typedef_table_rows, start=1):
|
|
current_methodlist_count = typedef_table_row.MethodList.value
|
|
try:
|
|
next_methodlist_count = typedef_table_rows[i].MethodList.value
|
|
method_count = next_methodlist_count - current_methodlist_count
|
|
except IndexError:
|
|
method_count = len(methoddef_table_rows) + 1 - current_methodlist_count
|
|
|
|
type_string_address = typedef_table_row.string_stream_references['TypeName']
|
|
type_name = self.dotnetpe.get_string(type_string_address)
|
|
namespace_string_address = typedef_table_row.string_stream_references['TypeNamespace']
|
|
namespace_name = self.dotnetpe.get_string(namespace_string_address)
|
|
type_flags = typedef_table_row.Flags.value
|
|
|
|
if not method_count:
|
|
result.append(Struct.TypesMethods(type_name, namespace_name, [], type_flags))
|
|
continue
|
|
|
|
methods = []
|
|
for j in range(current_method_index, current_method_index + method_count):
|
|
method_string_address = methoddef_table_rows[j].string_stream_references['Name']
|
|
method_name = self.dotnetpe.get_string(method_string_address)
|
|
|
|
method_signature_index = methoddef_table_rows[j].Signature.value
|
|
method_signature = self.dotnetpe.dotnet_blob_lookup[method_signature_index]
|
|
|
|
method_flags = methoddef_table_rows[j].Flags.value
|
|
|
|
methods.append(Struct.Methods(method_name, method_signature.string_representation, method_flags))
|
|
|
|
result.append(Struct.TypesMethods(type_name, namespace_name, methods, type_flags))
|
|
current_method_index += method_count
|
|
|
|
return result
|
|
|
|
|
|
# Table 6
|
|
@metatable
|
|
class MethodDef:
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
def get_method_names(self, method_access: int = Type.MethodDefMemberAccess.ANY) -> List[str]:
|
|
"""
|
|
Get a list of method names.
|
|
|
|
Method access options:
|
|
Type.MethodDefMemberAccess.COMPILERCONTROLLED, Type.MethodDefMemberAccess.PRIVATE,
|
|
Type.MethodDefMemberAccess.FAMANDASSEM, Type.MethodDefMemberAccess.ASSEM,
|
|
Type.MethodDefMemberAccess.FAMILY, Type.MethodDefMemberAccess.FAMORASSEM,
|
|
Type.MethodDefMemberAccess.PUBLIC, Type.MethodDefMemberAccess.ANY
|
|
"""
|
|
result = []
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['MethodDef'].table_rows:
|
|
method_string_address = table_row.string_stream_references['Name']
|
|
method_name = self.dotnetpe.get_string(method_string_address)
|
|
|
|
if (method_access & Type.MethodDefMemberAccess.ANY) == Type.MethodDefMemberAccess.ANY:
|
|
result.append(method_name)
|
|
continue
|
|
|
|
flag = table_row.Flags.value & Type.MethodDefMask.MEMBERACCESS
|
|
|
|
if (method_access & Type.MethodDefMemberAccess.COMPILERCONTROLLED) == Type.MethodDefMemberAccess.COMPILERCONTROLLED \
|
|
and flag == 0:
|
|
result.append(method_name)
|
|
elif (method_access & Type.MethodDefMemberAccess.PRIVATE) == Type.MethodDefMemberAccess.PRIVATE \
|
|
and flag == 1:
|
|
result.append(method_name)
|
|
elif (method_access & Type.MethodDefMemberAccess.FAMANDASSEM) == Type.MethodDefMemberAccess.FAMANDASSEM \
|
|
and flag == 2:
|
|
result.append(method_name)
|
|
elif (method_access & Type.MethodDefMemberAccess.ASSEM) == Type.MethodDefMemberAccess.ASSEM \
|
|
and flag == 3:
|
|
result.append(method_name)
|
|
elif (method_access & Type.MethodDefMemberAccess.FAMILY) == Type.MethodDefMemberAccess.FAMILY \
|
|
and flag == 4:
|
|
result.append(method_name)
|
|
elif (method_access & Type.MethodDefMemberAccess.FAMORASSEM) == Type.MethodDefMemberAccess.FAMORASSEM \
|
|
and flag == 5:
|
|
result.append(method_name)
|
|
elif (method_access & Type.MethodDefMemberAccess.PUBLIC) == Type.MethodDefMemberAccess.PUBLIC \
|
|
and flag == 6:
|
|
result.append(method_name)
|
|
|
|
return result
|
|
|
|
def get_windows_forms_app_entry_point(self) -> List[Optional[Struct.EntryPoint]]:
|
|
"""
|
|
Get entry point of Windows Forms app which is the method with STAThreadAttribute
|
|
"""
|
|
result = []
|
|
|
|
if not self.dotnetpe.metadata_table_exists('CustomAttribute') and not \
|
|
self.dotnetpe.metadata_table_exists('MemberRef') and not \
|
|
self.dotnetpe.metadata_table_exists('MethodDef') and not \
|
|
self.dotnetpe.metadata_table_exists('TypeRef') and not \
|
|
self.dotnetpe.metadata_table_exists('TypeDef'):
|
|
self.dotnetpe.logger.debug('Cross-reference error: File does not have CustomAttribute, MemberRef,'
|
|
' MethodDef, TypeRef and TypeDef tables.')
|
|
return result
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['CustomAttribute'].table_rows:
|
|
if table_row.table_references['Type'][0] == 'MemberRef':
|
|
member_ref_index = table_row.table_references['Type'][1] - 1
|
|
member_ref_parent_table = \
|
|
self.dotnetpe.metadata_tables_lookup['MemberRef'].table_rows[member_ref_index].table_references[
|
|
'Class'][0]
|
|
|
|
if member_ref_parent_table == 'TypeRef':
|
|
typeref_table_index = \
|
|
self.dotnetpe.metadata_tables_lookup['MemberRef'].table_rows[member_ref_index].table_references[
|
|
'Class'][1] - 1
|
|
type_name_address = self.dotnetpe.metadata_tables_lookup['TypeRef'].table_rows[
|
|
typeref_table_index].string_stream_references['TypeName']
|
|
|
|
if self.dotnetpe.get_string(type_name_address) == 'STAThreadAttribute':
|
|
parent_table = table_row.table_references['Parent'][0]
|
|
|
|
if parent_table == 'MethodDef':
|
|
methoddef_table_index = table_row.table_references['Parent'][1] - 1
|
|
method_string_address = self.dotnetpe.metadata_tables_lookup['MethodDef'].table_rows[
|
|
methoddef_table_index].string_stream_references['Name']
|
|
method_name = self.dotnetpe.get_string(method_string_address)
|
|
|
|
method_signature_index = self.dotnetpe.metadata_tables_lookup['MethodDef'].table_rows[
|
|
methoddef_table_index].blob_stream_references['Signature']
|
|
method_signature = self.dotnetpe.dotnet_blob_lookup[method_signature_index]
|
|
|
|
types_with_methods = self.dotnetpe.TypeDef.get_type_names_with_methods()
|
|
|
|
for type_with_method in types_with_methods:
|
|
for method in type_with_method.Methods:
|
|
if method.Name == method_name and \
|
|
method.Signature == method_signature.string_representation:
|
|
result.append(
|
|
Struct.EntryPoint(method_name, method_signature.string_representation,
|
|
type_with_method.Type, type_with_method.Namespace))
|
|
|
|
return result
|
|
|
|
def get_entry_points(self) -> List[Optional[Struct.EntryPoint]]:
|
|
"""
|
|
Get possible entry points along with their types, namespaces and parameters.
|
|
|
|
TODO: Add more blob signatures or create proper blob signature decoding
|
|
"""
|
|
result = []
|
|
|
|
if self.dotnetpe.is_windows_forms_app():
|
|
return self.get_windows_forms_app_entry_point()
|
|
|
|
all_types = self.dotnetpe.TypeDef.get_type_names_with_methods()
|
|
|
|
for general_type in all_types:
|
|
type_flag = general_type.Flags & Type.TypeDefMask.VISIBILITY
|
|
|
|
# Check if type is public (1) or nested public (2) and contains methods
|
|
if (type_flag == 1 or type_flag == 2) and general_type.Methods:
|
|
for method in general_type.Methods:
|
|
# Check if MemberAccess is public
|
|
if method.Flags & Type.MethodDefMask.MEMBERACCESS == 6:
|
|
if method.Name not in ['.ctor', '.cctor']:
|
|
if not ((method.Flags & Type.MethodDefMask.SPECIALNAME == Type.MethodDefMask.SPECIALNAME and
|
|
method.Name.startswith(("set_", "get_", "add_", "remove_"))) or
|
|
(method.Flags & Type.MethodDefMask.VIRTUAL == Type.MethodDefMask.VIRTUAL and
|
|
method.Name in ['Invoke', 'BeginInvoke', 'EndInvoke'])):
|
|
result.append(Struct.EntryPoint(method.Name, method.Signature, general_type.Type,
|
|
general_type.Namespace))
|
|
|
|
return result
|
|
|
|
|
|
# Table 10
|
|
@metatable
|
|
class MemberRef:
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
def get_memberref_names(self, deduplicate: bool = False) -> List[str]:
|
|
"""
|
|
Get a list of reference names to Methods and Field of a class.
|
|
"""
|
|
result = []
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['MemberRef'].table_rows:
|
|
memberref_string_address = table_row.string_stream_references['Name']
|
|
memberref_name = self.dotnetpe.get_string(memberref_string_address)
|
|
|
|
result.append(memberref_name)
|
|
|
|
if deduplicate:
|
|
result = list(set(result))
|
|
|
|
return result
|
|
|
|
def get_memberref_hash(self, hash_type: Type.Hash = Type.Hash.SHA256, strings_sorted: bool = False) -> str:
|
|
"""
|
|
MemberRefHash: Get hash of reference (to methods and fields of a class) and table names of their
|
|
corresponding classes.
|
|
|
|
We take the table name of the belonging class and the reference name to create a string pair separated by a "-"
|
|
character. This table<->name string pair is added to a list and the hash value is calculated at the end. The
|
|
class always belongs to one of the five tables 'TypeDef', 'TypeRef', 'ModuleRef', 'MethodDef' or 'TypeSpec'.
|
|
|
|
The list of table and reference names can be sorted alphabetically after the reference names before being
|
|
hashed. Strings are used case-sensitive.
|
|
|
|
Options:
|
|
- Type.Hash.MD5, Type.Hash.SHA1, Type.Hash.SHA256
|
|
- Strings unsorted or sorted (after the reference names)
|
|
"""
|
|
tables_names = []
|
|
for current_row_index, table_row in enumerate(self.dotnetpe.metadata_tables_lookup['MemberRef'].table_rows):
|
|
name_string_address = table_row.string_stream_references['Name']
|
|
name_string = self.dotnetpe.get_string(name_string_address)
|
|
|
|
class_table = table_row.table_references['Class'][0]
|
|
|
|
tables_names.append(f'{class_table}-{name_string}')
|
|
|
|
if strings_sorted:
|
|
tables_names.sort(key=lambda x: x.split('-')[1])
|
|
|
|
return self.dotnetpe.get_hash(hash_type, tables_names)
|
|
|
|
|
|
# Table 20
|
|
@metatable
|
|
class Event:
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
def get_event_names(self) -> List[str]:
|
|
"""
|
|
Get a list of event names.
|
|
"""
|
|
result = []
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['Event'].table_rows:
|
|
event_string_address = table_row.string_stream_references['Name']
|
|
event_name = self.dotnetpe.get_string(event_string_address)
|
|
result.append(event_name)
|
|
|
|
return result
|
|
|
|
|
|
# Table 26
|
|
@metatable
|
|
class ModuleRef:
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
def get_unmanaged_module_names(self, modules_type: Type.UnmanagedModules = Type.UnmanagedModules.RAW) -> List[str]:
|
|
"""
|
|
Get a list of unmanaged module names.
|
|
|
|
Type options:
|
|
Type.UnmanagedModules.RAW, Type.UnmanagedModules.NORMALIZED
|
|
"""
|
|
result = []
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['ModuleRef'].table_rows:
|
|
string_address = table_row.string_stream_references['Name']
|
|
module_name = self.dotnetpe.get_string(string_address)
|
|
|
|
# Condition for mixed assemblies
|
|
if module_name:
|
|
if modules_type == Type.UnmanagedModules.RAW:
|
|
result.append(module_name)
|
|
elif modules_type == Type.UnmanagedModules.NORMALIZED:
|
|
module_name = module_name.lower()
|
|
if not module_name.endswith('.dll'):
|
|
module_name = f'{module_name}.dll'
|
|
if module_name not in result:
|
|
result.append(module_name)
|
|
|
|
return result
|
|
|
|
|
|
# Table 28
|
|
@metatable
|
|
class ImplMap:
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
def _get_function_name_from_methoddef_table(self, table_index: int) -> str:
|
|
result = ''
|
|
|
|
if not self.dotnetpe.metadata_table_exists('MethodDef'):
|
|
self.dotnetpe.logger.debug('Cross-reference error: File does not have an MethodDef table.')
|
|
return result
|
|
|
|
string_address = self.dotnetpe.metadata_tables_lookup['MethodDef'].table_rows[
|
|
table_index].string_stream_references['Name']
|
|
result = self.dotnetpe.get_string(string_address)
|
|
|
|
return result
|
|
|
|
def get_unmanaged_functions(self) -> List[str]:
|
|
"""
|
|
Get a list of unmanaged function names.
|
|
"""
|
|
result = []
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['ImplMap'].table_rows:
|
|
string_address = table_row.string_stream_references['ImportName']
|
|
function_name = self.dotnetpe.get_string(string_address)
|
|
|
|
# Handle mixed assemblies by getting the function name from the MethodDef table
|
|
if not function_name:
|
|
function_name = self._get_function_name_from_methoddef_table(
|
|
table_row.table_references['MemberForwarded'][1] - 1)
|
|
|
|
result.append(function_name)
|
|
|
|
return result
|
|
|
|
def get_platform_invoke_information(self, with_mapping_flags: bool = False) -> List[str]:
|
|
result = []
|
|
|
|
if not self.dotnetpe.metadata_table_exists('ModuleRef'):
|
|
self.dotnetpe.logger.debug('Cross-reference error: File does not have a ModuleRef table.')
|
|
return result
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['ImplMap'].table_rows:
|
|
function_string_address = table_row.string_stream_references['ImportName']
|
|
function_name = self.dotnetpe.get_string(function_string_address)
|
|
|
|
# Handle mixed assembly ImplMap entries by getting the function name from the MethodDef table and the
|
|
# library name from the PE header
|
|
module_name = ''
|
|
if not function_name:
|
|
function_name = self._get_function_name_from_methoddef_table(
|
|
table_row.table_references['MemberForwarded'][1] - 1)
|
|
|
|
# Handle native images created by Ngen as they don't have an import address table anymore
|
|
if hasattr(self.dotnetpe, 'DIRECTORY_ENTRY_IMPORT'):
|
|
for dll_import in self.dotnetpe.DIRECTORY_ENTRY_IMPORT:
|
|
for function_import in dll_import.imports:
|
|
if function_import.name:
|
|
if function_import.name.decode() == function_name:
|
|
module_name = dll_import.dll.decode()
|
|
break
|
|
else:
|
|
import_scope = table_row.ImportScope.value
|
|
module_string_address = self.dotnetpe.metadata_tables_lookup['ModuleRef'].table_rows[
|
|
import_scope - 1].string_stream_references['Name']
|
|
module_name = self.dotnetpe.get_string(module_string_address)
|
|
|
|
function_name = function_name.lower()
|
|
module_name = module_name.lower()
|
|
|
|
if module_name.endswith('.dll'):
|
|
module_name = module_name[:-4]
|
|
|
|
if with_mapping_flags:
|
|
mapping_flags = table_row.MappingFlags.value
|
|
result.append(f'{module_name}.{function_name}.{mapping_flags}')
|
|
else:
|
|
result.append(f'{module_name}.{function_name}')
|
|
|
|
return result
|
|
|
|
|
|
# Table 32
|
|
@metatable
|
|
class Assembly:
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
def get_assembly_name(self) -> str:
|
|
"""
|
|
Get assembly name.
|
|
"""
|
|
# To counteract .NET protectors like ConfuserEx that add additional entries, we skip the remaining row(s) as
|
|
# they are officially not supported and contain invalid entries
|
|
string_address = self.dotnetpe.metadata_tables_lookup['Assembly'].table_rows[0].string_stream_references['Name']
|
|
|
|
return self.dotnetpe.get_string(string_address)
|
|
|
|
def get_assembly_culture(self) -> str:
|
|
"""
|
|
Get assembly culture.
|
|
"""
|
|
# To counteract .NET protectors like ConfuserEx that add additional entries, we skip the remaining row(s) as
|
|
# they are officially not supported and contain invalid entries
|
|
string_address = self.dotnetpe.metadata_tables_lookup['Assembly'].table_rows[0].string_stream_references[
|
|
'Culture']
|
|
|
|
return self.dotnetpe.get_string(string_address)
|
|
|
|
def get_assembly_version_information(self) -> Optional[Struct.AssemblyInfo]:
|
|
"""
|
|
Get assembly version information (MajorVersion, MinorVersion, BuildNumber, RevisionNumber).
|
|
"""
|
|
result = None
|
|
|
|
try:
|
|
assembly = self.dotnetpe.metadata_tables_lookup['Assembly'].table_rows[0]
|
|
result = Struct.AssemblyInfo(assembly.MajorVersion.value, assembly.MinorVersion.value,
|
|
assembly.BuildNumber.value, assembly.RevisionNumber.value)
|
|
except (IndexError, AttributeError, KeyError):
|
|
pass
|
|
|
|
return result
|
|
|
|
|
|
# Table 35
|
|
@metatable
|
|
class AssemblyRef:
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
def get_assemblyref_names(self, deduplicate: bool = False) -> List[str]:
|
|
"""
|
|
Get a list of referenced assembly names.
|
|
"""
|
|
result = []
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['AssemblyRef'].table_rows:
|
|
string_address = table_row.string_stream_references['Name']
|
|
if string_address:
|
|
assembly_name = self.dotnetpe.get_string(string_address)
|
|
result.append(assembly_name)
|
|
|
|
if deduplicate:
|
|
result = list(set(result))
|
|
|
|
return result
|
|
|
|
def get_assemblyref_cultures(self) -> List[str]:
|
|
"""
|
|
Get a list of referenced assembly cultures.
|
|
"""
|
|
result = []
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['AssemblyRef'].table_rows:
|
|
string_address = table_row.string_stream_references['Culture']
|
|
if string_address:
|
|
result.append(self.dotnetpe.get_string(string_address))
|
|
|
|
return result
|
|
|
|
|
|
# Table 40
|
|
@metatable
|
|
class ManifestResource:
|
|
def __init__(self, dotnet_obj):
|
|
self.dotnetpe = dotnet_obj
|
|
|
|
def get_resource_names(self) -> List[str]:
|
|
"""
|
|
Get a list of .NET resource names.
|
|
"""
|
|
result = []
|
|
|
|
for table_row in self.dotnetpe.metadata_tables_lookup['ManifestResource'].table_rows:
|
|
string_address = table_row.string_stream_references['Name']
|
|
if string_address:
|
|
result.append(self.dotnetpe.get_string(string_address))
|
|
|
|
return result
|