stackprobe7s_memo

何処にも披露する見込みの無いものを書き落とす場所

C#のソースコードを難読化するツールを自作した(C#)

Java, C# で書かれたコードを (Java, C#コンパイラで) コンパイルした実行ファイルの中身は VM 用の中間言語であり、逆コンパイルが容易であることが知られていて、そのリバースエンジニアリング対策として難読化があります。
C# で書いたゲームを公開するに当たって難読化を実施しました。
使用したツールは ConfuserEx です。フリーで手に入るものとしては高い性能と実績があるようで、(他のツールと比較したところで結果の良し悪しは分からないので、これやっとけば十分だろくらいの判断で...)これを使うことにしました。ビルドは MSBuild を使ってコマンドラインから行っているので、ConfuserEx もコマンドから呼び出してバッチに組み込めれば良いのですが、ありがたいことに ConfuserEx はコマンドに対応しています。
 
コマンドから呼び出す用のツールがこのへん
https://github.com/soleil-taruto/Factory/blob/main/SubTools/CallConfuserCLI.c
設定ファイルがこのへん
https://github.com/soleil-taruto/Factory/blob/main/SubTools/CallConfuserCLI_Proj.txt
https://github.com/soleil-taruto/Factory/blob/main/SubTools/CallConfuserCLI_Proj.txt_maximum.txt
https://github.com/soleil-taruto/Factory/blob/main/SubTools/CallConfuserCLI_Proj.txt_minimum.txt
https://github.com/soleil-taruto/Factory/blob/main/SubTools/CallConfuserCLI_Proj.txt_normal.txt
https://github.com/soleil-taruto/Factory/blob/main/SubTools/CallConfuserCLI_Proj.txt_rename-only-letters.txt
https://github.com/soleil-taruto/Factory/blob/main/SubTools/CallConfuserCLI_Proj.txt_rename-only-letters_constants.txt
https://github.com/soleil-taruto/Factory/blob/main/SubTools/CallConfuserCLI_Proj.txt_rename-only-unicode.txt
https://github.com/soleil-taruto/Factory/blob/main/SubTools/CallConfuserCLI_Proj.txt_rename-only-unicode_constants.txt
 
難読化によりプログラムが正常に動かなくなることがあるという情報も Web 上ありますが、私の場合は preset = maximum でも特に問題は見つかりませんでした。識別子を軒並みリネームするせいで enum の名前列挙とかリフレクションで問題が起こるのかもしれません。そのあたり意識してリフレクション等行わず、シンプルに実装したのが功を奏したのかもしれません。
難読化は無事に成功しましたが、難読化した実行ファイルをほとんどのウィルス対策ソフトがウィルスと見なすようになるという問題にぶち当たりました。
VirusTotal で判定するとこんな感じ

ConfuserExで難読化したexeをVirusTotalで判定

どうやらウィルス自身の難読化に ConfuserEx がよく使われているようで、ConfuserEx で難読化したというだけでウィルスと見なされる (ウィルス嫌気が非常に高くなる) ようです。難読化レベルなど設定をいじると多少嫌気は下がりますが、やはり高いまま。ちゃんと調べてはないんですが、他の難読化ツールでも同じなんじゃないかと。そこでふと思ったことが
ウィルス対策ソフトは実行ファイルが (ConfuserEx によって) 難読化されていると判断できたからウィルスと見なしたのであって、それは恐らく ConfuserEx は実行ファイルのバイナリをいじるから何らかの痕跡 (普通にビルドしたのでは有り得ない部分) が生じてしまい、それを検出しているのだと思います。だとすれば、ソースコードを難読化して、それを普通にビルドしたらウィルスと見なされなくなるんじゃないか。ということ
そんなわけで、どうせならとソースコードの難読化ツールを作ってみました。
当初は Roslyn によるコード解析を経て難読化コードを吐き出す汎用的な難読化ツールを目指しましたが、Roslyn の解析結果が思いの外ややこしかったのと、そういう機能を使って実装するのが何か負けた気がするので、なるべく自力でやることにしました。自分のコーディング規約前提なので自分専用ツール止まりだけど、今回はそれで十分。


難読化前のソース (プロジェクトフォルダの中身) がこれ
https://github.com/soleil-taruto/Hatena/tree/main/a20201226/BeforeConfuse/Elsa20200001
 
難読化後のソースがこれ (毎回違うコードが出力される。比較のため 3 回実施した)
1回目
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confused_01/tmpsol/Elsa20200001
2回目
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confused_02/tmpsol/Elsa20200001
3回目
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confused_03/tmpsol/Elsa20200001
 
- - -
 
難読化前のソースをビルドした実行ファイルをデコンパイルしたプロジェクト (dnSpy_v6.0.5 を使用)
https://github.com/soleil-taruto/Hatena/tree/main/a20201226/Decompile/BeforeConfuse/Elsa20200001
 
難読化後のソースをビルドした実行ファイルをデコンパイルしたプロジェクト (dnSpy_v6.0.5 を使用)
1回目
https://github.com/soleil-taruto/Hatena/tree/main/a20201226/Decompile/Confused_01/Elsa20200001
2回目
https://github.com/soleil-taruto/Hatena/tree/main/a20201226/Decompile/Confused_02/Elsa20200001
3回目
https://github.com/soleil-taruto/Hatena/tree/main/a20201226/Decompile/Confused_03/Elsa20200001
 
- - -
 
同じクラスの比較
難読化前
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/BeforeConfuse/Elsa20200001/Games/TitleMenu.cs
難読化後の同じクラス
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confused_01/tmpsol/Elsa20200001/ReloadUnknownLogeEnvironment.cs
 
ぱっと見では同じクラスと分からないくらいに目眩ましできていると思います。
 
しかしながら、当ツールによる難読化は ConfuserEx が行うような難読化と比べれば弱い、安易な難読化と言えます。
ただ、難読化したからと言ってリバースエンジニアリングが不可能になった訳ではありません。ConfuserEx でも同じです。重要なのはリバースエンジニアリングによって得られるものを、得るための労力が上回れば良い。それは満たしていると思ったので良しとしました。リバースエンジニアリングについては知見が無いので、実際に解読しにくいのか確信はありませんが、恐らくこれを解読する腕のある PG なら同じものを自分で書くんじゃないかなと。
 
因みに VirusTotal の判定結果、犯罪係数 0 なんてクリアな色相...

自作ツールで難読化したexeをVirusTotalで判定

実装

難読化ツールのソース (プロジェクトフォルダの中身) はこちら
https://github.com/soleil-taruto/Hatena/tree/main/a20201226/Confuser/Claes20200001
しばらくは現役で使い続けるつもりなので、説明はざっくりで
やっていることを順番に書いていきます。

クラス配置の平滑化

名前空間ヒエラルキーを排除して、全てのクラス・構造体をデフォルトの名前空間 (Charlotte) の直下に置きます。
実装はここ
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSFile.cs#L25-L43

コメントの削除

C系コメント /* ~ */ C++系コメント // ~ 改行 は不要だし邪魔なので削除します。
実装はここ
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSFile.cs#L45-L145

プリプロセッサディレクティブの削除

コメントと同様、抑止されたコード #if false ~ #endif# ~ 改行 の記述自体は不要だし邪魔なので削除します。
想定しているのは #if #elif #else #endif #region #endregion 条件として true false !true !false のみ
実装はここ
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSFile.cs#L188-L225

アクセス修飾子の改変

スコープを広げたり制約を解除する方向へならアクセス修飾子等を改変しても動作に影響は無いはずなので、private, protected はなるべく public に、static classclass に、const はなるべく static に、readonly はなるべく取っ払うということをやっています。
const を static にしたら、定数が変数になって遅くなる懸念はありますが、今回はその程度は度外視。
実装はここ
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSFile.cs#L314-L350

リテラル文字とリテラル文字列の難読化

リテラル文字については 'A' といった記述を (char)65 といった記述に置き換えます。これは難読化というより、後続の処理でリテラルが問題にならないようにするための処置です。
リテラル文字列は大きく変えます。詳しい説明は省きますが、具体的には以下のように...
 
難読化前

	public static string DECIMAL = "0123456789";

 
難読化後

	public static string DetectExecutableAoedeMillisecond = ReferRemoteSinopeAllocation(); // 名前が変更されているが、このフィールドが DECIMAL
	public static string ContactTrueLawrenciumSession;

	public static string ReferRemoteSinopeAllocation()
	{
		if (ContactTrueLawrenciumSession == null)
		{
			ContactTrueLawrenciumSession = HighlightLocalYmirPayload();
		}
		return ContactTrueLawrenciumSession;
	}

	public static string HighlightLocalYmirPayload()
	{
		return new string(DumpConstantPapayaFile()
			.Where(DocumentMockEarthThirdparty => DocumentMockEarthThirdparty % 65537 != 0)
			.Select(HighlightAdditionalPlutoAccess => (char)(HighlightAdditionalPlutoAccess % 65537 - 1))
			.ToArray());
	}

	public static IEnumerable<int> DumpConstantPapayaFile()
	{
		yield return 1033125268;
		yield return 1580817977;
		yield return 1296780619;
		yield return 2032630055;
		yield return 1567186281;
		yield return 1608933350;
		yield return 1348030602;
		yield return 1540119500;
		yield return 1759209741;
		yield return 1484478638;
		yield return 2146140139;
		yield return 1297370504;
		yield return 1437881780;
		yield return 1137001466;
		yield return 1433949560;
		yield return 2095021333;
		yield return 1262766971;
		yield return 2070248293;
		yield return 1773955572;

		foreach (int SupportExistingTaygeteColumn in GenerateUndefinedAnthePreview())
		{
			yield return SupportExistingTaygeteColumn;
		}
	}

	public static IEnumerable<int> GenerateUndefinedAnthePreview()
	{
		yield return 1146242187;
		yield return 2061531930;
	}

 
実装はここ
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSFile.cs#L530-L770

空のブロックの展開

以下を

	public class AAA
	{ }

以下のように展開します。

	public class AAA
	{
	}

後続の処理のためだけの処理
実装はここ
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSFile.cs#L889-L906

ダミーメンバーの追加

適当な処理とフィールドを混ぜ込んでコード量を無駄に増やします。
実装はここ
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSFile.cs#L908-L994

識別子のリネーム

クラス名・構造体名・メソッド名・フィールド名・プロパティ名・変数名・列挙型名・列挙型メンバー名・ラベルをその機能が分からないような適当な名前に変更します。
変更後の名前はランダムに選んだ英単語を「動詞 + 形容詞 + 固有名詞 + 名詞」の順でつなげたもの
ランダムな文字列でも良かったのですが、ウィルス嫌気を上げないためにも人間が書いたように見えた方が良いと思ったのでこうしました。
構文解析はしないので、ソース上の全トークン毎に

  • 置き換え禁止ワード (C#予約語, 標準クラス・メンバー名) については置き換えない
  • 同じトークンは同じ名前に置き換える

をしています。
問題点として

  • 標準クラス・メンバー名と被ると、置き換えが行われない。
  • 元の名前が同じだと、同じ名前になる。

が挙げられますが、これを解決するには構文解析するしかないので今は現状で良しとします。
実装はここ
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSFile.cs#L1017-L1090

メンバーの並び替え

クラス・構造体のメンバーをランダムに並び替えます。
並んでいるメンバーは関連性が高くなるため、メンバー同士の関連性をぱっと見で分かりにくくするための処置です。
実装はここ
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSFile.cs#L1152-L1250

コンパイル単位の並び替え

プロジェクトファイル内のソースファイルをランダムに並び替えます。
元の名前の辞書順 (或いは実装順か?) に並んでいるため、これを推測できないようにするための処置です。
実装はここ
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSProjectFile.cs#L30-L40

コンパイル単位のリネーム

ソースファイル名を適当な名前に置き換えます。
置き換える名前は「識別子のリネーム」と同じ方法で生成します。
恐らくは必要の無い処理ですが、念のため実装しました。
実装はここ
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSProjectFile.cs#L102-L170

不要な情報の除去とプロジェクトフォルダの整理とソースの整形

難読化とは関係無いので説明は省略
実装はここ
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSFile.cs#L1152-L1250
https://github.com/soleil-taruto/Hatena/blob/main/a20201226/Confuser/Claes20200001/CSSolutions/CSProjectFile.cs#L230-L241


因みに、ここで難読化しているプログラムはこちら
https://www.freem.ne.jp/win/game/24661
_ttp://ornithopter.ccsp.mydns.jp:58946/anemoscope/Elsa2/e20201224_Udongedon
_ttp://ornithopter.myhome.cx:58946/anemoscope/Elsa/d20201224_Udongedon
いくらか更新・デバッグしたので、これのリンクも貼っておきます。自分の納得のためだけに...