#
Anti metadata
Contains various properties for anti-parsing tricks that .NET obfuscators like ConfuserEx use to confuse a PE viewer/parser.
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.
Malware example (SHA-256):
1ef2be748b6798c547db0753918bb3e1ddbbc9e0b66b71c5a3eed1b04a8ed556
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.
Malware example (SHA-256):
ba0b2518a0073173f4940923af6e235eb8c392283903053d55e5dd31236a3b3d
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.
Malware example (SHA-256):
6f30280da560a30bbe25835f00d881dad94e5a7641b63e028080628c1f82a992
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.
Malware example (SHA-256):
6f30280da560a30bbe25835f00d881dad94e5a7641b63e028080628c1f82a992
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.
Malware example (SHA-256):
6f30280da560a30bbe25835f00d881dad94e5a7641b63e028080628c1f82a992
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.
Malware example (SHA-256):
c64c9c62ec56ffded65ecf8585f98fd96e72027b0747b33dffc1e7b07065015a
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.
Malware example (SHA-256):
c64c9c62ec56ffded65ecf8585f98fd96e72027b0747b33dffc1e7b07065015a
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.
Malware example (SHA-256):
b1f26dce54fcdcf40fa5c65705698f81b23486d5adc4e4a035a07d074ce8e64d
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.
Malware example (SHA-256):
b65f40618f584303ca0bcf9b5f88c233cc4237699c0c4bf40ba8facbe8195a46
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.
Malware example (SHA-256):
55bcf947907068874f18870c331432b0336f0aa4821e992395790e5f3523b0d3
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.
Malware example (SHA-256):
9621a729349b6c5df925ad8e7695b92bcec7c842cc9f01bd22cd75a57599cdd2
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}')