This is a series of code snippets that I have found useful when developing configuration extractors for dotnet-based malware.

Here are some examples where I have applied these snippets.

This page would not exist without the work of these folk. Much of this work is based on their blogs and scripts.

Enumerate DotNet File For Call Instruction

This snippet allows you to enumerate a dotnet exe/dll and locate all call instructions. You can replace the OpCodes.Call with any instruction of your choosing.

Here is a list of all available instructions.

import clr
clr.AddReference("dnlib.dll")

import dnlib
from dnlib.DotNet import *
from dnlib.DotNet.Emit import OpCodes

filename = "yourfile.exe"
    
module = ModuleDefMD.Load(filename)
for types in module.GetTypes():
    for method in types.Methods:
        if method.HasBody:
            for instr in method.Body.Instructions:
                if instr.OpCode == OpCodes.Call:
                    result = "Something interesting" 

Enumerate Method Signatures

This snippet allows you to hone in on specific functions/methods within a dotnet binary.

This particular snippet locates the first method/function that takes an integer value as an argument, and then returns a string.

Note that this uses the System.Reflection library and not the dnlib library as in other examples.

import clr
clr.AddReference("System")

module = System.Reflection.Assembly.LoadFile(filename)   

for types in module.GetTypes():
    for method in types.GetMethods():
        params = method.GetParameters()
            if params[0].ParameterType.FullName == "System.Int32":
                if method.ReturnType.FullName == "System.String":  
                    return method

Enumerating Method Instructions For a Known Pattern


This snippet enumerates a method for a known IL instruction pattern.


def has_config_sequence(method):
    target_sequence = [OpCodes.Stsfld,OpCodes.Ldc_I4,OpCodes.Br_S,OpCodes.Call,OpCodes.Stsfld,OpCodes.Ret]
    
    target_len = len(target_sequence)
    for i in range(target_len):
        target_sequence[i] = target_sequence[i].Name

    if method.HasBody:
        current_sequence = [instr.OpCode.Name for instr in method.Body.Instructions]
        for i in range(1, target_len-1):
            if target_sequence[-i] != current_sequence[-i]:
                return False
    return True

Invoke a Static Method

This snippet invokes a static method with a single integer argument. This assumes that you have already located a method using something similar to the previous snippet.

import clr
clr.AddReference("System")

python_int = 55
dotnet_int = System.Int32(python_int)
module = <module found with previous snippet>
result = module.Invoke(None, (dotnet_int,))

Execute/Invoke a Method Via Metadata Token

A few notes

  • Python Integers must be converted to "System.Int32" integers or you will have issues.
  • This snippet resolves a method via it's metadata token, if you have resolved a method using the previous snippet, you can call Invoke directly on the returned value.
  • If the method you are invoking is static, then the first argument to Invoke can be None.
  • For some reason arguments must be passed as an array, hence the (dotnet_int,) rather than just dotnet_int
import clr
clr.AddReference("System")

python_int = 55
dotnet_int = System.Int32(python_int)
token = 0x06000005
method = module.ManifestModule.ResolveMethod(token)
result = method.Invoke(None, (dotnet_int,))

Invoke a Generic Method

Invokes a static generic method that takes a single integer value as an argument.

  • Assumes that you are expecting a string as a return value.
  • Assumes the method is Static
  • Assumes one integer argument is expected
import clr
clr.AddReference("System")   
   
method = <some method you have found using previous snippets>
concrete_method_str = method.MakeGenericMethod(clr.GetClrType(str))
python_int = 55
dotnet_int = System.Int32(python_int)
result = concrete_method_str.Invoke(None, (dotnet_int,))

Patch Anti-Debug Instructions with NOP Values

This snippet is a bit messy but possibly useful for someone.

It was used to patch all calls to GetExecutingAssembly and GetCallingAssembly which were followed by a Brfalse or Callvirt instruction.

This was a specific pattern used in a protector (I believe ConfuserEx) to prevent Invoke calls. By patching the calls and surrounding instructions, the decryption methods could be invoked without failing the GetExecutingAssembly == GetCallingAssembly anti-debug check.

import clr
clr.AddReference("dnlib.dll")

import dnlib
from dnlib.DotNet import *
from dnlib.DotNet.Emit import OpCodes

filename = "yourfile.exe"
    
module = ModuleDefMD.Load(filename)

def nop_executingAssembly(module):

    nop_methods = ["GetExecutingAssembly","GetCallingAssembly"]
    
    for types in module.GetTypes():
        for method in types.Methods:
            if method.HasBody:

                for i in range(len(method.Body.Instructions)):
                    if method.Body.Instructions[i].OpCode == OpCodes.Call:
                        for nop_method in nop_methods:
                            if nop_method in str(method.Body.Instructions[i]):
                                method.Body.Instructions[i].OpCode = OpCodes.Nop
                                
                                
                                for j in range(1,4):
                                    if method.Body.Instructions[i+j].OpCode == OpCodes.Callvirt:
                                        method.Body.Instructions[i+j].OpCode = OpCodes.Nop
                                        
                                    if method.Body.Instructions[i+j].OpCode == OpCodes.Brfalse:
                                        method.Body.Instructions[i+j].OpCode = OpCodes.Nop
                                        
    return dn_module

Argument Parsing

Obtain arguments from command line using flags/markers rather than the specific order.

This allows you to use args.file instead of sys.argv[1] and so on.

import argparse

parser =  argparse.ArgumentParser(description="Extract config from AgentTesla Payloads")
parser.add_argument('-f', '--file', required=False, help="Path to agentTesla file")
parser.add_argument('-d', '--dnlib', required=False, default="dnlib.dll",help="Path to dnlib file")
parser.add_argument('--allstrings', required=False, default=False, help="print all strings for files, not just c2")

args = parser.parse_args()