# Anti metadata

Contains various properties for anti-parsing tricks that .NET obfuscators like ConfuserEx use to confuse a PE viewer/parser.

# .NET data directory hidden

AntiMetadataAnalysis.is_dotnet_data_directory_hidden

Checks if the .NET data directory is hidden in the PE header. This is an old trick to confuse PE viewers/parsers (e.g. pefile) to properly analyze an assembly.

Usually, the value of NumberOfRvaAndSizes in the IMAGE_OPTIONAL_HEADER structure of a PE file is 0x10 (16). This value describes how many entries are in the following IMAGE_DATA_DIRECTORY structure. The last two entries are Reserved (16) and DIRECTORY_ENTRY_COM_DESCRIPTOR (15) that is the .NET data directory which contains the CLR header address and size. When you lower the NumberOfRvaAndSizes value to 0xE (14) to "hide" the last two entries in the data directories, you can confuse some PE viewer/parser and still have an executable .NET assembly.

Parameters:

-

Return value:

True or False

Example:

# Import class DotNetPE from module dotnetfile
from dotnetfile import DotNetPE

# Create an instance of DotNetPE with the file path as a parameter
dotnet_file = DotNetPE('/Users/<username>/my_dotnet_assembly.exe')

# Print out if .NET data directory is hidden or not
print(f'.NET data directory is hidden in PE header: {dotnet_file.AntiMetadataAnalysis.is_dotnet_data_directory_hidden}')

# Table header uses ExtraData

AntiMetadataAnalysis.has_metadata_table_extra_data

Checks if the tables header in the #~ or #- stream uses the undocumented ExtraData field. Usually, the end of the Tables header is made of row count fields of the existing tables. However, there is an undocumented field ExtraData that can be appended at the end of this table. The purpose of these 4 extra bytes is unknown and also why the CLR loader allows them to be present.

Parameters:

-

Return value:

True or False

Example:

# Import class DotNetPE from module dotnetfile
from dotnetfile import DotNetPE

# Create an instance of DotNetPE with the file path as a parameter
dotnet_file = DotNetPE('/Users/<username>/my_dotnet_assembly.exe')

# Print out if ExtraData field is used in .NET tables header
print(f'Has extra data at the end of the metadata table header: {dotnet_file.AntiMetadataAnalysis.has_metadata_table_extra_data}')

# TypeRef table has self-referencing entries

AntiMetadataAnalysis.has_self_referenced_typeref_entries

Entries in the TypeRef table have a field named ResolutionScope that is a token. It encodes in which table the target type is defined and what index it has in the table. This token can also point to the TypeRef table itself. Obfuscators use this to add bogus entries that reference each other in the ResolutionScope field.

Parameters:

-

Return value:

True or False

Example:

# Import class DotNetPE from module dotnetfile
from dotnetfile import DotNetPE

# Create an instance of DotNetPE with the file path as a parameter
dotnet_file = DotNetPE('/Users/<username>/my_dotnet_assembly.exe')

# Print out if TypeRef table contains self-referencing entries
print(f'Has bogus TypeRef entries that reference each other: {dotnet_file.AntiMetadataAnalysis.has_self_referenced_typeref_entries}')

# TypeRef table has invalid entries

AntiMetadataAnalysis.has_invalid_typeref_entries

Entries in the TypeRef table have a field named ResolutionScope that is a token. It encodes in which table the target type is defined and what index it has in the table. This token can also point to the TypeRef table itself with an index that isn't available. This is abused by some obfuscators as the CLR loader doesn't check if the index exists. Furthermore, entries in the TypeRef table have a field named TypeName. It is an index to a string in the #Strings stream that is set to 0 by some obfuscators. Technically, this is correct as the #Streams stream always starts with an index 0. However, this index never contains a string, and thus it doesn't make sense to reference it.

Parameters:

-

Return value:

True or False

Example:

# Import class DotNetPE from module dotnetfile
from dotnetfile import DotNetPE

# Create an instance of DotNetPE with the file path as a parameter
dotnet_file = DotNetPE('/Users/<username>/my_dotnet_assembly.exe')

# Print out if TypeRef table contains invalid entries
print(f'Has invalid TypeRef table entries: {dotnet_file.AntiMetadataAnalysis.has_invalid_typeref_entries}')

# .NET header has fake data streams

AntiMetadataAnalysis.has_fake_data_streams

Checks if a .NET assembly has fake data streams. Usually, an assembly has only the following streams:

  • #~ or #-
  • #Strings
  • #US
  • #GUID
  • #Blob

The CLR loader ignores any stream that is named differently and only parses the first occurring stream for one of the names above. Obfuscators sometimes add additional data streams with fake entries. These are either named similar to the original ones and placed before them. Or they're named identically, but follow afterward.

Parameters:

-

Return value:

True or False

Example:

# Import class DotNetPE from module dotnetfile
from dotnetfile import DotNetPE

# Create an instance of DotNetPE with the file path as a parameter
dotnet_file = DotNetPE('/Users/<username>/my_dotnet_assembly.exe')

# Print out if assembly has fake data streams
print(f'Has fake data streams: {dotnet_file.AntiMetadataAnalysis.has_fake_data_streams}')

# Module table has multiple rows

AntiMetadataAnalysis.module_table_has_multiple_rows

According to the .NET specification, the Module table should contain only one row. However, obfuscators sometimes add an additional row with invalid entries. That's possible as the CLR loader doesn't validate if there's only one row present.

Parameters:

-

Return value:

True or False

Example:

# Import class DotNetPE from module dotnetfile
from dotnetfile import DotNetPE

# Create an instance of DotNetPE with the file path as a parameter
dotnet_file = DotNetPE('/Users/<username>/my_dotnet_assembly.exe')

# Print out if assembly has more than one row in the Module table
print(f'Has more than one row in Module table: {dotnet_file.AntiMetadataAnalysis.module_table_has_multiple_rows}')

# Assembly table has multiple rows

AntiMetadataAnalysis.assembly_table_has_multiple_rows

According to the .NET specification, the Assembly table should contain only one row. However, obfuscators sometimes add an additional row with invalid entries. That's possible as the CLR loader doesn't validate if there's only one row present.

Parameters:

-

Return value:

True or False

Example:

# Import class DotNetPE from module dotnetfile
from dotnetfile import DotNetPE

# Create an instance of DotNetPE with the file path as a parameter
dotnet_file = DotNetPE('/Users/<username>/my_dotnet_assembly.exe')

# Print out if assembly has more than one row in the Assembly table
print(f'Has more than one row in Assembly table: {dotnet_file.AntiMetadataAnalysis.assembly_table_has_multiple_rows}')

# #Strings stream has invalid entries

AntiMetadataAnalysis.has_invalid_strings_stream_entries

Obfuscators sometimes add a row to the #Strings stream that has an invalid string reference. This points not to an actual string in the file, but to nowhere as the value is invalid. For example, the last row in the #Strings stream contains the value 0x7fff7fff as a reference which points to outside the file.

Parameters:

-

Return value:

True or False

Example:

# Import class DotNetPE from module dotnetfile
from dotnetfile import DotNetPE

# Create an instance of DotNetPE with the file path as a parameter
dotnet_file = DotNetPE('/Users/<username>/my_dotnet_assembly.exe')

# Print out if assembly has invalid entries in the #Strings stream
print(f'Has invalid entries in #Strings stream: {dotnet_file.AntiMetadataAnalysis.has_invalid_strings_stream_entries}')

# Stream names are mixed-case

AntiMetadataAnalysis.has_mixed_case_stream_names

In the specification and in compiler generated assemblies, those stream names with characters are only shown in one notation that is #Strings, #US, #GUID, #Blob and so forth. This creates the impression that the names have to have this notation while they can actually have mixed-case characters at will. For example, an assembly can have the stream names #stRinGs and #bloB and still run fine. This trick is used by some obfuscators to confuse a parser.

Parameters:

-

Return value:

True or False

Example:

# Import class DotNetPE from module dotnetfile
from dotnetfile import DotNetPE

# Create an instance of DotNetPE with the file path as a parameter
dotnet_file = DotNetPE('/Users/<username>/my_dotnet_assembly.exe')

# Print out if assembly has mixed-case stream names
print(f'Has mixed case stream name(s): {dotnet_file.AntiMetadataAnalysis.has_mixed_case_stream_names}')

# MethodDef table has invalid entries

AntiMetadataAnalysis.has_invalid_methoddef_entries

This property checks if there are rows in the MethodDef table with the columns RVA, Flags, Name and Signature set to 0. While it's possible that some of these fields could have this value, all of them being set to zero indicates a bogus method added by some obfuscators.

Parameters:

-

Return value:

True or False

Example:

# Import class DotNetPE from module dotnetfile
from dotnetfile import DotNetPE

# Create an instance of DotNetPE with the file path as a parameter
dotnet_file = DotNetPE('/Users/<username>/my_dotnet_assembly.exe')

# Print out if assembly has invalid entries in MethodDef table
print(f'Has invalid entries in MethodDef table: {dotnet_file.AntiMetadataAnalysis.has_invalid_methoddef_entries}')

# #Strings stream has maximum length exceeding strings

AntiMetadataAnalysis.has_max_len_exceeding_strings

While there is no maximum length of strings in the #Strings stream defined in the .NET specification, it is effectively set to 1024 characters in the Roslyn compiler. Some obfuscators inject overly long nonsense strings to confuse parsers.

Parameters:

-

Return value:

True or False

Example:

# Import class DotNetPE from module dotnetfile
from dotnetfile import DotNetPE

# Create an instance of DotNetPE with the file path as a parameter
dotnet_file = DotNetPE('/Users/<username>/my_dotnet_assembly.exe')

# Print out if assembly has maximum length exceeding strings
print(f'Has maximum length exceeding string(s): {dotnet_file.AntiMetadataAnalysis.has_max_len_exceeding_strings}')