快學會這個技能-.net api攔截技法

快學會這個技能-.net api攔截技法

怎麼在不改變源碼的情況下,篡改一個方法的入參?偽造返回結果?

最后更新 2023/2/13 下午8:21
沙漠尽头的狼
预计阅读 14 分钟
分类
.NET
标签
.NET C# 攔截 harmony hook

大家好,我是沙漠盡頭的狼。

本文先拋出以下問題,請在文中尋找答案,可在評論區回答:

  1. 什麼是api攔截?
  2. 一個方法被很多地方調用,怎麼在不修改這個方法源碼情況下,記錄這個方法調用的前後時間?
  3. 同2,不修改源碼的情況下,怎麼對方法的參數進行校正(篡改)?
  4. 同3,不修改源碼的情況下,怎麼對方法的返回值進行偽造? ...

1. 前言

前言翻译自一个国外的文章,他写的更容易让人理解 - Hacking .NET – rewriting code you don’t control

您是否曾經遇到過不屬於您但想要更改其行為的類庫方法?通常,該方法是非公開的,並且沒有很好的方法來覆蓋其行為。你可以看到它是如何工作的(因為你很棒,並且使用像resharper、dnspy之類反編譯工具,對吧?),你只是無法改變它。你真的需要改變它,因為xxx原因。

有幾個選項可供您使用:

  1. 通過反編譯或下載原始碼(如果首先可用)獲取原始碼。這通常很冒險,因為它經常伴隨著複雜的構建過程,許多依賴項,現在你負責維護庫的整個分支,即使你只想做一個很小的改變。

  2. 使用 ILDasm 反编译应用,直接修补 IL 代码,然后使用 ILAs 将其组装回来。在许多方面,这更好,因为您可以创建一个战略性的手术切口,而不是全面的“从头开始”的方法。缺点是您必须完全在IL中实现您的方法,这是一个不平凡的冒险。

如果您正在處理已簽名的庫,上述兩種方法也不起作用。

现在让我们看一下另一种解决方法-内存修补。这与游戏作弊引擎几十年来使用的技术相同,这些引擎附加到正在运行的进程,查找内存位置并改变其行为。听起来很复杂? 实际上,在 .NET 中做到这一点比听起来容易得多。我们将使用一个名为Harmony的库,该库在NuGet上可通过“Lib.Harmony”包获得。这是一个用于 .NET 的内存修补引擎,主要针对使用 Unity 构建的游戏,当然不止Unity

站長將在本文向您展示如何更改您認為不可能的事情 - 從攔截(hook)自己的庫開始,到攔截(hook) wpf庫和.net基礎庫結束。

2. 攔截(hook)自己的庫

2.1.準備工作

  1. 创建一个控制台程序 HelloHook,添加类 Student
namespace HelloHook;

public class Student
{
    public string GetDetails(string name)
    {
        return $"大家好,我是沙漠尽头的狼网站站长:{name}";
    }
}

Student类中定义了一个GetDetails方法,返回格式化的个人介绍信息,这个方法后面拦截试验使用。

  1. Program.cs中添加Student调用:
using HelloHook;

var student = new Student();
Console.WriteLine(student.GetDetails("沙漠尽头的狼"));

運行程式輸出如下:

大家好,我是沙漠尽头的狼网站站长:沙漠尽头的狼

基本工作準備完成,這就是一個簡單的控制台程式,後文的內容就根據這兩個工程展開細說。

2.2. 拦截GetDetails方法

  1. 引入攔截包-lib.harmony

我们使用Lib.Harmony包,API的拦截就靠它了,在HelloHook工程中添加如下NuGet包:

<PackageReference Include="Lib.Harmony" Version="2.2.2" />
  1. 攔截處理

添加拦截类HookStudent

using HarmonyLib;

namespace HelloHook;

[HarmonyPatch(typeof(Student))]
[HarmonyPatch(nameof(Student.GetDetails))]
public class HookStudent
{
    public static bool Prefix()
    {
        Console.WriteLine($"Prefix");
        return true;
    }

    public static void Postfix()
    {
        Console.WriteLine($"Postfix");
    }

    public static void Finalizer()
    {
        Console.WriteLine($"Finalizer");
    }
}

看代码中的注释,HookStudent类上添加了两个HarmonyPatch特性:

  • 第一个是关联被拦截的类Student类型;
  • 第二个是关联被拦截的类方法GetDetails

即当程序中调用Student类的GetDetails方法时,HookStudent内定义的方法就会分别执行,三个方法执行顺序是Prefix->Postfix->Finalizer,当然约定的方法不止这三个,其实我们常用的应该是PrefixPostfix,约定方法的意义见后方说明,没说就看Harmony wiki...

2.3.註冊攔截

Program.cs进行修改,添加Harmony对整个程序集的拦截:

using HarmonyLib;
using HelloHook;
using System.Reflection;

var student = new Student();
Console.WriteLine(student.GetDetails("沙漠尽头的狼"));  

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

Console.WriteLine(student.GetDetails("沙漠尽头的狼"));

Console.ReadLine();

運行程式輸出如下:

大家好,我是沙漠尽头的狼网站站长:沙漠尽头的狼
Prefix
Postfix
Finalizer
大家好,我是沙漠尽头的狼网站站长:沙漠尽头的狼

上面代码就完成了一个自定义类的拦截处理,使用PatchAll能自动发现补丁类HookStudent,从而自动拦截Student类的GetDetails方法调用,发现第二次调用student.GetDetails("沙漠尽头的狼")时,Harmony的三个生命周期方法都被调用了。

我们可以在拦截类的约定方法(PrefixPostfix等)里做一些日志记录(Console.WriteLine\ILogger.LogInfo等),类似于B/S中的AOP拦截,操作日志在这里记录正合适。

這就完了?說啥呢,這才開始。

2.4.說好的參數篡改呢?還有api結果偽造呢?

修改Program.cs,多打印几行数据方便区分:

using HarmonyLib;
using HelloHook;
using System.Reflection;

var student = new Student();
Console.WriteLine(student.GetDetails("沙漠尽头的狼"));

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

Console.WriteLine(student.GetDetails("沙漠之狐"));
Console.WriteLine(student.GetDetails("Dotnet"));

Console.ReadLine();

在注册Harmony之前,打印一次,注册后打印两次,注意看参数的不同。

修改HookStudent,我们只使用Prefix方法,其他Postfix等方法类似,可看Harmony wiki了解更多的使用方法,修改如下:

using HarmonyLib;

namespace HelloHook;

[HarmonyPatch(typeof(Student))]
[HarmonyPatch(nameof(Student.GetDetails))]
public class HookStudent
{
    public static bool Prefix(ref string name, ref string __result)
    {
        if ("沙漠之狐".Equals(name))
        {
            __result = $"这是我的曾用网名";
            return false;
        }

        if (!"沙漠尽头的狼".Equals(name))
        {
            name = "非站长名";
        }

        return true;
    }
}

先運行看輸出:

大家好,我是沙漠尽头的狼网站站长:沙漠尽头的狼
这是我的曾用网名
大家好,我是沙漠尽头的狼网站站长:非站长名
  • 第1行“大家好,我是沙漠盡頭的狼網站站長:沙漠盡頭的狼”,這是沒有註冊攔截之前的正常格式化輸出;
  • 第2行“這是我的曾用網名”,這裡實現的是結果的偽造;
  • 第3行“大家好,我是沙漠盡頭的狼網站站長:非站長名”,這裡實現的是參數的篡改。

結果偽造

注意看Prefix方法传入的参数ref string __result:其中ref表示引用传递,允许对结果进行修改;string与原方法的返回值类型必须一致;__result为返回值的约定命名,前面是两个"_",即命名必须为__result

if ("沙漠之狐".Equals(name))
{
    __result = $"这是我的曾用网名";
    return false;
}

注意返回值是false,表示不调用原生方法,这里就将被拦截的方法返回值伪造成功了。

參數篡改

看传入的参数ref string nameref表示参数是引用传递,允许对参数进行修改;string name必须与原方法参数定义一样。

if (!"沙漠尽头的狼".Equals(name))
{
    name = "非站长名";
}

Prefix方法默认返回的true,表示需要调用原生方法,这里会将篡改的参数传入原生方法,原生方法执行结果会将篡改的参数组合返回为“大家好,我是沙漠尽头的狼网站站长:非站长名”。

注意:

原生参数name和返回值__result是可选的,如果不进行篡改,去掉ref也是可以的。

上面的示例源码点这

3. 攔截(hook)wpf的api

我们创建一个简单的WPF程序HookWpf,拦截MessageBox.Show方法:

public static MessageBoxResult Show(string messageBoxText, string caption)

首先在App中使用自动拦截注册:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {

        base.OnStartup(e);

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

定义拦截类HookMessageBox

using HarmonyLib;
using System.Windows;

namespace HookWpf;

[HarmonyPatch(typeof(MessageBox))]
[HarmonyPatch(nameof(MessageBox.Show))]
[HarmonyPatch(new [] { typeof(string), typeof(string) })]
public class HookMessageBox
{
    public static bool Prefix(ref string messageBoxText, string caption)
    {
        if (messageBoxText.Contains("垃圾"))
        {
            messageBoxText = "这是一个不错的网站哟";
        }

        return true;
    }
}

HookMessageBox上关联拦截的MessageBox.Show重载方法,在Prefix里对提示框内容进行合法性验证,不合法进行修正。

最后就是在窗体MainWindow.xaml里添加两个弹出提示框的按钮:

<Window x:Class="HookWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <Button Content="弹出默认提示框" Width="120" Height="30" Click="ShowDialog_OnClick"></Button>
        <Button Content="这是一个垃圾网站" Width="120" Height="30" Click="ShowBadMessageDialog_OnClick"></Button>
    </StackPanel>
</Window>

後台處理按鈕點擊事件,彈出提示框:

using System.Windows;

namespace HookWpf;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void ShowDialog_OnClick(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("https://dotnet9.com 是一个热衷于技术分享的程序员网站", "Dotnet9");
    }

    private void ShowBadMessageDialog_OnClick(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("这是一个垃圾网站", "https://dotnet9.com");
    }
}

運行結果如下:

上面效果即完成了提示框內容的驗證,如果內容含有“垃圾”關鍵字,就換成好聽的話(這是一個不錯的網站喲)。

本示例源码在这

4. 攔截(hook).net默認的api

创建控制台程序HookDotnetAPI,引入Lib.Harmonynuget包,Program.cs修改如下:

using HarmonyLib;

var dotnet9Domain = "https://dotnet9.com";
Console.WriteLine($"9的位置:{dotnet9Domain.IndexOf('9',0)}");

var harmony = new Harmony("com.dotnet9");
harmony.PatchAll();

Console.WriteLine($"9的位置:{dotnet9Domain.IndexOf('9', 0)}");

[HarmonyPatch(typeof(String))]
[HarmonyPatch(nameof(string.IndexOf))]
[HarmonyPatch(new Type[] { typeof(char), typeof(int) })]
public static class HookClass
{
    public static bool Prefix(ref int __result)
    {
        __result = 100;
        return false;
    }
}

使用方法和前面类似,string.IndexOf方法被拦截后,总是返回100,不论查找的字符位置在哪,当然这个测试代码没有任何意义,这里只是演示而已,运行结果如下:

9的位置:14
9的位置:100

5. 總結及分享

5.1.總結

harmony的原理是利用反射獲取對應類中的方法,然後加上特性標籤進行邏輯控制,達到不破壞原有代碼進行更新的效果。

harmony用於在運行時修補替換和裝飾 .net/.net core 方法的庫。但是該技術可以與任何.net版本一起使用。它對同一方法的多次更改是累積而不是覆蓋。

再次分析可能產生的場景需要攔截,加深您對本文的記憶:

  1. net的一些方法,我們直接在代碼層面可能無法直接修改;
  2. 第三庫未提供源碼,但我們想改它的部分方法;
  3. 第三庫提供了源碼,雖然可以修改它的源碼,但萬一第三庫後面疊代升級,我們又不得不更新時,那自己做的修改跟著升級可能麻煩了;

攔截注意:如您所見,這提供了大量新的可能性。請記住,權力越大,責任越大。由於您以原始開發人員不打算的方式覆蓋行為,因此無法保證您的補丁代碼在他們發布新版本的代碼時會起作用。即上面第3點,不排除第三庫升級api結構也變了,我們也要跟著修改攔截邏輯哦

从《Harmony wiki patching》中翻译出以下使用注意事项:

  1. 最新2.0版支持 .net core ;
  2. Harmony支持手动(Patch,参考Harmony wiki上的使用)和自动(PatchAll,本文演示使用的这种方式,Lib.Harmony使用的是C#的特性机制);
  3. 它為每個原始方法創建dynamicmethod方法,並向其織入代碼,該代碼在開始(prefix)和結束時(postfix)調用自定義方法。它還允許您編寫過濾器(transpiler)來處理原始的il代碼,從而可以對原始方法進行更詳細的操作;
  4. getter/setter、虛/非虛 方法、 靜態 方法;
  5. 補丁方法必須是靜態方法;
  6. prefix需要返回void或者bool類型(void即不攔截);
  7. postfix需要返回void類型,或者返回的類型要與第一個參數一致(直通模式);
  8. 如果原方法不是靜態方法,則可以使用名為__instance(兩個下劃線)的參數來訪問對象實例;
  9. 可以使用名為__result(兩個下劃線)的參數來訪問方法的返回值,如果是prefix,則得到返回值的默認值;
  10. 可以使用名為__state(兩個下劃線)的參數在prefix補丁中存儲任意類型的值,然後在postfix中使用它,你有責任在prefix中初始化它的值;
  11. 可以使用與原方法中同名的參數來訪問對應的參數,如果你要寫入非引用類型,記得使用ref關鍵字;
  12. 補丁使用的參數必須嚴格對應類型(或者使用object類型)和名字;
  13. 我們的補丁只需要定義我們需要用到的參數,不用把所有參數都寫上;
  14. 要允許補丁重用,可以使用名為__originalmethod(兩個下劃線)的參數注入原始方法。

最后忘了补一条,.NET 7中使用Harmony还有点点问题,站长在测试WPF API和.NET基础库拦截Demo时一直不生效,折腾了2、3个晚上,以为是自己的使用问题,最后看到Harmony issue .NET 7 Runtime Skipping Patches #504,将程序降级为.NET 6即可。

5.2分享

读者朋友们,相信不少人使用过Harmony或者其他的 .NET Hook库,可在评论中留言分享,可提出自己的疑问,或自己的使用心得:

  1. 我使用過這個庫進行api的hook,它是xxx;
  2. 我自己實現過類似功能,分享文章連結是xxx;
  3. 想問,我能攔截這個api嗎?場景是xxxx

[我的分享] + [你的分享] ∈ [.net圈子的一份小力量]

6. 參考

写作本文时以下文章都做了参考,建议大家都看看,特别是 Harmony wiki中写了Harmony的详细使用方法:

7. 補充

  • 2023年9月23號 手動註冊攔截

非public类及方法如何拦截拦截|篡改|伪造.NET类库中不限于public的类和方法

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2026/2/7

aot使用經驗總結

從項目創建伊始,就應養成良好的習慣,即只要添加了新功能或使用了較新的語法,就及時進行 aot 發布測試。

继续阅读