net索好きな目から.netアプリケーションを保護する

「.netアプリケーションのコードを保護する方法」さまざまなフォーラムでよく聞かれる質問の1つです。



最も一般的なオプションは難読化です。 一方では使いやすく、他方では、ソースコードを十分に確実に隠すことができません。 私自身のオプションを提供します。これは、コードが望ましくない著者自身(または権限のある代理人)が使用することになっているユーティリティに適しています。



保護は、対称キーを使用したアセンブリの暗号化と、アプリケーション操作中の動的な復号化に基づいています。 暗号化キーは、展開段階でユーザーによって決定され、起動時にパスワードとして入力されます。



すべてを段階に分けます。

  1. 予備作業
  2. パスワード入力
  3. アセンブリ復号化
  4. アセンブリ負荷オーバーライド
  5. アプリケーションの起動
  6. ケーキの上のチェリー
  7. 追加のプロジェクト設定


そして、別のアイテムが行きます:

  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つのコンポーネントが必要です。



初期化ベクトルは秘密ではないため、暗号化されたデータと一緒に保存できます。



作業を容易にするために、特別なクラスCryptedDataを作成します。

クラスcrypteddata
 public sealed class CryptedData { /// <summary> ///     . /// </summary> public byte[] IV { get; set; } /// <summary> ///    . /// </summary> public byte[] EncryptedSource { get; set; } /// <summary> ///          . /// </summary> public byte[] ToArray() { using (MemoryStream ms = new MemoryStream()) { Store(ms); return ms.ToArray(); } } /// <summary> ///        . /// </summary> /// <param name="output">,     .</param> public void Store(Stream output) { Validate(this); if (!output.CanWrite) { throw new ArgumentException("    ", "output"); } using (BinaryWriter bw = new BinaryWriter(output)) { bw.Write(IV.Length); bw.Write(IV); bw.Write(EncryptedSource.Length); bw.Write(EncryptedSource); } } /// <summary> ///      . /// </summary> /// <param name="input">   .</param> public static CryptedData Create(Stream input) { if (!input.CanRead) { throw new ArgumentException("    ", "input"); } CryptedData data = new CryptedData(); using (BinaryReader reader = new BinaryReader(input)) { int ivLength = reader.ReadInt32(); data.IV = reader.ReadBytes(ivLength); int sourceLength = reader.ReadInt32(); data.EncryptedSource = reader.ReadBytes(sourceLength); } Validate(data); return data; } /// <summary> ///   . /// </summary> /// <param name="data">,   .</param> private static void Validate(CryptedData data) { if (data.IV == null || data.IV.Length == 0) { throw new ArgumentException("IV    "); } if (data.IV.Length > byte.MaxValue) { throw new ArgumentException(" IV     " + byte.MaxValue); } if (data.EncryptedSource == null || data.EncryptedSource.Length == 0) { throw new ArgumentException("Souce    "); } } }
      
      







AESアルゴリズムを使用して暗号化します。 便宜上、低レベルのラッパーを作成します。

クラスAesCryptography
 public static class AesCryptography { /// <summary> ///   . /// </summary> /// <returns></returns> internal static byte[] CreateIv() { using (AesManaged aes = new AesManaged()) { aes.GenerateIV(); return aes.IV; } } /// <summary> ///  . /// </summary> /// <param name="source">.</param> /// <param name="key"> .</param> /// <param name="iv"> .</param> /// <returns> .</returns> internal static byte[] Encrypt(byte[] source, byte[] key, byte[] iv) { Validate(source, key, iv); using (AesManaged aes = new AesManaged()) { using (ICryptoTransform transform = aes.CreateEncryptor(key, iv)) { using (MemoryStream ms = new MemoryStream()) { using (CryptoStream cs = new CryptoStream(ms, transform, CryptoStreamMode.Write)) { cs.Write(source, 0, source.Length); } byte[] encryptedBytes = ms.ToArray(); return encryptedBytes; } } } } /// <summary> ///  . /// </summary> /// <param name="source">  .</param> /// <param name="key"> .</param> /// <param name="iv"> .</param> /// <returns> .</returns> internal static byte[] Decrypt(byte[] source, byte[] key, byte[] iv) { Validate(source, key, iv); using (AesManaged aes = new AesManaged()) { using (ICryptoTransform transform = aes.CreateDecryptor(key, iv)) { using (MemoryStream ms = new MemoryStream(source)) { using (CryptoStream cs = new CryptoStream(ms, transform, CryptoStreamMode.Read)) { List<byte> bytes = new List<byte>(1024); int b; while ((b = cs.ReadByte()) != -1) { bytes.Add((byte)b); } return bytes.ToArray(); } } } } } /// <summary> ///  . /// </summary> /// <param name="source">.</param> /// <param name="key"> .</param> /// <param name="iv"> .</param> private static void Validate(byte[] source, byte[] key, byte[] iv) { if (source == null) { throw new ArgumentNullException("source"); } else if (source.Length == 0) { throw new ArgumentException("    ", "source"); } if (key == null) { throw new ArgumentNullException("key"); } else if (key.Length == 0) { throw new ArgumentException("    ", "key"); } if (key.Length.IsOneOf(16, 24, 32) == false) { throw new ArgumentException("    128, 192  256  (16, 24, 32 )", "key"); } if (iv == null) { throw new ArgumentNullException("iv"); } else if (iv.Length != 16) { throw new ArgumentException("     128  (16 )", "iv"); } } public static bool IsOneOf<T>(this T value, params T[] values) { return value.IsOneOf(values as IEnumerable<T>); } public static bool IsOneOf<T>(this T value, IEnumerable<T> values) { if (values == null) { throw new ArgumentNullException("values"); } foreach (T t in values) { if (Equals(t, value)) { return true; } } return false; } }
      
      







モジュール性を高めるために、抽象化のレベルをもう1つ追加します。

CryptographyHelperクラス
 internal static class CryptographyHelper { /// <summary> ///  . /// </summary> /// <param name="source"> .</param> /// <param name="password"> .</param> /// <returns></returns> public static CryptedData Encrypt(byte[] source, SecureString password) { byte[] iv = AesCryptography.CreateIv(); byte[] key = GetKey(password); byte[] encrypted = AesCryptography.Encrypt(source, key, iv); return new CryptedData() { EncryptedSource = encrypted, IV = iv }; } /// <summary> ///  . /// </summary> /// <param name="data">,   .</param> /// <param name="password"> .</param> /// <returns> .</returns> public static byte[] Decrypt(CryptedData data, SecureString password) { byte[] key = GetKey(password); byte[] decrypted = AesCryptography.Decrypt(data.EncryptedSource, key, data.IV); return decrypted; } /// <summary> ///     . /// </summary> /// <param name="key">,   .</param> /// <returns></returns> private static byte[] GetKey(SecureString key) { using (InsecureString insecure = new InsecureString(key)) { using (SHA256Managed sha256 = new SHA256Managed()) { byte[] rawKey = new byte[key.Length]; int i = 0; foreach (char c in insecure) { rawKey[i++] = Convert.ToByte(c); } byte[] hashedKey = sha256.ComputeHash(rawKey); Array.Clear(rawKey, 0, rawKey.Length); return hashedKey; } } } }
      
      







最後のメソッド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(); } } }
      
      







このコードはどうなりますか?

  1. 2つのコードが用意されています。1つはすべての作業を行うメインで、2つ目は例外の場合のクリーニングコードです。
  2. メインコードは、SecureString値が格納され、Disposeメソッドで強制的にクリアされる新しい行を作成します。
  3. SecureStringからポインターを介して内部文字列にデータをコピーし、内部文字列をガベージコレクターにロックします。
  4. 内部行を通じて、SecureStringデータを取得できます。


Disposeメソッドは、ポインターを介して内部文字列を上書きします。



デバッガが保護された文字列データを読み取るリスクを最小限に抑えるために、InsecureStringインスタンスの「存続期間」をできる限り短くすることが重要です。



ハッシュを取得するにはInsecureStringインスタンスのみが必要なので、上記のハッシュはこれに役立ちます。その後、元のSecureString値を取得できないハッシュ自体を操作します。



アセンブリ負荷オーバーライド



暗号化されたアセンブリを使用する予定なので、それらをダウンロードするための標準メカニズムを変更する必要があります。

アプリケーションドメイン(AppDomain)は、特別なAssemblyResolveイベントを介してアセンブリをロードします。

AssemblyResolveハンドラー
 /// <summary> ///   . /// </summary> private static Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args) { string[] fileParts = args.Name.Split(",".ToCharArray()); string assemblyPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileParts[0] + ".edll"); string symbolsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileParts[0] + ".epdb"); byte[] assemblyBytes = null, symbolsBytes = null; if (File.Exists(assemblyPath)) { assemblyBytes = DecryptFile(assemblyPath); } if (File.Exists(symbolsPath)) { symbolsBytes = DecryptFile(symbolsPath); } return Assembly.Load(assemblyBytes, symbolsBytes); } /// <summary> ///  . /// </summary> /// <param name="path">  .</param> /// <returns>  .</returns> private static byte[] DecryptFile(string path) { CryptedData data; using (FileStream fs = File.OpenRead(path)) { data = CryptedData.Create(fs); } byte[] bytes = CryptographyHelper.Decrypt(data, _password); return bytes; }
      
      









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つのポイントが残ります。

  1. アプリケーションが表示される前にコンソールウィンドウを非表示にします。
  2. アプリケーションの可用性をマスクする


コンソールウィンドウを非表示にする


管理されていないメソッドをいくつかインポートする必要があります

外部機能を接続します
 [DllImport("user32.dll")] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
      
      







ウィンドウ自体を非表示にします

コンソールを非表示にする
 /// <summary> ///    . /// </summary> /// <param name="visible">  .</param> private static void SetConsoleWindowVisibility(bool visible) { IntPtr hWnd = FindWindow(null, Console.Title); if (hWnd != IntPtr.Zero) { if (visible) ShowWindow(hWnd, 1); //1 = SW_SHOWNORMAL else ShowWindow(hWnd, 0); //0 = SW_HIDE } }
      
      







アプリケーションマスキング


起動直後にエラーを出すことで、アプリケーションをマスクできます

メイン(文字列[]引数)
 [STAThread] public static void Main(string[] args) { //     try { ArgumentException ex = new ArgumentException("There is not enough data to start application"); throw ex; } catch (ArgumentException ex) { Console.WriteLine(ex.ToString()); Console.WriteLine("Press Esc to exit"); } if (!ReadPassword()) return; RunApplication(); }
      
      







これが逆アセンブラーに対して機能しないことは明らかですが、アイドラーを奪います。



追加のプロジェクト設定



正常に動作させるには、ブートローダーアセンブリで安全でないコードを許可する必要があります。 これを行う最も簡単な方法は、プロジェクト設定を使用することです。







アセンブリの展開と暗号化



コンパイル後すぐにアセンブリを暗号化する必要があります。 このための関数をいくつか作成します。

アセンブリ暗号化
 /// <summary> ///      . /// </summary> private static void EncryptAssemblies() { Wiper wiper = new Wiper(); foreach (string file in Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll")) { byte[] source = File.ReadAllBytes(file); CryptedData crypted = CryptographyHelper.Encrypt(source, _password); string resultPath = Path.Combine(Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file) + ".edll"); File.WriteAllBytes(resultPath, crypted.ToArray()); //   wiper.WipeFile(file, 3); //File.Delete(file); } string currentAssemblyName = Assembly.GetEntryAssembly().GetName().Name; foreach (string file in Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.pdb")) { if (Path.GetFileNameWithoutExtension(file) == currentAssemblyName) continue; byte[] source = File.ReadAllBytes(file); CryptedData crypted = CryptographyHelper.Encrypt(source, _password); string resultPath = Path.Combine(Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file) + ".epdb"); File.WriteAllBytes(resultPath, crypted.ToArray()); //   wiper.WipeFile(file, 3); } }
      
      







元のファイルを上書きするために、ワイパーヘルパークラスを使用します。このクラスは、複数のパスでランダムデータでファイルを上書きし、その後削除します。

クラスワイパー
 internal sealed class Wiper { /// <summary> /// Deletes a file in a secure way by overwriting it with /// random garbage data n times. /// </summary> /// <param name="filename">Full path of the file to be deleted</param> /// <param name="timesToWrite">Specifies the number of times the file should be overwritten</param> public void WipeFile(string filename, int timesToWrite) { if (File.Exists(filename)) { // Set the files attributes to normal in case it's read-only. File.SetAttributes(filename, FileAttributes.Normal); // Calculate the total number of sectors in the file. double sectors = Math.Ceiling(new FileInfo(filename).Length / 512.0); // Create a dummy-buffer the size of a sector. byte[] dummyBuffer = new byte[512]; // Create a cryptographic Random Number Generator. // This is what I use to create the garbage data. using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) { // Open a FileStream to the file. FileStream inputStream = new FileStream(filename, FileMode.Open); for (int currentPass = 0; currentPass < timesToWrite; currentPass++) { // Go to the beginning of the stream inputStream.Position = 0; // Loop all sectors for (int sectorsWritten = 0; sectorsWritten < sectors; sectorsWritten++) { // Fill the dummy-buffer with random data rng.GetBytes(dummyBuffer); // Write it to the stream inputStream.Write(dummyBuffer, 0, dummyBuffer.Length); } } // Truncate the file to 0 bytes. // This will hide the original file-length if you try to recover the file. inputStream.SetLength(0); // Close the stream. inputStream.Close(); // As an extra precaution I change the dates of the file so the // original dates are hidden if you try to recover the file. DateTime dt = new DateTime(2037, 1, 1, 0, 0, 0); File.SetCreationTime(filename, dt); File.SetLastAccessTime(filename, dt); File.SetLastWriteTime(filename, dt); File.SetCreationTimeUtc(filename, dt); File.SetLastAccessTimeUtc(filename, dt); File.SetLastWriteTimeUtc(filename, dt); // Finally, delete the file File.Delete(filename); } } } }
      
      







あとがき



明らかに、このような保護の弱点は、アプリケーションを使用するすべての人のパスワードを知る必要があることです。

また、アプリケーションを使用する過程で逆アセンブラーがアセンブリコードを取得できることを理解することも重要です。



しかし、あなた自身が使用するユーティリティの機能をpr索好きな目から隠す必要がある場合、このアプローチは正当化されます。



素材






All Articles