cs obfuscation for fun and profit
c# obfuscation: making your code undetectable (but functional)
introduction
hey its me gerbsec, back again with more writing. as you’re probably not aware, i’ve recently started getting real deep into opsec and rt again and my next challenge was running all of our awesome c# assemblies in memory. only one issue, they are all signatures to hell and back…. so what do i do? do i run yara64.exe on every single binary and rinse and repeat for hours at a time till i get an undetectable binary? NO! I find a tool that obfuscates c#, update it to make it more practical and functional, and then write about it!
the tool in question is my fork of smokeyobfuscator. which initially had a gui and some other functionality that i decided was not relevant to rt. the author seems to be a dev or something but his tool is great!
Original author credit: TrentonH1ll
what is c# obfuscation?
c# obfuscation is the process of transforming your code to make it difficult to understand/detect. this is particularly useful for red teamers since our greatest enemy is static signatures right? right?
why obfuscate?
- security: protect sensitive logic and algorithms.
- opsec: won’t get detected on disk or in memory.
- IMPORANT: this won’t help with behavioral but its a start.
prerequisites
to follow along, you’ll need:
- visual studio or any c# ide
- smokeyobfuscator
setting up smokeyobfuscator
clone the repository and open it in your ide. let’s start with the main function where we load the binaries and execute our obfuscation:
private void button2_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(selectedDirectory))
{
MessageBox.Show("please select a directory first.");
return;
}
// get all .exe files from the selected directory
string[] files = Directory.GetFiles(selectedDirectory, "*.exe");
foreach (string file in files)
{
try
{
// load file into memory
byte[] fileBytes = File.ReadAllBytes(file);
// load module from memory
ModuleDefMD module;
using (var ms = new MemoryStream(fileBytes))
{
module = ModuleDefMD.Load(ms);
}
// perform obfuscation
NumberChanger.Process(module);
Strings.Execute(module);
ProxyInts.Execute(module);
HideMethods.Execute(module);
// save the obfuscated file back to disk
SaveFile(module, file);
}
catch (Exception ex)
{
// handle errors
skippedFiles.Add(file);
Console.WriteLine($"error processing {file}: {ex.Message}");
}
}
}
here we simply open a directory for files, rotate through the files and execute our obfuscation techniques. at its current state its a a gui tool. in my next release i’ll be pushing it out as a command line tool that takes a directory for an argument.
EDIT: I just pushed the above 7/7/2024.
protections
now, let’s break down the different protections used in smokeyobfuscator.
hide methods
hide methods
adds custom attributes and modifies method names to make them harder to understand:
public static void Execute(ModuleDef module)
{
// create a type reference for the 'CompilerGeneratedAttribute'
TypeRef attrRef = module.CorLibTypes.GetTypeRef("System.Runtime.CompilerServices", "CompilerGeneratedAttribute");
// create a constructor reference for the 'CompilerGeneratedAttribute'
var ctorRef = new MemberRefUser(module, ".ctor", MethodSig.CreateInstance(module.CorLibTypes.Void), attrRef);
// create a new custom attribute using the constructor reference
var attr = new CustomAttribute(ctorRef);
// iterate over all types in the module
foreach (var type in module.GetTypes())
{
// iterate over all methods in each type
foreach (var method in type.Methods)
{
// skip runtime special methods, special methods, and methods named 'Invoke'
if (method.IsRuntimeSpecialName || method.IsSpecialName || method.Name == "Invoke") continue;
// add the custom attribute to the method
method.CustomAttributes.Add(attr);
// rename the method to make it less recognizable here im using gerbserv.com but it could literally be anything.
method.Name = "<gerbserv.com>" + method.Name;
}
}
}
number changer
number changer
obfuscates numeric constants by replacing them with a series of mathematical operations:
public static void Process(ModuleDefMD module)
{
foreach (TypeDef type in module.Types)
{
foreach (MethodDef method in type.Methods)
{
if (method.Body != null)
{
for (int i = 0; i < method.Body.Instructions.Count; i++)
{
Instruction instruction = method.Body.Instructions[i];
if (instruction.Operand is int && instruction.IsLdcI4() && instruction.OpCode == OpCodes.Ldc_I4)
{
List<Instruction> instructions = GenerateInstructions(Convert.ToInt32(instruction.Operand));
instruction.OpCode = OpCodes.Nop;
foreach (Instruction instr in instructions)
{
method.Body.Instructions.Insert(i + 1, instr);
i++;
}
}
}
}
}
}
}
strings
strings
obfuscates string constants by encoding them:
public static void Execute(ModuleDefMD module)
{
MethodDefUser TTH = new MethodDefUser("gerbserv", MethodSig.CreateStatic(module.CorLibTypes.String, module.CorLibTypes.String), MethodImplAttributes.IL | MethodAttributes.Managed, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig | MethodAttributes.ReuseSlot);
module.GlobalType.Methods.Add(TTH);
CilBody body = new CilBody();
TTH.Body = body;
body.Instructions.Add(OpCodes.Nop.ToInstruction());
body.Instructions.Add(OpCodes.Call.ToInstruction(module.Import(typeof(Encoding).GetMethod("get_UTF8", new Type[] { }))));
body.Instructions.Add(OpCodes.Ldarg_0.ToInstruction());
body.Instructions.Add(OpCodes.Call.ToInstruction(module.Import(typeof(System.Convert).GetMethod("FromBase64String", new Type[] { typeof(string) }))));
body.Instructions.Add(OpCodes.Callvirt.ToInstruction(module.Import(typeof(System.Text.Encoding).GetMethod("GetString", new Type[] { typeof(byte[]) }))));
body.Instructions.Add(OpCodes.Ret.ToInstruction());
foreach (TypeDef type in module.Types)
{
if (type.Name != "Resources" || type.Name != "Settings")
{
foreach (MethodDef method in type.Methods)
{
if (!method.HasBody) continue;
for (int i = 0; i < method.Body.Instructions.Count(); i++)
{
if (method.Body.Instructions[i].OpCode == OpCodes.Ldstr)
{
method.Body.Instructions[i].Operand = Convert.ToBase64String(UTF8Encoding.UTF8.GetBytes(method.Body.Instructions[i].Operand.ToString()));
method.Body.Instructions.Insert(i + 1, new Instruction(OpCodes.Call, TTH));
i++;
}
}
method.Body.SimplifyBranches();
method.Body.OptimizeBranches();
}
}
}
}
- method creation: a new method named
gerbserv
is created in the global type. this method takes a base64-encoded string as an input and returns the decoded string. - cilbody: a cil (common intermediate language) body is created for the method. this body contains the instructions for decoding a base64 string.
- instructions:
OpCodes.Nop
: no operation, just a placeholder.OpCodes.Call
: calls theget_UTF8
method of theEncoding
class to get the utf8 encoding.OpCodes.Ldarg_0
: loads the first argument (the base64 string) onto the stack.OpCodes.Call
: calls theFromBase64String
method of theConvert
class to decode the base64 string.OpCodes.Callvirt
: calls theGetString
method of theEncoding
class to convert the byte array to a string.OpCodes.Ret
: returns the decoded string.
Now this actually works for now, but i have plans to aes encrypt instead with a randomly generated key and iv. this ensures that everytime this is generated we’ll have a new hash for the file and new strings etc.
seeing it in action
before obfuscation, our binary is easily detected by av:
after running smokeyobfuscator:
conclusion
you should now have a good understanding of c# obfuscation and how to use smokeyobfuscator to protect your code. feel free to reach out on my socials if you have any questions!
best, gerbsec