I needed to create a ClickOnce publish of an obfuscated exe for a customer since he was concerned about theft of his intellectual property. I added in several security features, but they were worthless if I couldn’t harden the executable against decompilers like .Net Reflector. If you don’t believe me, try running .Net Reflector on your code. You’ll be shocked as to how much meaningful information is retained in the output. Not only is your code exposed, but so are all the string literals, member names, and resources.
So I shopped around for an obfuscating (et al.) program. I looked at the industry-standard, Dotfuscator, since a free version even ships with Visual Studio. But the free version is worthless since it only renames public members (again, don’t take my word and just peek into it yourself). The commercial version of Dotfuscator very easily supports obfuscating a ClickOnce publish (see this handy demo) and additional hardening techniques, but the price is $4950 or $5940 depending on which edition you choose. They have a special deal for companies less than 40 employees, but the price is still $2500. I figured it was worth some shoe leather to find a less expensive solution.
After some hunting around and having mixed results with various other products, I found a program called .Net Reactor. I like it because it’s straightforward, fully-featured, has comprehensive command-line support, and is very reasonably priced at $179.
Now that I had an obfuscation tool, I had to figure out how to make it work with ClickOnce. I was initially hopeful seeing the post-build command-line property in the project properties. However, if you simply add the obfuscation to the post-build event, the published exe is not obfuscated. It turns out your exe is compiled into an intermediate directory (e.g. “obj\Release”) and then copied to your build output path and/or used to create a ClickOnce publish. The post-build event is too late to affect the publish.
To hook in before the publish, you have to turn to MSBuild, the compile tool used in the background by Visual Studio to consume and compile a .Net project. It turns your project file (.vbproj or .csproj) are actually MSBuild files that you can edit directly for more features than are supported through the IDE.
(Forgive me if I’ve violated MSBuild best-practices in my tasks and properties, but there just isn’t a lot of help out there and the MSBuild syntax seems pretty sloppy to begin with.)
- Download this file and place it in the same directory as the project file of the project you want to protect.
This file is analogous to the .Net provided .targets files that are also included with your project by default. This file will override or add to tasks that are handled by those files.
Note: You will have to modify this file to suit your particular situation. Search the targets file for MergeAssembliesList (to use a separator other than “/”), MergeAssembliesArg, ObfuscatorName, ObfuscatorLocation, and ObfuscatorLocation. Also, for .Net Reactor, the merging application is built into the obfuscator. If you are using a separate merging app, you’ll have to add a call to that program. You should be able to figure it out from how this targets file is formatted.
- Right-click on the project that you will be publishing and click ‘Unload Project’. (If your project is source-controlled, you may get a warning that says you still have files checked out, but you can just click ‘Continue’ without causing any problems.)
- Right-click on the project again and click ‘Edit MyApp.vbproj’.
- Paste the non-highlighted XML into your project file between the highlighted lines. The ‘.targets’ file may have a different file name if you’re using C# but it will still end in ‘.targets’.
<Import Project="$(MSBuildBinPath)\Microsoft.VisualBasic.targets" /> <!-- Use the following settings to control how and if the output is obfuscated. --> <Target Name="BeforeBuild" Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <CreateProperty Value="true"> <Output TaskParameter="Value" PropertyName="Obfuscate" /> </CreateProperty> <CreateProperty Value=""> <Output TaskParameter="Value" PropertyName="ObfuscatorOptionsOverride" /> </CreateProperty> <CreateProperty Value="true"> <Output TaskParameter="Value" PropertyName="ShouldMergeAssemblies" /> </CreateProperty> <CreateProperty Value=""> <Output TaskParameter="Value" PropertyName="MergeAssembliesArgOverride" /> </CreateProperty> </Target> <Import Project="$(MSBuildProjectDirectory)\Obfuscation.targets" /> </Project>
You can change the ‘true’ or ‘false’ values to control whether or not the obfuscation is done at all and whether or not assemblies are merged when obfuscating. You can also change the configuration from ‘Release’ to some other configuration that you have defined.
You can use the Overrides to change the arguments to the obfuscator or merging program. This is helpful if your program can’t be obfuscated by your chosen default obfuscation settings or you want to merge only certain assemblies.
Note: If you don’t intend to merge assemblies, each dll or exe that is included with your project must be separately obfuscated (unless of course it’s a 3rd party include). When obfuscating these includes, make sure NOT to obfuscate their public members or your main assembly won’t be able to use them.
- After saving your changes, right-click on the project and click ‘Reload Project’. You may get a security warning, but just choose to ‘Load project normally’.
- If you’re intending to Publish and merge assemblies, there’s one more step required. You must exclude files that are merged into the main assembly and would be normally output with the publish. Otherwise, these unprotected files, though unused, will be available for the world to decompile. (Note: this doesn’t apply if you’re not merging.)
Go to the ‘Publish’ tab in ‘My Project’ and click on ‘Application Files’. For any dll or exe file listed as ‘Include’ or ‘Include (auto)’, change it to ‘Exclude’. If you have 3rd party files or files listed as ‘Prerequisite’, merging may not be an option and you may have to resort to individually protecting your includes by obfuscation and security checks to ensure usage only by your programs.
- Change your configuration to ‘Release’ and build or publish. Note: For whatever reason, the IDE sometimes forgets what solution configuration was last selected, so always check that you have the right configuration selected if the obfuscator doesn’t run.
- When you’re working in XML, don’t forget to properly escape special characters. For example, a ‘<' should be escaped as
<. Also, you must escape quotation marks inside a string literal (using
") but you don’t have to if you’re only inside a tag, like when specifying a property value. Here’s a great thread on XML escape characters.
- To make sure Messages are output, in VS IDE go to Tools->Options->Projects & Solutions->Build & Run and change Output Verbosity from ‘Minimal’ to ‘Normal’. It helps a lot when debugging custom MSBuild configs.
- MSBuild will halt if a program’s exit code is not ‘0’. If your obfuscator of choice uses a different exit code or always returns something non-zero, then make sure to trap it in the way I did above and throw an error when appropriate. Actually, the error trapping I did was redundant (I think), but it’s a handy example.
- If you’re using another obfuscator, it should ideally conform to the MSBuild convention of returning 0 (zero) if successful and a non-zero number on an error. If your obfuscator works differently, for example, always returning 1 on success, or if you just want to ignore certain errors, you may use the following convention to trap and interpret exit codes. This example raises an error for all return values other than 0, effectively duplicating the standard MSBuild convention.
<Exec Command="$(ObfuscatorLocation) -file "$(SourceExe)" -targetfile "$(SecureExe)" $(ObfuscatorOptions) "> <Output TaskParameter="ExitCode" PropertyName="ObfuscatorErrorCode" /> </Exec> <Error Condition="'$(ObfuscatorErrorCode)' != '0'" Text="Error while executing '$(ObfuscatorName)'. Returned exit code of '$(ObfuscatorErrorCode)'." />
2010-05-30 Updated: Refined include method and added support for merging assemblies. Also, included instructions for trapping custom errors.
2011-01-03 Updated: Fixed problem with handling references to COM assemblies (or possibly other reference types) that would generate the following error:
C:\Dev\MyApplication\Obfuscation.targets(27,5): error MSB4096: The item "obj\Release\Interop.SomeCOMClass.dll" in item list "ReferencePath" does not define a value for metadata "ResolvedFrom". In order to use this metadata, either qualify it by specifying %(ReferencePath.ResolvedFrom), or ensure that all items in this list define a value for this metadata.
Also added support for files that are included as prerequisites that would generate the following custom error:
Assemblies set to merge during obfuscation but ClickOnce deployment still lists output dependencies!
2011-06-05 Updated: Updated xml for project file to include all the override arguments options. Also added notes about how the Obfuscation.targets file may need to be modified.