Harmony dotnet中的动态patch

警告
本文最后更新于 2023-05-11,文中内容可能已过时。

先说结论,没解决

# 前言

想做一个类似于java agent的效果出来,hook某些函数来留后门。搜了一下有很多解决方案,但是都不友好。以下是其中一种Harmony框架的动态patch方案,这种方案更倾向于热更新。

# 项目地址

GitHub https://github.com/pardeike/Harmony

文档地址 https://harmony.pardeike.net/articles/intro.html

# 基本使用

根据文档来看

Harmony有两种patch模式

  1. 注解自动patch
  2. 写代码手动patch

自动模式,根据自身程序集的注解来patch

1
2
3
4
5
var assembly = Assembly.GetExecutingAssembly();
harmony.PatchAll(assembly);

// or implying current assembly:
harmony.PatchAll();

注解HarmonyPatch用法如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class OriginalCode
{
    public void Test(int counter, string name)
    {
        // ...
    }
}

[HarmonyPatch(typeof(OriginalCode), nameof(OriginalCode.Test))]
class Patch
{
    static void Prefix(int counter, ref string name)
    {
        FileLog.Log("counter = " + counter); // read
        name = "test"; // write with ref keyword
    }
}

手动模式 一个一个函数找对应关系patch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// add null checks to the following lines, they are omitted for clarity
// when possible, don't use string and instead use nameof(...)
var original = typeof(TheClass).GetMethod("TheMethod");
var prefix = typeof(MyPatchClass1).GetMethod("SomeMethod");
var postfix = typeof(MyPatchClass2).GetMethod("SomeMethod");

harmony.Patch(original, new HarmonyMethod(prefix), new HarmonyMethod(postfix));

// You can use named arguments to specify certain patch types only:
harmony.Patch(original, postfix: new HarmonyMethod(postfix));
harmony.Patch(original, prefix: new HarmonyMethod(prefix), transpiler: new HarmonyMethod(transpiler));

Harmony通过挂钩的形式改变程序执行流,在方法前后可以插入自定义函数,并且可以在hook事件之间传递实例对象及参数。

image.png

在OriginalMethod执行时在方法体前插入jmp跳转到patch函数,以此实现hook。

有几大特殊事件函数

  1. Prefix 函数执行前
  2. Postfix 函数执行后
  3. Transpiler 转译器
  4. Finalizer 异常终结器

一个例子就看懂了,运行代码观察patch前后

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
using HarmonyLib;
using System;
using WebApplication1;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;

namespace ClassLibrary1
{
    public class Annotations
    {
        private readonly bool _isRunning;

        public IEnumerable<int> GetNumbers()
        {
            Console.WriteLine(nameof(GetNumbers));

            yield return 1;
            yield return 2;
            yield return 3;
        }
    }

    [HarmonyPatch(typeof(Annotations))]
    [HarmonyPatch(nameof(Annotations.GetNumbers))]
    public class AnnotationsPatcher
    {
        static AccessTools.FieldRef<Annotations, bool> isRunningRef =
            AccessTools.FieldRefAccess<Annotations, bool>("_isRunning");

        public static void Patch()
        {
            var harmony = new Harmony(nameof(AnnotationsPatcher));

            Console.WriteLine(new Annotations().GetNumbers().Join());
            harmony.PatchAll();
            Console.WriteLine(new Annotations().GetNumbers().Join());
        }

        static bool Prefix(Annotations __instance)
        {
            Console.WriteLine("Prefix");

            return true;
        }

        /// <summary>Not working</summary>
        static IEnumerable<int> Postfix(IEnumerable<int> values)
        {
            yield return 0;
            foreach (var value in values)
                if (value > 1)
                    yield return value * 10;
            yield return 99;
            Console.WriteLine(nameof(Postfix));
        }

        // looks for STDFLD someField and inserts CALL MyExtraMethod before it
        public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
        {
            Console.WriteLine(nameof(Transpiler));
            //var found = false;
            foreach (var instruction in instructions)
            {
                //if (instruction.opcode == OpCodes.Stfld && instruction.operand == f_someField)
                //{
                //    yield return new CodeInstruction(OpCodes.Call, m_MyExtraMethod);
                //    found = true;
                //}
                yield return instruction;
            }
            //if (found == false)
            //    ReportError("Cannot find <Stdfld someField> in OriginalType.OriginalMethod");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            AnnotationsPatcher.Patch();
            Console.ReadKey();
        }
    }
}

输出

1
2
3
4
5
6
7
GetNumbers
1, 2, 3
Transpiler
Prefix
GetNumbers
Postfix
0, 20, 30, 99

改变了GetNumbers的执行顺序和返回值

# 用途

本来是想用来做类似于java agent那种内存马,但是发现通过dll注入将patch.dll注入到target.exe进程中不起作用,猜测是appdomain不同的问题导致patch不生效,待解决吧。

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。