C#进阶 - 破解注入外挂必备神器Harmony

发布时间 2023-09-11 17:31:42作者: tossorrow

前言

Harmony是适用于.NET和Mono的通用非破坏性破解程序库。
Harmony使用简单,文档齐全,兼容性强,实为破解注入外挂之首选。
官网:https://harmony.pardeike.net/
GitHub:https://github.com/pardeike/Harmony

1,快速开始

用visual studio新建一个console项目,Nuget安装Lib.Harmony,代码如下:

namespace _01_玩一玩Harmony
{
    class Program
    {
        //Harmony 是适用于 .NET 和 Mono 模块的通用的非破坏性破解程序库
        //Nuget安装Lib.Harmony
        static void Main(string[] args)
        {
            int result = 0;

            //Patch前-----
            SomeGameClass someGameClass = new SomeGameClass();
            result = someGameClass.DoSomething();  //DoSomething 0
            result = someGameClass.DoSomething();  //DoSomething 0

            //Patch后-----
            MyPatcher.DoPatching();
            result = someGameClass.DoSomething();  //DoSomething 10
            Console.WriteLine($"result {result}");  //result 20
            result = someGameClass.DoSomething();  //DoSomething 20
            Console.WriteLine($"result {result}");  //result 40
            result = someGameClass.DoSomething();  //DoSomething 30
            Console.WriteLine($"result {result}");  //result 60
            result = someGameClass.DoSomething();  //不执行
            Console.WriteLine($"result {result}");  //result 0
            Console.ReadKey();
        }
    }

    //原始代码
    public class SomeGameClass
    {
        private bool isRunning;
        private int counter;

        public int DoSomething()
        {
            if (isRunning)
            {
                counter++;
            }
            Console.WriteLine($"DoSomething {counter * 10}");
            return counter * 10;
        }
    }


    public class MyPatcher
    {
        // 调用DoPatching方法Patch才会生效
        public static void DoPatching()
        {
            var harmony = new Harmony("com.example.patch");
            harmony.PatchAll();
        }
    }

    [HarmonyPatch(typeof(SomeGameClass))]  //类
    [HarmonyPatch("DoSomething")]  //方法,防止写错尽量用nameof()
    class Patch01
    {
        //获取类型成员
        static FieldRef<SomeGameClass, bool> isRunningRef = FieldRefAccess<SomeGameClass, bool>("isRunning");

        //Prefix返回一个bool,如果false,则不执行后续Prefix,不执行原始方法
        [HarmonyPrefix]
        static bool Prefix(SomeGameClass __instance, ref int ___counter)
        {
            isRunningRef(__instance) = true;  //给成员变量isRunning赋值
            if (___counter >= 3)
            {
                return false;
            }
            return true;
        }

        //Postfix永远执行
        [HarmonyPostfix]
        static void Postfix(ref int __result)  //__result表示DoSomething方法的返回值
        {
            __result *= 2;
        }
    }
}

1.1 SomeGameClass类

即将被Patch的代码,注意观察Patch前和Patch后执行DoSomething方法的变化。

1.2 Patch01类

指明了具体需要Patch的类,方法和具体行为。[HarmonyPatch(typeof(SomeGameClass))][HarmonyPatch("DoSomething")]表示要修改SomeGameClass类的DoSomething方法,isRunningRef用于访问SomeGameClass类的isRunning变量,[HarmonyPrefix]表示在执行DoSomething方法之前要执行的代码,[HarmonyPostfix]表示执行DoSomething方法之后要执行的代码。

1.3 MyPatcher类

仅仅用于启动Patch,即调用harmony.PatchAll(),使Patch01类生效,或者说使带[HarmonyPatchAttribute]的类都生效。

1.4 跑起来

执行和结果都写在Main方法里了,很直白。

2,破解

此处的破解指的是改变第三方dll的行为,典型场景是你做开发时引用第一个第三方dll,这个库会先给试用,之后就需要激活码或授权文件才能继续使用。判断是否激活的方法一般都存在于第三方dll中,只需要用Harmony修改激活方法就算破解了。

一般流程是先使用第三方dll,观察dll报需要激活时的信息,然后反编译dll找出与激活相关的代码,最后用Harmony改变这些代码行为。(部分情况下dll会做混淆加壳防止破解,混淆的话通过观察也基本能确定对应的代码,费点眼神就是了。加壳的话,要专门学习脱壳技术,我也不太会)

创建一个需要激活才能使用的类库项目。
创建一个调用此类库的winform项目。

2.1 类库项目

namespace ClassLibrary1
{
    public class Class1
    {
        bool 已激活 = false;
        MD5 md5 = MD5.Create();
        public void 激活(string code)
        {
            if (!已激活)
            {
                if (Convert.ToBase64String(md5.ComputeHash(Encoding.UTF8.GetBytes(code))) == "ICy5YqxZB1uWSwcVLSNLcA==")
                {
                    已激活 = true;
                    Console.WriteLine("激活成功!");
                }
                else
                {
                    Console.WriteLine("激活失败!");
                }
            }
        }
        public void DomeSomething()
        {
            if (已激活)
            {
                Console.WriteLine("调用成功!");
            }
            else
            {
                Console.WriteLine("需要激活!");
            }
        }
    }
}

很简单,需要调用激活方法,给对激活码才能成功调用。MD5保证了有密文也反推不了明文。

2.2 winform项目

namespace WindowsFormsApp1
{
    //Nuget安装Lib.Harmony
    public partial class Form1 : Form
    {
        Harmony harmony = new Harmony("com.example.patch");
        Class1 class1 = new Class1();
        public Form1()
        {
            InitializeComponent();
        }

        private void 破解_Click(object sender, EventArgs e)
        {
            harmony.PatchAll();
            class1.激活("任意字符");
        }

        private void 调用_Click(object sender, EventArgs e)
        {
            class1.DomeSomething();
        }
    }

    [HarmonyPatch("ClassLibrary1.Class1", "激活")]  //类,方法
    class Patch01
    {
        //获取类型成员
        static FieldRef<Class1, bool> 已激活Ref = FieldRefAccess<Class1, bool>("已激活");

        //Prefix返回一个bool,如果false,则不执行后续Prefix,不执行原始方法
        [HarmonyPrefix]
        static bool Prefix(Class1 __instance)
        {
            已激活Ref(__instance) = true;  //给成员变量已激活赋值
            return false;  //跳过原始方法
        }
    }
}

winform项目引用ClassLibrary1.dll,直接点击调用按钮会输出需要激活!,点击破解按钮之后,再点击调用按钮会输出调用成功!

3,注入

此处的注入指的是改变正在运行的应用程序的行为,典型场景就是使用C#写的工具或游戏,用着用着就要激活码,要收费。这种相较于破解要稍微麻烦一点,因为目标已经独立运行起来了,需要用到dll注入工具FastWin32。

一般流程是先使用应用程序,观察应用程序报需要激活时的信息,然后反编译应用程序找出与激活相关的代码,然后写一个类库项目用Harmony改变这些代码行为编译成dll,最后用FastWin32将dll注入给应用程序。

创建一个winform项目叫02_目标winform,即需要改变行为的应用程序。
创建一个类库项目02_注入用dll,使用Harmony改变应用程序的行为。
创建一个winform项目叫02_注入工具FastWin32,使用FastWin32将dll注入给应用程序。

3.1 目标winform

namespace _02_目标winform
{
    public partial class 目标Form : Form
    {
        public 目标Form()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            textBox1.Text = MakeMessage();
        }

        private string MakeMessage()
        {
            return "我是Form1";
        }
    }
}

Form很简单就两个控件,一个TextBox,一个Button,MakeMessage方法就是我们要改变的目标。

3.2 注入用dll

namespace _02_注入用dll
{

    public class MyPatcher
    {
        //注意!!FastWin32要求函数签名必须是static int MethodName(string)
        public static int DoPatching(string msg)
        {
            var harmony = new Harmony("com.example.patch");
            harmony.PatchAll();
            return 1;
        }
    }

    [HarmonyPatch("_02_目标winform.目标Form", "MakeMessage")]  //类,方法
    class Patch01
    {
        //Prefix返回一个bool,如果false,则不执行后续Prefix,不执行原始方法
        [HarmonyPrefix]
        static bool Prefix(ref string __result)  //__result表示MakeMessage方法的返回值
        {
            __result = "我是Harmony的Prefix";
            return false;
        }

        //Postfix永远执行
        [HarmonyPostfix]
        static void Postfix(ref string __result)  //__result表示MakeMessage方法的返回值
        {
            __result = $"{__result} 我是Harmony的Postfix";
        }
    }
}

DoPatching方法有参数和返回值,只是为了满足FastWin32对函数的签名要求,没别的意义。其余部分一路看下来的应该很直白明了。

3.3 注入工具FastWin32

namespace _02_注入工具FastWin32
{
    public partial class 注入工具Form : Form
    {
        public 注入工具Form()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //Nuget安装FastWin32
            OpenFileDialog dialog = new OpenFileDialog();
            dialog.InitialDirectory = AppDomain.CurrentDomain.BaseDirectory;
            if (dialog.ShowDialog() == DialogResult.OK)  //手动选择02_HarmonyTest\02_注入用dll\bin\Debug\02_注入用dll.dll
            {
                uint id = Convert.ToUInt32(textBox1.Text);  //在任务管理器看02_目标winform.exe的PID,手动填上
                Injector.InjectManaged(id, dialog.FileName, "_02_注入用dll.MyPatcher", "DoPatching", "", out int returnValue);
                MessageBox.Show($"returnValue:{returnValue}");
            }
        }
    }
}

Form很简单就两个控件,一个TextBox,一个Button,这块逻辑写的略微粗糙,先打开任务管理器,找到02_目标winform.exe的PID,填在TextBox里,然后点击按钮,选择3.2节中生成的02_注入用dll.dllInjector.InjectManaged方法就会在02_目标winform.exe的程序域中执行DoPatching方法,就搞定了。

3.4 跑起来

先运行02_目标winform.exe

然后在任务管理器找到PID。

然后运行02_注入工具FastWin32.exe,填上PID,选择好02_注入用dll.dll

再回到02_目标winform.exe,点击按钮,就会发生变化。

4,絮叨

使用Harmony的同期也尝试过别的方法,比如DotNetDetour,比如打开unsafe直接替换函数指针,但是效果都不好,远远不如Harmony。这才能说Harmony实为破解注入外挂之首选。