Dynamically editing assembly information for C# executables

I've been building numerous utilities using C# for my red team engagements since last year. I normally use execute-assembly to run them and never had to worry about any identifiers or static signatures for these utilities. Until I built a run-of-the-mill implant/loader!

Since I was now directly running these executables and dealing AV/EDR vendors slurping them up from client environments, I wanted a way to randomize assembly details, namespaces, etc. in a quest to make every implant I generate as unique as possible and reduce static identifiers.

I started by just dynamically generating AssemblyInfo.cs files and putting the implants through EAZFuscator. However, this still left a few possible identifiers behind and didn't necessarily work for 3rd party binaries without some legwork.

Since we can use dnlib to dynamically edit C# executables, I figured it wouldn't be too difficult to change assembly info. After much googling and hitting against a few walls, I found what I was looking for in the ConfuserEx source code.

Contents

Editing values in AssemblyInfo.cs

When you define values in AssemblyInfo.cs, you are actually adding specific custom attributes on the executable.

// Below definitions
using System.Reflection;
using System.Runtime.InteropServices;

[assembly: AssemblyTitle("innocent title")]
[assembly: AssemblyDescription("my innocent application is innocent")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("innocent corp, inc.")]
[assembly: AssemblyProduct("innocent product")]
[assembly: AssemblyCopyright("Copyright © 2022")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("378103bd-1be6-4af6-a07a-223fbaf37b4a")]
[assembly: AssemblyVersion("0.1.0.1")]
[assembly: AssemblyFileVersion("0.1.0.1")]

// become this collection of attributes when compiled.
[AssemblyTitleAttribute("innocent title")]
[AssemblyDescriptionAttribute("my innocent application is innocent")]
[AssemblyConfigurationAttribute("")]
[AssemblyCompanyAttribute("innocent corp, inc.")]
[AssemblyProductAttribute("innocent product")]
[AssemblyCopyrightAttribute("Copyright © 2022")]
[AssemblyTrademarkAttribute("")]
[AssemblyCultureAttribute("")]
[ComVisibleAttribute(false)]
[GuidAttribute("378103bd-1be6-4af6-a07a-223fbaf37b4a")]
[AssemblyVersionAttribute("0.1.0.1")]
[AssemblyFileVersionAttribute("0.1.0.1")]

You can access these attributes under module.Assembly.CustomAttributes or assembly.CustomAttributes when using dnlib or System.Reflection.Assembly respectively. So, a simple way to overwrite these attributes would be to loop through them, figure out if we are interested in the specific attribute, and if so change its value.

using System.Reflection;
using dnlib.DotNet;

// Load the contents of our executable
var contents = File.ReadAllBytes("not-malicious.exe");

// Create a new module context
var modCtx = ModuleDef.CreateModuleContext();

// Load our module with dnlib
var module = ModuleDefMD.Load(contents, modCtx);

// Define custom attributes we are interested in changing
var customAttributes = new Dictionary<string, string> {
  {nameof(GuidAttribute), "32472274-6f83-47db-9935-d6631d60ff06"},
  {nameof(AssemblyTitleAttribute), "rubbery duck"},
  {nameof(AssemblyDescriptionAttribute), "a duck that is very rubbery"},
  {nameof(AssemblyCompanyAttribute), "Slippery Toys, Inc."},
  {nameof(AssemblyCopyrightAttribute), "© Microsoft 2022"},
  {nameof(AssemblyTrademarkAttribute), "Slippery Toys, Inc."},
};

// Filter `module.CustomAttributes` for only the attributes we are interested in and loop through them.
foreach (var attribute in module.CustomAttributes.Where(attribute => customAttributes.ContainsKey(attribute.AttributeType.Name)))
{
  // The value for each attribute is set a an argument in their constructor, 
  //   so we need to replace the existing argument value with our value.
  // We are using `module.CorLibTypes.String` since we are only interested in patching strings, 
  //   but attributes can have different value types, too.
  attribute.ConstructorArguments[0] = new CAArgument(module.CorLibTypes.String, customAttributes[attribute.AttributeType.Name]);
}

// Filter `module.Assembly.CustomAttributes` for only the attributes we are interested in and loop through them.
foreach (var attribute in module.Assembly.CustomAttributes.Where(attribute => customAttributes.ContainsKey(attribute.AttributeType.Name)))
{
  // Same as above
  attribute.ConstructorArguments[0] = new CAArgument(module.CorLibTypes.String, customAttributes[attribute.AttributeType.Name]);
}

The above code first loads the assembly using dnlib, then loops through the existing custom attributes to replace any values we are interested in changing. We won't add an attribute that doesn't already exist, but if you wanted to, you could do it like below.

using dnlib.DotNet;

// Load the contents of our executable
var contents = File.ReadAllBytes("not-malicious.exe");

// Create a new module context
var modCtx = ModuleDef.CreateModuleContext();

// Load our module with dnlib
var module = ModuleDefMD.Load(contents, modCtx);

// Create System.Reflection.AssemblyTitleAttribute type reference
var attributeRef = module.CorLibTypes.GetTypeRef("System.Reflection", "AssemblyTitleAttribute");

// Create 'void System.Reflection.AssemblyTitleAttribute::.ctor(string)' method reference
var ctor = new MemberRefUser(
    module,
    // Default constructor method is named `.ctor`
    ".ctor",
    // This is a method that accepts a string, creates an instance, and returns nothing (void)
    MethodSig.CreateInstance(module.CorLibTypes.Void, module.CorLibTypes.String),
    // Reference to the type for which we need the constructor
    attributeRef);

// Create a new `CustomAttribute` instance
var attr = new CustomAttribute(ctor);

// Add our constructor argument
attr.ConstructorArguments.Add(new CAArgument(module.CorLibTypes.String, "RubberDuck"));

// Add the custom attribute to the assembly
// This will not replace an already existing `AssemblyTitle` attribute.
// It would be better to check if the attribute already exists and replace the constructor value,
// and only add it as an additional attribute if it doesn't already exist. 
module.Assembly.CustomAttributes.Add(attr);

Editing Assembly Name and Version

Now that we've changed some attributes above, let's check how it looks like in dnSpy.

A patched Levinas.dll in dnSpy

All the assembly values we've changed above are as expected, but what is this? The assembly is still named Levinas (just a random name itself 😉) and the filename is Levinas.dll even though this is an executable named NotMalicious.exe.

C# assemblies actually keep a reference to the original assembly and file names, so even if you rename a file, you'll still be leaving the original name for forensics to find. It's really easy to change this as well as the version information using dnlib, so let's do it!

using System.Reflection;
using dnlib.DotNet;

// Find an innocent looking name
var name = "WinTelemetryCli"; 

// Replace the module name
module.Name = name;

// Replace the assembly name
module.Assembly.Name = name;

// Make a list of name related attributes we might want to overwrite
var nameRelatedAttributes = new List<string>
{
    nameof(AssemblyTitleAttribute),
    nameof(AssemblyProductAttribute),
};

// As with the attribute patching above, get a list of existing attributes that matches our list and overwrite them for the module
foreach (var attribute in module.CustomAttributes.Where(attribute => nameRelatedAttributes.Contains(attribute.AttributeType.Name)))
{
    attribute.ConstructorArguments[0] = new CAArgument(module.CorLibTypes.String, new UTF8String(name));
}

// And the same for the assembly
foreach (var attribute in module.Assembly.CustomAttributes.Where(attribute => nameRelatedAttributes.Contains(attribute.AttributeType.Name)))
{
    attribute.ConstructorArguments[0] = new CAArgument(module.CorLibTypes.String, new UTF8String(name));
}

// Now, let's do the same for version.
// Generate a random looking version
var version = "1.18.0.0"; 

// Replace the assembly version
module.Assembly.Version = new Version(version);

// Make a list of version related attributes we might want to overwrite
var versionRelatedAttributes = new List<string>
{
    nameof(AssemblyVersionAttribute),
    nameof(AssemblyFileVersionAttribute),
};

// As with the attribute patching above, get a list of existing attributes that matches our list and overwrite them for the module
foreach (var attribute in module.CustomAttributes.Where(attribute => versionRelatedAttributes.Contains(attribute.AttributeType.Name)))
{
    attribute.ConstructorArguments[0] = new CAArgument(module.CorLibTypes.String, new UTF8String(version));
}

// And the same for the assembly
foreach (var attribute in module.Assembly.CustomAttributes.Where(attribute => versionRelatedAttributes.Contains(attribute.AttributeType.Name)))
{
    attribute.ConstructorArguments[0] = new CAArgument(module.CorLibTypes.String, new UTF8String(version));
}

Now, if we check how our binary looks like in dnSpy:

A better patched Levinas.dll in dnSpy

Excellent!

Changing the root namespace

All good so far, but our root namespace, Levinas, is all over the place in the dnSpy output. Maybe we can fix this using EAZFuscator.

Levinas after EAZFuscator

It looks like EAZFuscator is not changing our namespace. This is actually as expected. There are various guards in EAZFuscator that keeps it from renaming anything that might break functionality. While in our case, it would actually be fine to change Levinas.Licenses to something else, EAZFuscator lacks the context to know this is safe.

We can give EAZFuscator a helping hand here and reduce our known IoCs in our non-obfuscated builds by replacing our namespace with something else using dnlib.

Now, a few caveats I figured out while trying to do this:

  • All type references in our assembly can be loaded with module.GetTypes(), but this will not include any nested types. Nested types are anything that's defined in a .cs file as an addition to the type referenced by the filename. For instance, if you define a public enum ProgramType { ... } in Program.cs in addition to public class Program { ... }, ProgramType will appear as a nested type under Program.
  • When you change the namespace, all references to that type will also reference the new namespace. However, I encountered an edge case when trying this with Seatbelt where typeof() calls in attributes will refer to the old namespace. The solution is to walk through all custom attributes in all types (nested or otherwise) and replace the namespace in any attribute that has the System.Type type.

With these in mind, we can replace our namespace like below:

using dnlib.DotNet;

// Load the contents of our executable
var contents = File.ReadAllBytes("not-malicious.exe");

// Create a new module context
var modCtx = ModuleDef.CreateModuleContext();

// Load our module with dnlib
var module = ModuleDefMD.Load(contents, modCtx);

// Let's make the namespace match the name from above
var namespaceValue = "TelemetryCli"

// Use a dictionary to store replaced namespaces
var rootNamespaces = new Dictionary<string, string>() { };

// If we have more than one namespace, let's lazily attach a counter
//   and keep using the single replacement namespace.
// It would be better to have multiple alternatives though.
var counter = 12;

// First change the root namespace for all existing types
foreach (var typeDef in module.GetTypes())
{
  // Skip anything unnecessary
  if (typeDef.Namespace == "") continue;
  if (typeDef.Namespace.StartsWith("System")) continue;
  if (typeDef.Namespace.StartsWith("Microsoft")) continue;

  var currentNamespace = typeDef.Namespace.ToString().Split(".").First() ?? "";

  // Add our replacement value if we haven't yet
  if (rootNamespaces.Count == 0)
  {
    rootNamespaces[currentNamespace] = namespaceValue;
    typeDef.Namespace = typeDef.Namespace.Replace(currentNamespace, rootNamespaces[currentNamespace]);

  } 
  // If the dictionary does contain the namespace already, just replace it
  else if (rootNamespaces.ContainsKey(currentNamespace))
  {
    typeDef.Namespace = typeDef.Namespace.Replace(currentNamespace, rootNamespaces[currentNamespace]);
  }
  // If we are here, that means we have more than one namespace and this one hasn't been added to
  //   rootNamespaces yet. Attach a counter to the namespace, replace it, and increase the counter.
  else
  {
    rootNamespaces[currentNamespace] = $"{namespaceValue}{counter:x8}";
    typeDef.Namespace = typeDef.Namespace.Replace(currentNamespace, rootNamespaces[currentNamespace]);

    counter += 3;
  }
}

// typeof calls in attributes don't change even if we change the namespace above
// we need to painstakingly patch each attribute with the `System.Type` type
foreach (var typeDef in module.GetTypes())
{
  // Skip anything unnecessary
  if (typeDef.Namespace == "") continue;
  if (typeDef.Namespace.StartsWith("System")) continue;
  if (typeDef.Namespace.StartsWith("Microsoft")) continue;

  // Patch all attributes in types
  foreach (var attribute in typeDef.CustomAttributes)
  {
    var tempArgs = attribute.ConstructorArguments.ToArray();

    foreach (var (index, arg) in tempArgs.Select((value, i) => (i, value)))
    {
      if (arg.Value == null) continue;
      if (arg.Type.ReflectionFullName != "System.Type") continue;

      var currentValue = arg.Value.ToString();

      var currentNamespace = currentValue?.Split(".").First() ?? "";

      if (!rootNamespaces.ContainsKey(currentNamespace)) continue;

      var renamedType = module.GetTypes()
        .FirstOrDefault(t => t.FullName == currentValue?.Replace(currentNamespace, rootNamespaces[currentNamespace]));

      if (renamedType == null) continue;

      attribute.ConstructorArguments[index] = new CAArgument(arg.Type, new ClassSig(renamedType));
    }
  }

  // Patch all attributes in sub-types
  foreach (var subtype in typeDef.NestedTypes)
  {
    if (!subtype.HasCustomAttributes) continue;

    foreach (var attribute in subtype.CustomAttributes)
    {
      var tempArgs = attribute.ConstructorArguments.ToArray();

      foreach (var (index, arg) in tempArgs.Select((value, i) => (i, value)))
      {
        if (arg.Value == null) continue;
        if (arg.Type.ReflectionFullName != "System.Type") continue;

        var currentValue = arg.Value.ToString();

        var currentNamespace = currentValue?.Split(".").First() ?? "";

        if (!rootNamespaces.ContainsKey(currentNamespace)) continue;

        var renamedType = module.GetTypes()
          .FirstOrDefault(t => t.FullName == currentValue?.Replace(currentNamespace, rootNamespaces[currentNamespace]));

        if (renamedType == null) continue;

        attribute.ConstructorArguments[index] = new CAArgument(arg.Type, new ClassSig(renamedType));
      }
    }
  }
}

Once done and re-obfuscated with EAZFuscator, we don't see any mention of Levinas in our assembly.

Levinas fully patched

Exporting methods from DLLs without using DllExport

NOTE: If you are using EAZFuscator, you should obfuscate your dll before adding an unmanaged export using dnlib. EAZFuscator can't parse the resulting method signature and will fail otherwise.

dnlib allows us to do even more with C# executables, but the one I'm most excited about is exporting dll methods without using DllExport. DllExport is quite finicky, isn't cross-platform, and needs to be fixed every time we change the namespace or do anything more substantial with the implant.

Rasta Mouse has a great tutorial on how to do this here. dnlib's README also has a good guide on this.

Turning Dlls into Exes

Another annoyance I had with implants before was when trying to generate exes, since the default output mode was dll. dnlib makes this pretty easy as well. This means we can compile as dll and generate both a dll with exported methods and an exe from it.


public static class DnlibExtensions {

    private static readonly IList<ModuleKind> ExecutableModuleKinds = new List<ModuleKind>
    {
        ModuleKind.Console,
        ModuleKind.Windows
    };

	public static async Task<byte[]> ConvertToExe(this ModuleDefMD module, string typeName, string methodName, ModuleKind moduleKind)
    {
        var type = module.GetTypes().FirstOrDefault(t => t.Name == typeName);
        
        if (type == null)
        {
            throw new Exception($"Unable to locate type `{typeName}`");
        }
        
        var method = type.Methods.FirstOrDefault(m => m.Name == methodName);

        if (method == null)
        {
            throw new Exception($"Unable to locate method `{methodName} on type `{typeName}`");
        }
        
        module.EntryPoint = (entrypoint.Method == "Main" ? method : type.Methods.FirstOrDefault(m => m.Name == "Main")) 
                            ?? AddMainMethod(module, type, method);
        
        module.Kind = ExecutableModuleKinds.Contains(moduleKind) ? moduleKind : ModuleKind.Console;

        var opts = new ModuleWriterOptions(module)
        {
            PEHeadersOptions =
            {
                Characteristics = dnlib.PE.Characteristics.ExecutableImage | dnlib.PE.Characteristics.LargeAddressAware
            }
        };

        await using var ms = new MemoryStream();
        module.Write(ms, opts);
        
        return ms.ToArray();
    }

    private static MethodDef? AddMainMethod(ModuleDef module, TypeDef type, IMethod method)
    {
        var main = new MethodDefUser("Main", MethodSig.CreateStatic(module.CorLibTypes.Void, new SZArraySig(module.CorLibTypes.String)))
        {
            Attributes = MethodAttributes.Static,
            ImplAttributes = MethodImplAttributes.IL | MethodImplAttributes.Managed
        };
        
        main.ParamDefs.Add(new ParamDefUser("args", 1));
        
        var mainBody = new CilBody();
        main.Body = mainBody;
        
        mainBody.Instructions.Add(OpCodes.Call.ToInstruction(method));
        mainBody.Instructions.Add(OpCodes.Ret.ToInstruction());
        
        type.Methods.Add(main);

        return main;
    }
}

public class Entrypoint
{
    public string? Type { get; set; }
    public string? Method { get; set; }
    public string? ExportName { get; set; }

    public override string ToString() => $"{Type}@{Method} as {ExportName}";
}