.NETクラスライブラリ内のpublicに限らないクラスとメソッドのインターセプト、改ざん、偽装

.NETクラスライブラリ内のpublicに限らないクラスとメソッドのインターセプト、改ざん、偽装

本記事では、.NETクラスライブラリ内のメソッドをインターセプトする方法を振り返り、メソッドパラメータの改ざんやメソッド戻り値の偽装を実現する方法に加え、.NETクラスライブラリ内の非publicなクラスやメソッドをどのようにインターセプトするかに重点を置いて紹介します。

最終更新 2023/09/23 12:31
沙漠尽头的狼
読了目安 7 分
カテゴリ
.NET
タグ
.NET C#

皆さん、こんにちは。砂漠の果ての狼です。

本記事は初出Dotnet9にて、Lib.Harmonyライブラリを使用してサードパーティの.NETライブラリのメソッドをインターセプトし、そのソースコードを変更せずにメソッドのロジックや期待される動作を変更する方法を紹介します。さらに、publicアクセス修飾子のクラスやメソッドに限定せずインターセプト可能であることも説明します。目次は以下の通りです:

  1. メソッドインターセプトとは?
  2. サンプルプログラムのインターセプト
  3. 非publicメソッドをインターセプトするには?
  4. まとめ

1. メソッドインターセプトとは?

メソッドインターセプトとは、メソッドが呼び出される前または後に、カスタムコードを挿入してメソッドの動作を変更することです。メソッドインターセプトにより、開発者は元のコードを変更することなく、メソッドの入力パラメータの検証、メソッドの戻り値の変更、メソッドの呼び出しログの記録などを行うことができます。

本記事ではLib.Harmonyライブラリを使用してサードパーティライブラリのメソッドをインターセプトします。このライブラリについては、サイト管理人が以前このスキルを習得しよう-.NET APIインターセプト技法という記事を書いていますので、そちらもご覧ください。ただし、その記事では非publicなクラスやメソッドのインターセプトについては触れていませんので、本記事で補足します。

2. サンプルプログラムのインターセプト

2.1. 数字を取得する段落プログラムの作成

.NETクラスライブラリプロジェクト(例えばTestDll)を作成し、ユーティリティクラスTestToolを追加します:

namespace TestDll;

public class TestTool
{
    /// <summary>
    /// 数字を含む美しい段落
    /// </summary>
    private readonly List<string> _sentences = new()
    {
        "一は孤独の象徴、寂しさの代弁者、 詩の始まりにひとり立ち、想像をかき立てる。",
        "二は相対的な存在、対立の伴侶、 影のように寄り添い、互いに依存し合う。",
        "三は完全な数字、三角形の安定、 詩に調和のリズムをもたらす。",
        "四は均衡の象徴、四季の循環、 詩の構造をより強固にする。",
        "五は活気あふれる数字、五色の花々、 詩の中で美しい絵を咲かせる。",
        "六は平凡な数字、六角形の形、 詩に安定感をもたらす。",
        "七は神秘的な数字、七色の虹、 詩の中で不思議な光を放つ。",
        "八は無限の数字、八方の宇宙、 詩の想像力を限りなく広げる。",
        "九は完全な数字、九曲の江河、 詩に流れるような美しさをもたらす。",
        "十は円満の数字、十全十美の象徴、 詩の結末をより完璧にする。"
    };

    /// <summary>
    /// 対応する数字の段落を取得
    /// </summary>
    /// <param name="number"></param>
    /// <returns></returns>
    public string GetNumberSentence(int number)
    {
        var mo = number % _sentences.Count;

        // 一の位が0の場合は最後を取得
        if (mo == 0)
        {
            mo = 10;
        }

        if (mo == 6)
        {
            mo = 1;
        }

        var sentencesIndex = mo - 1;
        return _sentences[sentencesIndex];
    }
}

上記のGetNumberSentenceメソッドのロジック:整数numberパラメータを受け取り、10(_sentencesコレクションの要素数)で割った余りを計算し、10以内の数字に対応する美しい段落を返します。ただし、余りが6の場合は数字1の段落を返すようになっています(これはインターセプトのロジックを検証するために追加したものです)。

以下は作成したAvaloniaUIプログラムのテスト画面です。UIは本記事の主眼ではないので、動画とコードのスクリーンショットのみ掲載します。記事末にソースコードのリンクもあります:

2.2. なぜ一の位が6のとき、常に数字1の段落が表示されるのか?

上記のコードを分析すると、mo == 6のときにmo = 1とするロジックを削除したいと考えます。dnSpyなどの逆コンパイルツールを使ってコードを修正する方法もありますが、Lib.Harmonyこのスキルを習得しよう-.NET APIインターセプト技法 - Dotnet9)を使用してGetNumberSentenceメソッドをインターセプトすることもできます。

  1. Lib.Harmonyパッケージのインストール
<PackageReference Include="Lib.Harmony" Version="2.3.0-prerelease.2" />
  1. インターセプト置換クラスの作成

このスキルを習得しよう-.NET APIインターセプト技法 - Dotnet9を参考に、以下のようなインターセプト置換クラスを追加します:

  • インターセプトクラスに、インターセプトする元のクラス型、元のメソッド名、およびパラメータのデータ型を登録します
  • 元のメソッド内のコードをインターセプト置換メソッドPrefixにコピーします。元のクラスのプロパティやフィールドはリフレクションで取得します(例:_sentencesコレクション)
  • mo == 6のコードをコメントアウトします
using HarmonyLib;
using System.Reflection;
using TestDll;

namespace MultiVersionLibrary;

/// <summary>
/// HarmonyPatch属性でインターセプトするクラスとメソッドを関連付ける
/// </summary>
[HarmonyPatch(typeof(TestTool))]
[HarmonyPatch(nameof(TestTool.GetNumberSentence))]
[HarmonyPatch(new Type[] { typeof(int) })]
internal class HookGetNumberSentence
{
    /// <summary>
    /// GetNumberSentenceのインターセプト置換メソッド
    /// </summary>
    /// <param name="__instance">インターセプトされたTestToolインスタンス</param>
    /// <param name="number">GetNumberSentenceメソッドと同名のパラメータ定義。変更することでメソッドパラメータの改ざんが可能</param>
    /// <param name="__result">GetNumberSentenceメソッドの戻り値。変更することでメソッド値の偽装が可能</param>
    /// <returns></returns>
    public static bool Prefix(ref object __instance, int number, ref string __result)
    {
        try
        {
            //元のメソッドロジックをすべてコピーし、一部修正する

            //1. _sentencesはインターセプト対象クラスTestToolのプライベートフィールドなので、リフレクションで値を取得する
            var sentences =
                __instance.GetType().GetField("_sentences", BindingFlags.NonPublic | BindingFlags.Instance)
                    ?.GetValue(__instance) as List<string>;
            if (sentences?.Any() != true)
            {
                __result = "あら、美しい文はありませんか?";
                return true;
            }

            var mo = number % sentences.Count;

            // 一の位が0の場合は最後を取得
            if (mo == 0)
            {
                mo = 10;
            }

            // 2. 曖昧だと思われるコードをコメントアウト
            //if (mo == 6)
            //{
            //    mo = 1;
            //}

            var sentencesIndex = mo - 1;
            __result = sentences[sentencesIndex];

            return false;
        }
        catch (Exception ex)
        {
            return true;
        }
    }
}

ProgramまたはApp.xamlの初期化メソッドで、インターセプトクラスを自動登録するのを忘れないでください:

var harmony = new Harmony("https://dotnet9.com");
harmony.PatchAll(Assembly.GetExecutingAssembly());

メインプログラムを再実行すると、数字6を入力したときに正常に数字6に対応する段落が表示されます:

これで、サードパーティライブラリのソースコードを変更せずに結果を改ざんすることができました。サイト管理人が.NET 8でインターセプトを行ったところ例外が発生し、.NET 6に変更することで正常に動作しました。例外情報は以下の通りです。おそらくLib.Harmonyがまだ.NET 8をサポートしていないためと思われます:

HarmonyLib.HarmonyException:「メソッド System.String TestDll.TestTool::GetNumberSentence(System.Int32 number) のパッチ適用中に例外が発生しました」

TypeInitializationException: 'MonoMod.Utils.DMDEmitDynamicMethodGenerator' の型初期化子が例外をスローしました。

InvalidOperationException: DynamicMethod に returnType フィールドが見つかりません

3. 非publicメソッドをインターセプトするには?

3.1. 数字段落取得メソッドの修正

TestToolクラスを修正し、さらにGetNumberSentence2メソッドを追加します。メソッド内に数値検証操作 mo = new CalNumber().GetValidNumber(mo); を追加します。メソッド定義は以下の通りです:

/// <summary>
/// 対応する数字の段落を取得
/// </summary>
/// <param name="number"></param>
/// <returns></returns>
public string GetNumberSentence2(int number)
{
    var mo = number % _sentences.Count;

    // 一の位が0の場合は最後を取得
    if (mo == 0)
    {
        mo = 10;
    }

    // 新しく数値検証メソッドを追加
    mo = new CalNumber().GetValidNumber(mo);

    var sentencesIndex = mo - 1;
    return _sentences[sentencesIndex];
}

検証メソッドの定義は以下の通りです:

  • CalNumberクラスとGetValidNumberメソッドはinternalで宣言されており、クラスまたはメソッドが現在のプロジェクト内でのみ使用可能であることを意味します。
internal class CalNumber
{
    internal int GetValidNumber(int number)
    {
        // ここに複雑なアルゴリズムコードを追加可能
        if (number == 6)
        {
            number = 1;
        }

        return number;
    }
}

そしてメインプロジェクトの数字取得段落メソッド呼び出し箇所を次のように変更します:

public string? Number
{
    get { return _number; }
    set
    {
        _number = value;
        TryParse(_number, out var factNumber);

        // メソッド2に変更
        Message = _testTool.GetNumberSentence2(factNumber);
    }
}

数字6を入力すると再び数字1の段落が返されます:

問題:internalメソッドはどのようにインターセプトするのか?

コード mo = new CalNumber().GetValidNumber(mo); を直接コメントアウトするわけにはいきません。検証メソッドが非常に重要な場合、ロジックの一部を変更するだけで、全体の元のロジックは変えるべきではありません。

3.2. internalメソッドのインターセプト方法

新しいインターセプトクラスHookGetValidNumberを追加します。今度はクラスに属性([HarmonyPatch(typeof(CalNumber))])を付けることができません。なぜなら、CalNumberはpublicアクセス修飾子ではなく、プロジェクトを跨いで直接使用できないからです。構文的にサポートされていません:

属性が使えないので、手動でインターセプトしたいメソッドを登録します。これが本記事の要点です。以下にコードを示し、簡単に説明します:

  • 手動登録コードは自動登録の属性宣言と似ていますが、書き方が異なります。
  • インターセプト置換メソッドはHarmonyMethodメソッドでラップする必要があります。
  • harmony.Patch(hookMethod, replaceHarmonyMethod); でインターセプト対象メソッドと置換メソッドを関連付けます。
/// <summary>
/// 手動でインターセプト対象メソッドと置換メソッドを関連付ける
/// </summary>
public static void StartHook()
{
    var harmony = new Harmony("https://dotnet9.com");
    var hookClassType = typeof(TestTool).Assembly.GetType("TestDll.CalNumber");
    var hookMethod = hookClassType!.GetMethod("GetValidNumber", BindingFlags.NonPublic | BindingFlags.Instance,
        new[] { typeof(int) });
    var replaceMethod = typeof(HookGetValidNumber).GetMethod(nameof(Prefix));
    var replaceHarmonyMethod = new HarmonyMethod(replaceMethod);
    harmony.Patch(hookMethod, replaceHarmonyMethod);
}

置換メソッドの定義は以下の通りです:

  • Prefixメソッド名には制限はありませんが、上記の手動登録(var replaceMethod = typeof(HookGetValidNumber).GetMethod(nameof(Prefix));)と一致していれば良いです。
  • 数字が6の場合、偽装結果を8に変更します。
/// <summary>
/// GetNumberSentenceのインターセプト置換メソッド
/// </summary>
/// <param name="__instance">インターセプトされたTestToolインスタンス</param>
/// <param name="number">GetNumberSentenceメソッドと同名のパラメータ定義。変更することでメソッドパラメータの改ざんが可能</param>
/// <param name="__result">GetNumberSentenceメソッドの戻り値。変更することでメソッド値の偽装が可能</param>
/// <returns></returns>
public static bool Prefix(ref object __instance, int number, ref int __result)
{
    //元のメソッドロジックをすべてコピーし、一部修正する

    // ここに複雑なアルゴリズムコードを追加可能
    if (number == 6)
    {
        number = 8;
    }

    __result = number;

    return false;
}

最後に、元の自動登録コードの下に、手動登録コードを1行追加すれば完了です:

// 1. 自動インターセプトクラス登録:インターセプトクラスに、インターセプト対象クラスとメソッドの属性を付与
var harmony = new Harmony("https://dotnet9.com");
harmony.PatchAll(Assembly.GetExecutingAssembly());

// 2. 手動インターセプトクラス登録:インターセプト対象クラスとメソッドの情報を構築してインターセプト
HookGetValidNumber.StartHook();

実行結果は以下の通りです。6を入力すると数字8の段落が表示されます:

4. まとめ

  • 技術交流・グループ参加はサイト管理人のWeChat ID: codewf まで
  • 記事内のサンプルコード:MultiVersionLibrary

Lib.Harmonyライブラリを使用したインターセプト登録には2つの方法があり、その用途は以下の通りです:

  1. 自動登録:
    • インターセプトクラスに属性を使用してインターセプト対象クラスとメソッド定義を関連付けることで、自動的にインターセプトロジックを登録できます。この方法は、インターセプトすべきクラスやメソッドが多い場合に有効で、手動登録の手間を減らし、開発効率を向上させます。
    • 自動登録は通常、publicなクラスまたはメソッドのみ関連付けることができます。IDEがコードの可視性に基づいてフィルタリングやヒントを提供するためです。
  2. 手動登録:
    • コードでインターセプト対象クラスとメソッド定義を構築して手動登録することで、インターセプトロジックをより柔軟に制御できます。この方法は、インターセプトロジックをカスタマイズして処理する必要がある場合に有効で、特定のニーズに応じてインターセプトするクラスやメソッドを選択し、インターセプトロジックを詳細に設定できます。
    • 手動登録はより柔軟で、internalを含むさまざまなクラスやメソッドをインターセプトできます。手動登録ではコードを記述することで非publicなクラスやメソッドを関連付けることができますが、これによりコードの複雑さやメンテナンスコストが増加する可能性があることに注意が必要です。
さらに探索

関連読書

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

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

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

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

AOTの使用経験のまとめ

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

続きを読む