「.netアプリケーションのコードを保護する方法」さまざまなフォーラムでよく聞かれる質問の1つです。
最も一般的なオプションは難読化です。 一方では使いやすく、他方では、ソースコードを十分に確実に隠すことができません。 私自身のオプションを提供します。これは、コードが望ましくない著者自身(または権限のある代理人)が使用することになっているユーティリティに適しています。
保護は、対称キーを使用したアセンブリの暗号化と、アプリケーション操作中の動的な復号化に基づいています。 暗号化キーは、展開段階でユーザーによって決定され、起動時にパスワードとして入力されます。
すべてを段階に分けます。
- 予備作業
- パスワード入力
- アセンブリ復号化
- アセンブリ負荷オーバーライド
- アプリケーションの起動
- ケーキの上のチェリー
- 追加のプロジェクト設定
そして、別のアイテムが行きます:
- アセンブリの展開と暗号化
予備作業
起動する前に何らかの方法でアプリケーションを復号化する必要があるため、この感謝のない仕事を引き受けるラッパーを作成します。
ラッパーは通常のコンソールアプリケーションになります。
パスワード入力
入力するパスワードはどこかに保存する必要があります。 通常、文字列はこれらの目的に使用されますが、.netでは不変です。つまり、入力したパスワードはデバッガーによって簡単に取り出すことができます。 これを回避するために、暗号化された形式でデータを保存する特別なSecureStringクラス(System.Security名前空間)を使用します。
読み取りパスワードprivate static bool ReadPassword() { ConsoleKeyInfo consoleKey = Console.ReadKey(true); while (consoleKey.Key != ConsoleKey.Enter) { if (consoleKey.Key == ConsoleKey.Escape) { return false; } _password.AppendChar(consoleKey.KeyChar); consoleKey = Console.ReadKey(true); } return _password.Length > 0; }
入力すると、入力文字は画面に表示されず、Enterキーを押すと入力が終了します。
_password-ユーザーが入力したパスワードが保存されるクラスフィールド。
アセンブリ復号化
暗号化は、対称と非対称の2つのタイプに分けられます。 対称では、同じキーが暗号化と復号化に使用され、非対称で異なるキーが使用されます。
別のキーは必要ないため、対称暗号化に焦点を当てます。
暗号化されたものを解読するには、次の3つのコンポーネントが必要です。
- 暗号化されたデータ。
- 暗号化に使用されるキー。
- 初期化ベクトル(IV)-暗号化の最初の段階で使用された未分類のデータ。
初期化ベクトルは秘密ではないため、暗号化されたデータと一緒に保存できます。
作業を容易にするために、特別なクラスCryptedDataを作成します。
クラスcrypteddata public sealed class CryptedData {
AESアルゴリズムを使用して暗号化します。 便宜上、低レベルのラッパーを作成します。
クラスAesCryptography public static class AesCryptography {
モジュール性を高めるために、抽象化のレベルをもう1つ追加します。
CryptographyHelperクラス internal static class CryptographyHelper {
最後のメソッドGetKeyにはいくつかの魔法があります。
最初のポイントは、キーの長さが128、192、または256ビットであることです。 また、実行するパスワードは任意の長さの文字列にすることができます。 したがって、パスワード文字列をsha256でハッシュし、必要な長さを取得するだけです。
2番目の魔法は突然で、SecureStringに関連しています。 このクラスは書き込み専用です。その内容を取得するには、安全でないコードを使用する必要があります。
クラスInsecureString [CLSCompliant(false)] public sealed class InsecureString : IDisposable, IEnumerable<char> { internal InsecureString(SecureString secureString) { _secureString = secureString; Initialize(); } public string Value { get; private set; } private readonly SecureString _secureString; private GCHandle _gcHandle; #if !DEBUG [DebuggerHidden] #endif private void Initialize() { unsafe { // We are about to create an unencrypted version of our sensitive string and store it in memory. // Don't let anyone (GC) make a copy. // To do this, create a new gc handle so we can "pin" the memory. // The gc handle will be pinned and later, we will put info in this string. _gcHandle = new GCHandle(); // insecurePointer will be temporarily used to access the SecureString IntPtr insecurePointer = IntPtr.Zero; RuntimeHelpers.TryCode code = delegate { // create a new string of appropriate length that is filled with 0's Value = new string((char)0, _secureString.Length); // Even though we are in the ExecuteCodeWithGuaranteedCleanup, processing can be interupted. // We need to make sure nothing happens between when memory is allocated and // when _gcHandle has been assigned the value. Otherwise, we can't cleanup later. // PrepareConstrainedRegions is better than a try/catch. Not even a threadexception will interupt this processing. // A CER is not the same as ExecuteCodeWithGuaranteedCleanup. A CER does not have a cleanup. Action alloc = delegate { _gcHandle = GCHandle.Alloc(Value, GCHandleType.Pinned); }; ExecuteInConstrainedRegion(alloc); // Even though we are in the ExecuteCodeWithGuaranteedCleanup, processing can be interupted. // We need to make sure nothing happens between when memory is allocated and // when insecurePointer has been assigned the value. Otherwise, we can't cleanup later. // PrepareConstrainedRegions is better than a try/catch. Not even a threadexception will interupt this processing. // A CER is not the same as ExecuteCodeWithGuaranteedCleanup. A CER does not have a cleanup. Action toBSTR = delegate { insecurePointer = Marshal.SecureStringToBSTR(_secureString); }; ExecuteInConstrainedRegion(toBSTR); // get a pointer to our new "pinned" string char* value = (char*)_gcHandle.AddrOfPinnedObject(); // get a pointer to the unencrypted string char* charPointer = (char*)insecurePointer; // copy for (int i = 0; i < _secureString.Length; i++) { value[i] = charPointer[i]; } }; RuntimeHelpers.CleanupCode cleanup = delegate { // insecurePointer was temporarily used to access the securestring // set the string to all 0's and then clean it up. this is important. // this prevents sniffers from seeing the sensitive info as it is cleaned up. if (insecurePointer != IntPtr.Zero) { Marshal.ZeroFreeBSTR(insecurePointer); } }; // Better than a try/catch. Not even a threadexception will bypass the cleanup code RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(code, cleanup, null); } } #if !DEBUG [DebuggerHidden] #endif public void Dispose() { unsafe { // we have created an insecurestring if (_gcHandle.IsAllocated) { // get the address of our gchandle and set all chars to 0's char* insecurePointer = (char*)_gcHandle.AddrOfPinnedObject(); for (int i = 0; i < _secureString.Length; i++) { insecurePointer[i] = (char)0; } #if DEBUG string disposed = "¡DISPOSED¡"; disposed = disposed.Substring(0, Math.Min(disposed.Length, _secureString.Length)); for (int i = 0; i < disposed.Length; ++i) { insecurePointer[i] = disposed[i]; } #endif _gcHandle.Free(); } } } public IEnumerator<char> GetEnumerator() { if (_gcHandle.IsAllocated) { return Value.GetEnumerator(); } else { return new List<char>().GetEnumerator(); } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } private static void ExecuteInConstrainedRegion(Action action) { RuntimeHelpers.PrepareConstrainedRegions(); try { } finally { action(); } } }
このコードはどうなりますか?
- 2つのコードが用意されています。1つはすべての作業を行うメインで、2つ目は例外の場合のクリーニングコードです。
- メインコードは、SecureString値が格納され、Disposeメソッドで強制的にクリアされる新しい行を作成します。
- SecureStringからポインターを介して内部文字列にデータをコピーし、内部文字列をガベージコレクターにロックします。
- 内部行を通じて、SecureStringデータを取得できます。
Disposeメソッドは、ポインターを介して内部文字列を上書きします。
デバッガが保護された文字列データを読み取るリスクを最小限に抑えるために、InsecureStringインスタンスの「存続期間」をできる限り短くすることが重要です。
ハッシュを取得するにはInsecureStringインスタンスのみが必要なので、上記のハッシュはこれに役立ちます。その後、元のSecureString値を取得できないハッシュ自体を操作します。
アセンブリ負荷オーバーライド
暗号化されたアセンブリを使用する予定なので、それらをダウンロードするための標準メカニズムを変更する必要があります。
アプリケーションドメイン(AppDomain)は、特別なAssemblyResolveイベントを介してアセンブリをロードします。
Mainを入力するまでに既に必要な場合があるため、タイプコンストラクターの早い段階でメカニズムを再定義します。 同じ場所で、便宜上、例外処理を固定します。
ハンドラー接続 static Program() { AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => Console.WriteLine(eventArgs.ExceptionObject.ToString()); AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve; _password = new SecureString(); }
アプリケーションの起動
ローンチにより、すべてが非常にシンプルになりました。
アプリケーションの起動 private static void RunApplication() { SetConsoleWindowVisibility(false); App app = new App(); MainWindow window = new MainWindow(); app.Run(window); }
ケーキの上のチェリー
2つのポイントが残ります。
- アプリケーションが表示される前にコンソールウィンドウを非表示にします。
- アプリケーションの可用性をマスクする
コンソールウィンドウを非表示にする
管理されていないメソッドをいくつかインポートする必要があります
外部機能を接続します [DllImport("user32.dll")] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
ウィンドウ自体を非表示にします
アプリケーションマスキング
起動直後にエラーを出すことで、アプリケーションをマスクできます
メイン(文字列[]引数) [STAThread] public static void Main(string[] args) {
これが逆アセンブラーに対して機能しないことは明らかですが、アイドラーを奪います。
追加のプロジェクト設定
正常に動作させるには、ブートローダーアセンブリで安全でないコードを許可する必要があります。 これを行う最も簡単な方法は、プロジェクト設定を使用することです。

アセンブリの展開と暗号化
コンパイル後すぐにアセンブリを暗号化する必要があります。 このための関数をいくつか作成します。
元のファイルを上書きするために、ワイパーヘルパークラスを使用します。このクラスは、複数のパスでランダムデータでファイルを上書きし、その後削除します。
クラスワイパー internal sealed class Wiper {
あとがき
明らかに、このような保護の弱点は、アプリケーションを使用するすべての人のパスワードを知る必要があることです。
また、アプリケーションを使用する過程で逆アセンブラーがアセンブリコードを取得できることを理解することも重要です。
しかし、あなた自身が使用するユーティリティの機能をpr索好きな目から隠す必要がある場合、このアプローチは正当化されます。
素材