Obfuscating a ClickOnce Publish

Edit: See updated version specifically for use with VS 2010.

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.)

Steps

  1. Download this file and place it in the same directory as the project file of the project you want to protect.
    Download

    Obfuscation.targets


    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.
  2. 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.)
  3. Right-click on the project again and click ‘Edit MyApp.vbproj’.
  4. 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.

  5. 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’.
  6. 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.
  7. 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.

Notes

  • When you’re working in XML, don’t forget to properly escape special characters. For example, a ‘<' should be escaped as &lt;. Also, you must escape quotation marks inside a string literal (using &quot;) 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 &quot;$(SourceExe)&quot; -targetfile &quot;$(SecureExe)&quot; $(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.

References

26 thoughts on “Obfuscating a ClickOnce Publish

  1. Excellent Job! I was having trouble with using batch files with click once but this solved it. Thanks!

  2. Hi Nathan.

    You could consider using ClickOnceMore as a solution here. After you’ve built your assemblies and obfuscated them, you can build your ClickOnce manifests using ClickOnceMore.

    My company, Red Sky Software, publishes ClickOnceMore intended for use in just this way. Might be worth a try… http://www.clickoncemore.net.

    Great article!

    Cheers,
    Greg

  3. I tried you approach without success on a C# solution. It gives me an error:

    Error 6 The item “C:\Windows\assembly\GAC\Microsoft.mshtml\7.0.3300.0__b03f5f7f11d50a3a\Microsoft.mshtml.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.

    Any ideas?

  4. @ Marjan : The .targets file is a simple xml file. You can open it in Visual Studio (you needn’t add it to your project) and edit it from there and you’ll get all the syntax highlighting, etc.

  5. @Robson: I solved the issue that you were having. It looks like you had another solution that in a roundabout way solved the problem, but you should get the latest version of the Obfuscation.targets file.

  6. Hi. I am using dotfuscator. I was able to run it from the targets file. dotfuscator outputs the files to the bin\release folder. After dotfuscator completes, the VS continues publishing. However, it overwrites the obfuscated files with its own unobfuscated files. What should I do?

    Assaf

  7. I just noticed that if I do Clean before Build, then the dotfuscator complains that it has no files to obfuscate. I looked into the bin\release folder and I noticed that it is empty. Am I missing something?

  8. By the way, my app is signed with a certificate (unless you tell me how to avoid it) as well as, it is composed of exe and dlls.

  9. @Assaf: It sounds like you have your source and outputs all mixed up. The source files to be obfuscated should come from the intermediate folder (not “bin\Release”) and the obfuscated files should be output to a temp folder. Then the source files should be overwritten with their obfuscated versions.

    You see, when the compiler is invoked in a configuration called Release either by a build or a publish, it compiles and assembles all the necessary files in the “obj\Release” folder. This is called the “intermediate output path” which you’ll see used in the Obfuscation.targets file.

    If you’re performing a publish, once the build is complete vs will build the publish using these intermediate files and generate and sign the publish manifest. We do the obfuscation in the intermediate folder because after the publish manifest is generated, the published files may not change.

    Only after everything is completely built are things copied to the “bin\Release” folder or the publish destination.

    By the way, are you using the community edition of Dotfuscator? Keep in mind that its obfuscations are extremely weak and probably won’t do much to protect your program. Download .NET Reflector to see just how much of your program is still susceptible to disassembly.

  10. Hey Nathan,

    This seems like a great solution but I can’t seem to get it working, not sure if I’m doing something wrong.

    I get the following error message when builing my project:
    C:\Windows\Microsoft.NET\Framework\v4.0.30319\Microsoft.Common.targets(2580,9): error MSB3171: Problem generating manifest. Could not load file or assembly ‘C:\ProjectName\obj\Release\AppName.exe’ or one of its dependencies. An attempt was made to load a program with an incorrect format.

    Line 2580 in Microsoft.Common.targets is inside the section.

    I’ve noticed that when I remove the following from your .targets file, the project compiles but it is then obviously working with the original exe and not the obfuscated version:

    I’m also not quite sure what you mean with the following related to the _CopyFilesMarkedCopyLocal, GenerateManifests and DoubleCheckDependencies targets
    “Borrowed from target in ‘Microsoft.Common.targets’ included with MSBuild 2.0. You may need to make sure it matches the targets file included with your version of MSBuild.”

    Does this mean that I have to overwrite the target in your .targets file with what’s found in the Microsoft.Common.targets file (located in C:\Windows\Microsoft.NET\Framework\v4.0.30319)?

    These are probably obvious questions but I’m not at all familiar with the way .target files work.

    Thanks in advance
    Stefan

  11. Sorry, looks like a part of my message was stripped out because it contained xml.

    Third paragraph should read:
    Line 2580 in Microsoft.Common.targets is inside the “GenerateApplicationManifest” target.

  12. Hi Nathan.

    I was able to run my project but cookcomputing namespace not working,
    GetPages GetPageList = (GetPages)CookComputing.XmlRpc.XmlRpcProxyGen.Create(typeof(GetPages));

    Create method not invoking the page, what should i do, anything i missed?

  13. @Baskar. Your question is a bit light on details, particularly any exception that is generated. If you’re not getting any compile errors, my suspicion is that there is some bug in the obfuscator you’re using. I’m using .NET Reactor and there were several settings I had to tweak for some of my programs. I’m not sure why they caused a problem only for those programs, but that’s what it took to get them running.

  14. @Stefan. The borrowing from the common targets comment means that I had to override the built-in _CopyFilesMarkedCopyLocal and so copied/pasted its original contents to preserve it’s original functionality. That first section in my version is from the common targets and the second section I created to copy the mapping file.

    I took a look in my framework folder and it looks like the 4.0 framework has a different _CopyFilesMarkedCopyLocal. Try this modified version and let me know if it works for you (diff from orig highlighted):

    
    <Target
        Name="_CopyFilesMarkedCopyLocal"
        Condition="'@(ReferenceCopyLocalPaths)' != ''"
        >
    
        <PropertyGroup>
            <!-- By default we're not using Hard Links to copy to the output directory, and never when building in VS -->
            <CreateHardLinksForCopyLocalIfPossible Condition="'$(BuildingInsideVisualStudio)' == 'true' or '$(CreateHardLinksForCopyLocalIfPossible)' == ''">false</CreateHardLinksForCopyLocalIfPossible>
        </PropertyGroup>
    
        <Copy
            SourceFiles="@(ReferenceCopyLocalPaths)"
            DestinationFiles="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')"
            SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
            OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
            Retries="$(CopyRetryCount)"
            RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
            UseHardlinksIfPossible="$(CreateHardLinksForCopyLocalIfPossible)"
            Condition="'$(UseCommonOutputDirectory)' != 'true' And ('$(Obfuscate)'!='true' Or '$(ShouldMergeAssemblies)'!='true')"
            >
    
            <Output TaskParameter="DestinationFiles" ItemName="FileWritesShareable"/>
    
        </Copy>
        
         <!-- Since this target is already overriden, let's use this to copy the mapping file. -->
        <Copy SourceFiles="@(ObfuscationMapFiles)" DestinationFiles="@(ObfuscationMapFiles->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" SkipUnchangedFiles="true" Condition="'$(Obfuscate)'=='true'">
          <Output TaskParameter="DestinationFiles" ItemName="FileWrites" />
        </Copy>
    
    
    </Target>
    
  15. I’m intending to publish (clickonce) and merge assemblies. I’ve to exclude all assemblies. But the app depends also on third party assemblies. I’want to merge and protect only own assemblies. Other third party assemblies must be either included without protection. How can i achieve this?

  16. There’s not an easy way with the targets I’ve created. Since I usually only have one or two third party assemblies, I just allow them to be merged in. This shouldn’t cause any issues if you’re ClickOnce publishing.

  17. After following the instruction in this post, we are having problem with using .NET Reactor with ClickOnce deployment.
    We get the error below when we publish with ClickOnce.
    We have a simple windows app which we have modified the
    ClickOnceDemo.csproj file to add .NET Reactor. So everytime we make a build or publish
    it run .NET Reactor and publish the app files to my local IIS.
    However when I install the publish app we get this error.

    If we take off .NET Reactor off the equation here it will publish without any problem.
    So there might be something in .NET Reactor that causes this error.

    Also, if we only build it with .NET Reactor it is going to build fine but would not publish correctly and we get this problem.
    I am using VS 2010.

    Thanks

    ——————————————————————————
    PLATFORM VERSION INFO
    Windows : 5.1.2600.196608 (Win32NT)
    Common Language Runtime : 4.0.30319.269
    System.Deployment.dll : 4.0.30319.245 (RTMGDR.030319-2400)
    clr.dll : 4.0.30319.269 (RTMGDR.030319-2600)
    dfdll.dll : 4.0.30319.1 (RTMRel.030319-0100)
    dfshim.dll : 4.0.31106.0 (Main.031106-0000)

    SOURCES
    Deployment url : http://tata-2345/ClickOnceDemo/ClickOnceDemo.application
    Server : Microsoft-IIS/5.1
    X-Powered-By : ASP.NET
    Deployment Provider url : http://tata-2345/ClickOnceDemo/ClickOnceDemo.application
    Application url : http://tata-2345/ClickOnceDemo/Application%20Files/ClickOnceDemo_1_0_0_8/ClickOnceDemo.exe.manifest
    Server : Microsoft-IIS/5.1
    X-Powered-By : ASP.NET

    IDENTITIES
    Deployment Identity : ClickOnceDemo.application, Version=1.0.0.8, Culture=neutral, PublicKeyToken=93d21c3e11713568, processorArchitecture=x86
    Application Identity : ClickOnceDemo.exe, Version=1.0.0.8, Culture=neutral, PublicKeyToken=93d21c3e11713568, processorArchitecture=x86, type=win32

    APPLICATION SUMMARY
    * Installable application.

    ERROR SUMMARY
    Below is a summary of the errors, details of these errors are listed later in the log.
    * Activation of http://tata-2345/ClickOnceDemo/ClickOnceDemo.application resulted in exception. Following failure messages were detected:
    + Exception occurred loading manifest from file ClickOnceDemo.exe: the manifest may not be valid or the file could not be opened.
    + Cannot load internal manifest from component file.

    COMPONENT STORE TRANSACTION FAILURE SUMMARY
    No transaction error was detected.

    WARNINGS
    There were no warnings during this operation.

    OPERATION PROGRESS STATUS
    * [11/11/2012 9:14:17 AM] : Activation of http://tata-2345/ClickOnceDemo/ClickOnceDemo.application has started.
    * [11/11/2012 9:14:17 AM] : Processing of deployment manifest has successfully completed.
    * [11/11/2012 9:14:17 AM] : Installation of the application has started.
    * [11/11/2012 9:14:17 AM] : Processing of application manifest has successfully completed.
    * [11/11/2012 9:14:18 AM] : Found compatible runtime version 4.0.30319.
    * [11/11/2012 9:14:18 AM] : Request of trust and detection of platform is complete.

    ERROR DETAILS
    Following errors were detected during this operation.
    * [11/11/2012 9:14:18 AM] System.Deployment.Application.InvalidDeploymentException (ManifestLoad)
    – Exception occurred loading manifest from file ClickOnceDemo.exe: the manifest may not be valid or the file could not be opened.
    – Source: System.Deployment
    – Stack trace:
    at System.Deployment.Application.Manifest.AssemblyManifest.ManifestLoadExceptionHelper(Exception exception, String filePath)
    at System.Deployment.Application.Manifest.AssemblyManifest.LoadFromInternalManifestFile(String filePath)
    at System.Deployment.Application.Manifest.AssemblyManifest..ctor(String filePath)
    at System.Deployment.Application.DownloadManager.ProcessDownloadedFile(Object sender, DownloadEventArgs e)
    at System.Deployment.Application.FileDownloader.DownloadModifiedEventHandler.Invoke(Object sender, DownloadEventArgs e)
    at System.Deployment.Application.FileDownloader.OnModified()
    at System.Deployment.Application.SystemNetDownloader.DownloadSingleFile(DownloadQueueItem next)
    at System.Deployment.Application.SystemNetDownloader.DownloadAllFiles()
    at System.Deployment.Application.FileDownloader.Download(SubscriptionState subState)
    at System.Deployment.Application.DownloadManager.DownloadDependencies(SubscriptionState subState, AssemblyManifest deployManifest, AssemblyManifest appManifest, Uri sourceUriBase, String targetDirectory, String group, IDownloadNotification notification, DownloadOptions options)
    at System.Deployment.Application.ApplicationActivator.DownloadApplication(SubscriptionState subState, ActivationDescription actDesc, Int64 transactionId, TempDirectory& downloadTemp)
    at System.Deployment.Application.ApplicationActivator.InstallApplication(SubscriptionState& subState, ActivationDescription actDesc)
    at System.Deployment.Application.ApplicationActivator.PerformDeploymentActivation(Uri activationUri, Boolean isShortcut, String textualSubId, String deploymentProviderUrlFromExtension, BrowserSettings browserSettings, String& errorPageUrl)
    at System.Deployment.Application.ApplicationActivator.ActivateDeploymentWorker(Object state)
    — Inner Exception —
    System.Deployment.Application.DeploymentException (InvalidManifest)
    – Cannot load internal manifest from component file.
    – Source:
    – Stack trace:

    COMPONENT STORE TRANSACTION DETAILS
    No transaction information is available.

  18. The obfuscation targets used here will work with VS 2005 and 2008, but not 2010 or later since some of the components I pulled out of the common targets has changed. Someday I’ll make a post about a more elegant solution for VS 2010, but the short of it is to use inline msbuild tasks. That means you can put code in “.targets” file or I suppose even your project file that will execute during the build. This lets you do complex tasks without having to decipher msbuild or create arcane msbuild task libraries.

    Edit: See new post on obfuscating a ClickOnce in 2010.

  19. @GeorgeWaters: I’m not sure why it isn’t working for you without seeing your project file. Note that this works through VS2010, but may need to be tweaked or changed altogether for later versions. Also make sure that the obfuscation targets are referenced and available for your project. You might try testing with the fully-qualified paths.

  20. For those getting invalid manifest error (i.e. Exception occurred loading manifest from file ClickOnceDemo.exe: the manifest may not be valid or the file could not be opened.) go to the .NET Reactor, open project, and go to Settings > 2. Protection Settins > AntiILDASM / Suppress Decompilation > set Inject Invalid Metadata to False.

    A bit late, but hopefully still relevant- we haven’t found a solution with a better price / performance ratio for obfuscation / licensing.

  21. Hi,

    I have a solution project in VS2008, it has a project for the .exe and several projects for .dll but also 3rd party dll’s, how do I have to handle this in order to obfuscate my assemblies?

    Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *