Umbra
Table of Contents
C# Obfuscation has been on my mind for a little while but a lot of the projects that provide it tend to all be the same thing with minor changes here and there.
Whether it’s the generic option of ConfuserEx or .NET Reactor, to the semi odd ball ones like KoiVM and a paid option like “DNGuard HVM”, whatever the hell that is. A lot of the offerings by these obfuscators are all the same:
- String Encryption
- Constants Encryption
- Resource Encryption
- Control Flow Flattening
- Symbol Renaming
- Ref Proxies
- Anti Tamper and Anti Debug
- Custom VM Implementation (like KoiVM)
But there never is anything more interesting outside of these options to make your code either truly confusing to read or almost impossible to read.
Umbra⌗
That’s where Umbra comes in.
Umbra offers the same set of options as every other obfuscator but gives some extra passes that might be so far out of left field that the person trying to reverse engineer the code re-evaluates their life and does something else.
Features⌗
Umbra has the basic features that’s already been listed, even it’s own custom VM implementation that will likely prevent OldRod from reversing it, but it also has a few other tricks up it’s sleeves.
Method Cloning⌗
Give your code some extra meat by cloning methods and redirecting calls to the original to the clones. Makes the call graph more annoying to read as there is no one place to do a given thing, now there’s 3, 4, 5 or more and they are all used.
Before:
public static void Main(string[] args)
{
Log("Hello world");
Log(string.Join(", ", args));
Log(Environment.UserName);
Log("umbra is pretty cool");
Log("Cores: " + Environment.ProcessorCount);
}
private static void Log(string msg) => Console.WriteLine("INF | " + msg);
After:
public static void Main(string[] args)
{
Log_Cl2("Hello world");
Log_Cl3(string.Join(", ", args));
Log_Cl1(Environment.UserName);
Log("umbra is pretty cool");
Log_Cl3("Cores: " + Environment.ProcessorCount);
}
private static void Log(string msg) => Console.WriteLine("INF | " + msg);
private static void Log_Cl1(string msg) => Console.WriteLine("INF | " + msg);
private static void Log_Cl2(string msg) => Console.WriteLine("INF | " + msg);
private static void Log_Cl3(string msg) => Console.WriteLine("INF | " + msg);
Now whenever someone decompiles the code and checks who calls what, it’ll be a mismash of calls that all do the same thing.
Reflection Ref Proxies⌗
Typically a reference proxy is just the following
Before:
public static void Main(string[] args)
{
DoAThing(args[0]);
}
public static void DoAThing(string a)
{
Console.WriteLine(a.ToUpper().Substring(0, 5));
}
After:
public static void Main(string[] args)
{
DoAThing(args[0]);
}
private static void DoAThing(string a)
{
MethodA(a.ToUpper().Substring(0, 5));
}
internal static void MethodA(string A_0)
{
Console.WriteLine(A_0);
}
Where calls get a forwarder, or a “proxy”.
But this is a little too easy to read still, it’s not really obfuscating anything, it’s just moving something to somewhere else with more or less the same signature or at least the same visual identity as what it was already doing.
This is where Reflection Ref Proxies come in.
Before:
public static void Main(string[] args)
{
DoAThing(args[0]);
}
public static void DoAThing(string a)
{
Console.WriteLine(a.ToUpper().Substring(0, 5));
}
After:
public static void Main(string[] args)
{
DoAThing(args[0]);
}
private static void DoAThing(string a)
{
MethodA(a.ToUpper().Substring(0, 5));
}
internal static void MethodA(string A_0)
{
((Action<string>)<Module>.Dictionary["Identifier"])(A_0);
}
Now you don’t really know what it’s doing, but you know it’s signature, the previous normal ref proxies are easy to defeat by looking at it, reflection ref proxies make it intentionally annoying to read with the added bonus of there being many identifiers for a given proxy, attempting to aid in the confusing aspect of which one is the real one.
Method Body Merging⌗
I haven’t seen anything like this out in the wild but I found it to be a rather interesting tactic. Essentially you take a class’s methods, give it a new merged method and throw in every method’s body into it with a switch case. Then the original methods forward their parameters and return values based on the merged method.
Before:
public static void Main(string[] args)
{
DoAThing(args[0]);
}
public static void DoAThing(string a)
{
Console.WriteLine(a.ToUpper().Substring(0, 5));
}
After:
public static void Main(string[] args)
{
MergedMethod(new object[] { args }, 0);
}
public static void DoAThing(string a)
{
MergedMethod(new object[] { a }, 1);
}
internal static object MergedMethod(object[] A_0, int A_1)
{
switch (A_1)
{
default:
{
string[] array = (string[])A_0[0];
DoAThing(array[0]);
return null;
}
case 1:
{
string text = (string)A_0[0];
Console.WriteLine(text.ToUpper().Substring(0, 5));
return null;
}
}
}
On it’s own it’s only annoying to look at, but still very readable. But in combination with Control Flow, it’s a true nightmare.
Indirect Calls⌗
This is one that apparently exists, but I can’t find any public obfuscator with it as a feature. It’s another pass that while it doesn’t do all that much it’s not a common option so it might throw people off when they look at it.
Before:
public static void Main(string[] args)
{
DoAThing(args[0]);
}
public static void DoAThing(string a)
{
Console.WriteLine(a.ToUpper().Substring(0, 5));
}
After:
public static void Main(string[] args)
{
calli(System.Void(System.String), args[0], ldftn(DoAThing));
}
public static void DoAThing(string a)
{
calli(System.Void(System.String), a.ToUpper().Substring(0, 5), ldftn(WriteLine));
}
This output is the latest dnSpyEx v6.6.0. Which uses a ILSpy C# Decompiler in the background so this might work on ILSpy too. I don’t fully understand why the output is like this, if I was to guess calli is so obscure as a IL OpCode that decompilers tend to omit giving it any thought on an actual decompilation. So it just looks weird.
There is a update to this I will be making to move the ldftn(DoAThing) and ldftn(WriteLine) outside of the method itself, since it is just returning a function pointer.
Umbra VM⌗
Like every other obfuscator’s idea of a VM, this is more or less the same just different enough to defeat existing reversing programs.
Before:
public static void Main(string[] args)
{
DoAThing(args[0]);
}
public static void DoAThing(string a)
{
Console.WriteLine(a.ToUpper().Substring(0, 5));
}
After:
public static void Main(string[] args)
{
<Module>.Vm(0, 0, new object[] { args });
}
private static void DoAThing(string a)
{
<Module>.Vm(22, 0, new object[] { a });
}
internal static int Vm_Thunk1(object[] A_0, int A_1)
{
DoAThing((string)A_0[A_1 - 1]);
return A_1 - 1;
}
Nothing super special here, and in fact it’s the worst for performance, around a 40-50% slowdown since it’s a custom interpreter being injected and used.
ShadowIL⌗
My favourite, and an idea I have had for a very long time, I just haven’t had the brain to get round to making it.
Before:
public static void Main(string[] args)
{
DoAThing(args[0]);
}
public static void DoAThing(string a)
{
Console.WriteLine(a.ToUpper().Substring(0, 5));
}
After:
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static void Main(string[] args)
{
throw new InvalidDataException();
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static void DoAThing(string a)
{
throw new InvalidDataException();
}
This is like if Harmony and ConfuserEx had a baby. Basically at Module Init, entry point or a manually configured entry point, the ShadowRuntime class patches all shadow methods like Harmony and provides full native speeds, no slowdowns like vm, with the original code intact. Instructions for a method are taken out, paired with some metadata and encrypted as a embedded resource. The inverse happens when the runtime kicks in.
This has 2 modes, Eager which you see in the example above and Lazy which has a different output, looks like this.
Before:
public static void Main(string[] args)
{
DoAThing(args[0]);
}
public static void DoAThing(string a)
{
Console.WriteLine(a.ToUpper().Substring(0, 5));
}
After:
public static void Main(string[] args)
{
ShadowRuntime.EnsureUncloaked(-92362333);
MethodA(args);
}
private static void DoAThing(string a)
{
ShadowRuntime.EnsureUncloaked(1653183034);
MethodB(a);
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
internal static void MethodA(string[] A_0)
{
throw new InvalidOperationException();
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
internal static void MethodB(string A_0)
{
throw new InvalidOperationException();
}
The caveat between the 2 is Eager decrypts and patches every method, even ones that might not see any calls during it’s lifetime. Lazy is the opposite, a method is only decrypted and patched if it’s called.
I think Eager is a bit better overall as the hitch to decrypt and patch happens all at once and the paths become warm. Whereas with Lazy a method call could happen in a performance critical section and hitch everything.
Why I made Umbra⌗
Research mainly, I got bored one day and thought “how hard can it be to come up with some unique obfuscation techniques” and in fairness it’s not really all that hard, the problem is weighing in if the hit to performance is truly worth it.
In the case of the VM pass being 40-50% slower, it’s fine for tooling that doesn’t need the performance but still wants to be relatively protected against enthusiastic reverse engineers.
Of course no pass is truly impossible to crack, while I love the ShadowIL pass, it wouldn’t be that hard for someone to reverse it and it wasn’t really a goal for me to make something that was impossible to reverse. It was to make interesting obfuscation passes that yielded good results in the “this method looks like an absolute nightmare, fuck it, it can stay obfuscated” mindset and I think ShadowIL did the best since it truly messes with the mind in what is possible.
A lot of these weird obfuscation passes work best with other passes in combination, it wouldn’t make much sense to use Method Cloning on it’s own, ideally Method Cloning, Control Flow and even Ref Proxies for extra awfulness.
No release, no source, I’m not that bothered to release something that no one will use. Which is again fine, it’s not about having something to use, it was about learning.