Skip to content

A C# Source Generator for obfuscating string or byte array values using multi-byte xor encryption.

License

Notifications You must be signed in to change notification settings

Tan90909090/CompileTimeObfuscator

Repository files navigation

CompileTimeObfuscator

NuGet

A C# Source Generator for obfuscating string or byte array values using multi-byte xor encryption.

Sample code:

using System;
using System.Buffers;
using System.Text;
using CompileTimeObfuscator;

namespace Sample;
internal static partial class Program
{
    private static string PlainText() => "This is a plain string";

    [ObfuscatedString("This is an obfuscated string 1")]
    private static partial string ObfuscatedText1();

    [ObfuscatedString("This is an obfuscated string 2")]
    private static partial IMemoryOwner<char> ObfuscatedText2();

    private static ReadOnlySpan<byte> PlainBytes() => "This is a plain bytes"u8;

    [ObfuscatedBytes(new byte[] { (byte)'T', (byte)'h', (byte)'i', (byte)'s', (byte)' ', (byte)'i', (byte)'s', (byte)' ', (byte)'a', (byte)'n', (byte)' ', (byte)'o', (byte)'b', (byte)'f', (byte)'u', (byte)'s', (byte)'c', (byte)'a', (byte)'t', (byte)'e', (byte)'d', (byte)' ', (byte)'b', (byte)'y', (byte)'t', (byte)'e', (byte)'s', (byte)' ', (byte)'1', })]
    private static partial byte[] ObfuscatedBytes1();

    [ObfuscatedBytes(new byte[] { (byte)'T', (byte)'h', (byte)'i', (byte)'s', (byte)' ', (byte)'i', (byte)'s', (byte)' ', (byte)'a', (byte)'n', (byte)' ', (byte)'o', (byte)'b', (byte)'f', (byte)'u', (byte)'s', (byte)'c', (byte)'a', (byte)'t', (byte)'e', (byte)'d', (byte)' ', (byte)'b', (byte)'y', (byte)'t', (byte)'e', (byte)'s', (byte)' ', (byte)'2', })]
    private static partial IMemoryOwner<byte> ObfuscatedBytes2();

    private static void Main()
    {
        Console.WriteLine(PlainText());
        Console.WriteLine(ObfuscatedText1());
        using var memoryOwnerChar = ObfuscatedText2();
        Console.WriteLine(memoryOwnerChar.Memory.Span.ToString());

        Console.WriteLine(Encoding.UTF8.GetString(PlainBytes()));
        Console.WriteLine(Encoding.UTF8.GetString(ObfuscatedBytes1()));
        using var memoryOwnerByte = ObfuscatedBytes2();
        Console.WriteLine(Encoding.UTF8.GetString(memoryOwnerByte.Memory.Span));
    }
}

The result of executing the sample code, and the result of strings command indicating that the obfuscation target string/bytes is not included in a binary as plain text:

Prerequirement

  • Visual Studio 2022 Version 17.3 or higher
  • C# 9 or higher
  • .NET Standard 2.1 or higher

I tested an operation in following environments:

  • Visual Studio 2022 Version 17.5.1 and a project using C# 9 and .NET Core 3.1
  • Visual Studio 2022 Version 17.5.1 and a project using C# 11 and .NET 6

Usage

  1. Add a nuget packege CompileTimeObfuscator.
  2. Use CompileTimeObfuscator.ObfuscatedString and CompileTimeObfuscator.ObfuscatedBytes in your source.
  • CompileTimeObfuscator.ObfuscatedString can obfuscate a string. The return type of the method can be a string or System.Buffers.IMemoryOwner<char>.
  • CompileTimeObfuscator.ObfuscatedString can obfuscate a byte array. The return type of the method can be a byte[] or System.Buffers.IMemoryOwner<byte>.
  • For both attribute cases, I recommend using the IMemoryOwner<T> version if execution efficiency is a priority. string or byte[] versions are wrappers on IMemoryOwner<T> version with additional conversion.
  • For both attribute cases, you can configure xor key length and whether the deobfuscated buffer is to be filled to zero when the IMemoryOwner<T>.Dispose method is called. To configure, add attribute arguments:
using CompileTimeObfuscator;
public partial class C
{
    [ObfuscatedString("test", KeyLength = 1, ClearBufferWhenDispose = false)]
    public static partial string M();
}

Sample of source generation results

The generator generates a following source after initialization:

// <auto-generated/>
#nullable enable

using System;
using System.Buffers;
using System.Diagnostics;

namespace CompileTimeObfuscator;

/// <summary>Obfuscate the specified string to preventing the string from appearing in a metadata. The obfuscated string is deobfuscated at runtime. The method must return <see cref="string"/> or <see cref="IMemoryOwner{T}"/> of type <see cref="char"/>.</summary>
[Conditional("COMPILE_TIME_ONLY")]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
internal sealed class ObfuscatedStringAttribute: Attribute
{
    /// <summary>Initializes a new instance of the <see cref="ObfuscatedStringAttribute"/> with the specified string.</summary>
    /// <param name="value">The string to obfuscate.</param>
    internal ObfuscatedStringAttribute(string value)
    {
    }

    /// <summary>Indicates the key length to obfuscate. A default value is 16.</summary>
    public int KeyLength = 16;

    /// <summary>Indicates whether a deobfuscated buffer will cleared after disposing an <see cref="IMemoryOwner{T}"/> object. A default value is true.</summary>
    public bool ClearBufferWhenDispose = true;
}

/// <summary>Obfuscate the specified bytes to preventing the bytes from appearing in a metadata. The obfuscated bytes is deobfuscated at runtime. The method must return <see cref="byte"/>[] or <see cref="IMemoryOwner{T}"/> of type <see cref="byte"/>.</summary>
[Conditional("COMPILE_TIME_ONLY")]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
internal sealed class ObfuscatedBytesAttribute: Attribute
{
    /// <summary>Initializes a new instance of the <see cref="ObfuscatedBytesAttribute"/> with the specified bytes.</summary>
    /// <param name="value">The bytes to obfuscate.</param>
    internal ObfuscatedBytesAttribute(byte[] value)
    {
    }

    /// <summary>Indicates the key length to obfuscate. A default value is 16.</summary>
    public int KeyLength = 16;

    /// <summary>Indicates whether a deobfuscated buffer will cleared after disposing an <see cref="IMemoryOwner{T}"/> object. A default value is true.</summary>
    public bool ClearBufferWhenDispose = true;
}

internal sealed class ClearableBuffer<T> : IMemoryOwner<T>
{
    private T[]? _array;
    private readonly int _length;
    private readonly bool _clearBufferWhenDispose;

    internal ClearableBuffer(int length, bool clearBufferWhenDispose)
    {
        _array = ArrayPool<T>.Shared.Rent(length);
        _length = length;
        _clearBufferWhenDispose = clearBufferWhenDispose;
    }

    public void Dispose()
    {
        if (_array is null) { return; }

        // Even if clearArray parameter of ArrayPool<T>.Shared.Return is set to true,
        // the array will not be cleared if the array is not returned to the pool.
        // Therefore, clear the array manually here.
        if (_clearBufferWhenDispose)
        {
            _array.AsSpan().Fill(default!);
        }

        ArrayPool<T>.Shared.Return(_array);
        _array = null;
    }

    /// <summary>Returns <see cref="Memory{T}"/> that length is the originally required length. This behavior is different from an <see cref="IMemoryOwner{T}"/> returned from <see cref="MemoryPool{T}.Shared"/>.</summary>
    public Memory<T> Memory
    {
        get
        {
            if (_array is null) { throw new ObjectDisposedException(GetType().FullName); }

            return new Memory<T>(_array, 0, _length);
        }
    }
}

For a following sample code:

using CompileTimeObfuscator;

public partial class C
{
    [ObfuscatedString("test")]
    public static partial string M1();
}

A generator generates a following code, where key takes a different value for each generation:

// <auto-generated/>
#nullable enable

public partial class C {
    public static partial string M1()
    {
        // The compiler optimize a code if `new byte[]{...}` is converted to ReadOnlySpan<byte>. https://vcsjones.dev/csharp-readonly-span-bytes-static/
        System.ReadOnlySpan<byte> obfuscatedValue = new byte[]{117,226,34,33,148,30,41,116};
        System.ReadOnlySpan<byte> key = new byte[]{1,226,71,33,231,30,93,116,226,66,50,253,253,60,3,216};
        using var buffer = new CompileTimeObfuscator.ClearableBuffer<char>(obfuscatedValue.Length / 2, clearBufferWhenDispose: true);
        var span = buffer.Memory.Span;
        for (int i = span.Length - 1; i >= 0; i--)
        {
            byte upper = (byte)(obfuscatedValue[2 * i + 1] ^ key[(2 * i + 1) % key.Length]);
            byte lower = (byte)(obfuscatedValue[2 * i + 0] ^ key[(2 * i + 0) % key.Length]);
            span[i] = (char)(upper << 8 | lower);
        }
        return new string(buffer.Memory.Span);
    }
}

The same is true when the return type is IMemoryOwner<char> or when obfuscating a byte array instead of a string:

using System.Buffers;
using CompileTimeObfuscator;

public partial class C
{
    [ObfuscatedString("test")]
    public static partial IMemoryOwner<char> M2();

    [ObfuscatedBytes(new byte[] { (byte)'t', (byte)'e', (byte)'s', (byte)'t', })]
    public static partial byte[] M3();

    [ObfuscatedBytes(new byte[] { (byte)'t', (byte)'e', (byte)'s', (byte)'t', })]
    public static partial IMemoryOwner<byte> M4();
}
// omitted
public partial class C {
    public static partial global::System.Buffers.IMemoryOwner<char> M2()
    {
        // omitted
        System.ReadOnlySpan<byte> obfuscatedValue = new byte[]{92,228,183,152,126,175,18,190};
        System.ReadOnlySpan<byte> key = new byte[]{40,228,210,152,13,175,102,190,199,226,250,53,66,54,118,164};
        var buffer = new CompileTimeObfuscator.ClearableBuffer<char>(obfuscatedValue.Length / 2, clearBufferWhenDispose: true);
        // omitted
        return buffer;
    }
}
// omitted
public partial class C {
    public static partial byte[] M3()
    {
        // omitted
        System.ReadOnlySpan<byte> obfuscatedValue = new byte[]{26,241,65,188};
        System.ReadOnlySpan<byte> key = new byte[]{110,148,50,200,144,242,236,138,214,152,182,90,190,251,180,67};
        using var buffer = new CompileTimeObfuscator.ClearableBuffer<byte>(obfuscatedValue.Length, clearBufferWhenDispose: true);
        // omitted
        return buffer.Memory.ToArray();
    }
}
// omitted
public partial class C {
    public static partial global::System.Buffers.IMemoryOwner<byte> M4()
    {
        // omitted
        System.ReadOnlySpan<byte> obfuscatedValue = new byte[]{70,203,42,46};
        System.ReadOnlySpan<byte> key = new byte[]{50,174,89,90,38,90,34,100,22,82,253,238,231,239,130,127};
        var buffer = new CompileTimeObfuscator.ClearableBuffer<byte>(obfuscatedValue.Length, clearBufferWhenDispose: true);
        // omitted
        return buffer;
    }
}

Currently, a generator generates a source file for each method.

About

A C# Source Generator for obfuscating string or byte array values using multi-byte xor encryption.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages