XenocodeRCE / neo-ConfuserEx

Updated ConfuserEX, an open-source, free obfuscator for .NET applications
http://yck1509.github.io/ConfuserEx/
Other
746 stars 89 forks source link

internal const string is null when obfuscating Release build #36

Open TooMuchBlue opened 5 years ago

TooMuchBlue commented 5 years ago

Summary: An internal const string returns null when compiled for Release and obfuscated. It behaves normally when compiled for Debug and obfuscated, and when compiled for Release and not obfuscated.

Description: The code below is a portion of a shared helper library used in multiple projects. The library is compiled into a single DLL which I want to obfuscate to protect the propietary code inside, but still use it across a variety of projects. Most of the code in the library is static.

MyBuildInfo.cs:

namespace MyBuildInfo
{
  internal static class Details
  {
    internal const string AssemblyFileVersion = "1.0.13533.1000";
  }
}

Common.cs:

namespace My.Library
{
  public static class Common
  {
    public static string CommonAssemblyFileVersion()
    {
      return MyBuildInfo.Details.AssemblyFileVersion;
    }
  }
}

When used in a larger project, we often want to display build/configuration/version information about the library to the user of the outer project to aid in error reporting. As such, the library has methods like CommonAssemblyFileVersion() which report build information about the shared library. MyBuildInfo is autogenerated from the source code repository and is used in other projects (including the outer project). The library provides the method in order to avoid name collisions and confusion.

I'm using a .crproj file to control obfuscation options. I don't have any [Obfuscate] attributes in the library project. My crproj uses template "none" and at least this protection:

<protection id="constants">
  <argument name="mode" value="normal" />
  <argument name="elements" value="S" />
</protection>

When I compile for Debug, then obfuscate, CommonAssemblyFileVersion() returns the expected value. When I compile for Release, then obfuscate, the same method returns null. (Naturally, the method returns the correct value at all times when not obfuscated.)

I'm calling the library like this:

  string x = Common.CommonAssemblyConfiguration();
  txtConfiguration.Text = string.Format("{0} {1}", x.Length, x);

When this fails, I get a NullReferenceException on x.Length. (I could deal with that if it was normal, but it doesn't help me get the data I need.)

Here are the Debug and Release build configuration settings from my csproj:

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>..\bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <PlatformTarget>AnyCPU</PlatformTarget>
    <RunCodeAnalysis>true</RunCodeAnalysis>
    <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
    <DocumentationFile>..\bin\Debug\McGladrey.Library.XML</DocumentationFile>
    <Prefer32Bit>false</Prefer32Bit>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>..\bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DocumentationFile>..\bin\Release\McGladrey.Library.XML</DocumentationFile>
    <Prefer32Bit>false</Prefer32Bit>
  </PropertyGroup>

Additional details:

TooMuchBlue commented 5 years ago

IL from the obfuscated release build. (Pardon the right-to-left text. Yay for Unicode!)

.class public auto ansi abstract sealed beforefieldinit My.Library.Common
    extends [mscorlib]System.Object
{
    // Methods
    .method public hidebysig static 
        string CommonAssemblyFileVersion () cil managed 
    {
        // Method begins at RVA 0x3ec0
        // Code size 13 (0xd)
        .maxstack 8

        // return global::<Module>.‏‏‪‮​‭‪‫‬​‎​‏‫‮‍‎‎​‍‌‮‪‮<string>(1283113728u);
        IL_0000: ldc.i4 1283113728
        // (no C# code)
        IL_0005: br.s IL_0007

        IL_0007: call !!0 '<Module>'::'‏‏‪‮​‭‪‫‬​‎​‏‫‮‍‎‎​‍‌‮‪‮'<string>(uint32)
        IL_000c: ret
    } // end of method Common::CommonAssemblyFileVersion
} // end of class My.Library.Common
mkaring commented 5 years ago

I am currently tracking this error in my fork as well. Are you able to produce a minimal working example to produce the error? I believe that there is a problem in the decryption method for the protected constants that causes the method to return null for some invocations. How ever I believe that it depends on the other protected constants in the assembly, because the issue seems to disappear in case the affected code is extracted into an minimal example, even if the code of the affected method still looks the same.

TooMuchBlue commented 5 years ago

I would love to produce a MWE, but it's not going to happen this week, probably not the next.

What I can do is produce the IL for the Debug and Release builds, pre-obfuscation. Some of the optimization that happens in a Release build will remove interim variables and rearrange code. Perhaps that's a contributing factor.

If you think this is valuable, I'll find the time to generate the IL and add it here.

mkaring commented 5 years ago

If you could attach or send the obfuscated assembly or extract the IL code of the string decoder along with it's broken invocation and the attached data array, that would help in case the problem is where I expect it to be.