.NET assemblies are (in general) remarkably easy to decompile and obtain reasonably intelligible source code. A .NET obfuscator can be run as a post-build step to make decompiling the assembly much less useful to thieves who are trying to steal your intellectual property. There are many .NET obfuscators out there.
In this post, I review the SecureTeam CliSecure tool, version 22.214.171.124.
Introducing the test case
My “test case” DLL is not a simple “Hello, World!” app. It’s dotnetopenauth.dll, which is just over 1MB in size and pushes the limits of the .NET Framework enough that a few bugs in the CLR, compiler and build tools were discovered while this assembly was written. It defines its own .NET configuration sections, compiles with Code Contracts’ IL rewriter and ILMerge’s IL rewriter and is strong-name (delay) signed. It also very carefully uses log4net.dll when present, but doesn’t whine when the log4net is not present (defying most people’s assumptions about .NET’s runtime dependency rules). This DLL is enough to make most obfuscators fail in some way or another.
That said, the disclaimer is that this is just one test case, and is not intended to warrant or otherwise guarantee results on your own assemblies. Your mileage may vary.
Since in course of this review I was a relatively new user to this software, and usability most impacts a new user, I’ll start with usability. If you care more about functionality than usability, just skip this section. (Does anyone buy an obfuscator because it’s user-friendly?)
I find CliSecure easier than most obfuscators that I’ve tried, but it still has its oddities. Launching CliSecure opens a non-intimidating GUI application with few enough controls that it looks like something a newbie can get a handle on, and it sports the new Ribbon UI that’s been so hot lately.
But the first two buttons on the toolbar, New and Open, don’t apply to a first-timer. The app opens with a “New” project, so the New button is a no-op, and “Open” opens CliSecure projects, not DLLs to obfuscate. The next buttons, Save and Options, didn’t seem to apply either. Finally a small button called “Add” on the Ribbon suggests maybe that’s the one I was looking for. Bingo!
There’s a field at the bottom that displays a derived a sub-path where the obfuscated DLL goes. Cool. But when I changed the DLL I was obfuscating, the sub-path didn’t change with it automatically.
The Options button led to a dialog box with a grid and an explanatory paragraph for each configurable setting. Most of the explanations were adequate, although the most interesting one to me (Control Flow Obfuscation: Basic or Advanced) didn’t explain one over the other. The help documentation did include this detail including the the pros and cons of each option.
I found the Help documentation quite decent – complete with screenshots and explanation of the high level concepts needed to make informed obfuscation decisions.
- Support for strong-name and delay signing.
Whirlwind obfuscation run
CliSecure has a whole array of powerful obfuscation options. But beware: they’re all off by default. When obfuscating with the default settings in CliSecure the assemblies selected are not obfuscated at all but success is still reported.
After setting some reasonable obfuscation options, clicking “Build” (the button isn’t called “Obfuscate”) resulted in a few seconds of obfuscation during which there was an accurate progress bar. When it was done, there was no output window, no warnings or errors, and the “building” window just disappeared. Fortunately, near the button of the CliSecure window is a field that says where the “secured” assembly will be saved to, and it’s conveniently a read-only text field so I can copy the path out into Windows Explorer to open that folder (a click button to open the folder would have been a bonus though).
While the CliSecure UI was up after obfuscating my DLL, I was pleased to see it didn’t have any open file handles to the input or output DLLs, so I could run builds in another process without having to close CliSecure first.
There is a plethora of options offered in the GUI app (all off by default):
- Input. You can obfuscate several assemblies at once. Besides just batching for convenience, this should allow you to obfuscate internal members without breaking other assemblies with privileged InternalsVisibleTo allowing them to call these internal members.
- Code Protection. This is probably the most unique obfuscation feature in CliSecure, and truly worth your careful consideration. It actually encrypts your IL and makes it completely invisible to .NET Reflector, only decrypting the IL at execution time at a per-method level. But this decryption requires that your app run with Full Trust, which means you can’t use it on assemblies you send up to a shared host web server, among other things. But it should be fine for an assembly that you ship to a customer. This comes with a performance hit to the app, however.
- Renaming. This performs the most basic obfuscation function: renaming of types and members. CliSecure even will rename your “public” API and fix up any assemblies that call into that assembly. A great way to protect your own “internal” class libraries used by your multi-assembly application. It helps you get away from InternalsVisibleTo, which some argue encourages bad design.
- Control Flow. Changes the idioms that compilers use for looping constructs, breaking a decompiler’s ability to reconstruct for and while loops.
- Method Call Obfuscation. This feature protects the public APIs that your assembly calls in other assemblies. Depending on what you’re trying to obfuscate, this feature can be very important. Without it, every call to a method in .NET’s base class library will be apparent when people decompile your code, making it easier to figure out what you’re doing. CliSecure will replace these discoverable calls with dynamically generated delegates so a static analyzer or decompiler cannot discover them. This comes with a performance hit to the app, however.
- Merging. CliSecure comes with ILMerge-like behavior built-in, allowing you to conceal even more code by merging your helper library .dll with your application .exe and thus removing all the public surface area your library would have otherwise left exposed.
For my next test, I turned on everything except Code Protection or Method Call Obfuscation so I could still use .NET Reflector to see what obfuscation had taken place (and because DotNetOpenAuth.dll runs in shared hosting web servers and requiring Full Trust isn’t an option). I also didn’t use the Merging feature. Although DotNetOpenAuth uses ILMerge and could therefore possibly benefit from this feature, not every SKU of DotNetOpenAuth would be obfuscated, so making every SKU take a dependency on running CliSecure doesn’t make sense.
With so many protections turned on, obfuscation took a few seconds to complete this time. In .NET Reflector I saw just the public namespaces, and within those just the public types and members. All the obfuscated stuff was under the default (empty) namespace in a flat structure. Not only is this good for obfuscation, it’s good for the stuff that you want to be discoverable because all the obfuscated stuff doesn’t clutter up the stuff you want to be discoverable. Extra points for clean discoverability of public stuff.
The internal types were mostly obfuscated, with the necessary exception of the members that implemented public interfaces, and some internal types that are used to implement .NET .config file sections. I think these could have been obfuscated and were not, but configuration section types aren’t typically where people store their sensitive intellectual property anyway. Some of the types and members had such weird names that .NET Reflector crashed a few times as I was looking through the assembly. That’s just a bonus (since it discourages those snoopers).
Some methods were so obfuscated that .NET Reflector gave up on their implementations:
Others were decompiled, but quite uselessly so:
While most of this is unintelligible, I did manage to read that an HtmlLink object is instantiated here, which is puzzling since I turned on Method Call Obfuscation and thought that this type of thing would be hidden. I also found some internal strings that didn’t appear to have the obfuscated/encrypted values I expected since I turned on “String Obfuscation”. That said, .NET Reflector was unable to analyze where these strings were used, so that’s a half-win.
The bizarrely named methods were no more intelligible when you use .NET Reflector’s click-through to see the method implementation: the method “implementation” is merely a static field that holds a delegate and is initialized somewhere else that isn’t evident.
I’d give the obfuscator effectiveness a rating of 9.5/10. Yes, there were a couple words I could read here and there and the unobfuscated configuration section types, but it seems impossible to reverse engineer, and it obfuscated the names of properties, which other obfuscators say cannot be done. And remember, I haven’t even turned on Code Protection, which presumably would give it, what, a score of 15/10?
This is the trip cord for almost every obfuscator. If “peverify.exe” reports no errors on the original assembly, an obfuscator must produce a verifiable obfuscated assembly. Also, if the obfuscator injects code into the assembly that generates and executes IL at runtime, this code must also be verifiable (although peverify.exe cannot validate this unfortunately).
CliSecure passed this test by generating verifiable code when Renaming, Control Flow and String Obfuscation were turned on. But it generated 32 peverify errors when Method Call Obfuscation was turned on, and 2 more peverify errors when Code Protection was added. Since these two protections also impact performance and require the protected app to run with full trust, leaving these two protections off anyway makes sense for me. But for apps that would like to use these protection mechanisms the peverify errors they cause could cause verification/test issues at best and runtime failures at worst.
Does it run?
So although CliSecure passed the peverify.exe test, would the assembly actually run when dropped in-place of a non-obfuscated DLL? I had to find out.
I built the entire solution of DotNetOpenAuth and all its samples. Since CliSecure didn’t rename public types and members, a post-build in-place obfuscation should theoretically keep the sites working. Some of these sites are configured for only ASP.NET Medium Trust, so it would help validate that the obfuscator continues to run under this permission level as well.
Verdict: the obfuscated assembly appears to work. I qualify with “appears” to work because it’s a very complex assembly with many code paths and I haven’t finished testing all of them. But the core scenario works.
Any time you obfuscate an assembly you make debugging more difficult. The end user may report callstacks that are totally useless both to your user and to you! A good obfuscator will provide a way for you as the code owner to de-obfuscate stack traces, or even interactively debug your assembly with a reasonably similar experience to an unobfuscated assembly (provided you have the source code).
Amazingly enough, the obfuscated DLL was still debuggable using the original .pdb symbols file. The callstacks were obfuscated but stepping through the code still worked when the source code is available. Not all the breakpoints seemed to fire which may be a VS2010 bug or a bug in the obfuscation I don’t know. But stepping over and stepping into worked great once the debugger was stopped.
The callstack de-obfuscator failed, in that the callstack it generated was still illegible. That’s a real bummer when customer reports come in with a callstack that you then need to decipher.
Command line obfuscation support
CliSecure comes with a command-line version of their tool that takes all parameters at the command line, or takes a convenient single parameter that is the CliSecure project file that contains all the details within it. While there is no MSBuild task included, the command line syntax seems simple enough that an MSBuild “Exec” task could easily handle kicking off the process as part of an automated build.
This is by far the most feature rich .NET obfuscation tool I’ve seen. Its obfuscation protections are superb, even with the couple of semi-faulty options turned off. Debugging is pretty good, although de-obfuscating callstacks needs some work. During this review the SecureTeam behind the product were very responsive and fixed many bugs I reported rapidly, so I fully expect a new version coming near you will fix the remaining issues.