免责声明

使用者本人对于传播和利用本公众号提供的信息所造成的任何直接或间接的后果和损失负全部责任。公众号及作者对于这些后果不承担任何责任。如果造成后果,请自行承担责任。谢谢!

大家好,我是沙漠尽头的狼。

本文首发于Dotnet9,结合前面两篇(如何在没有第三方.NET库源码的情况下调试第三库代码?拦截、篡改、伪造.NET类库中不限于public的类和方法),本文将设计一个案例,手把手地带大家应用这两篇文章中涉及的技能,并介绍一种支持多个版本的库的兼容性解决方案(涉及第三方库的反编译和强签名)。

本文的目录如下:

  1. 前言
  2. 案例设计
  3. 使用dnSpy进行调试
  4. 使用Lib.Harmony拦截
  5. 引入高版本Lib.Harmony:支持多个版本的库的兼容性使用
  6. 总结

1. 前言

技术的存在即合理,关键在于如何使用。在前面的文章中,有读者留言:

Lib.Harmony似乎不是一个正经的库,有什么合法的场景需要使用它吗?

站长回答:非常正经。当你使用一个第三方库,并且确定了版本并已经上线,有时候不能随意升级第三方库,因为可能存在潜在的风险。这时,你只能修改自己的代码,而不动第三方库。

还有读者说得很有道理:

这个工具非常强大,但有时也很可怕。

既然读者有疑问,所以我写了这篇文章,尽量模拟一个看起来比较实际的应用场景。你可以跟着做一做,看看这个工具到底是不是正经的。本文提供了详细的手把手教程。

2. 案例设计

这是一个小动画游戏,我已经将其发布到NuGet上:Dotnet9Games。在这个小动画游戏中,我设置了两个陷阱。我们将按照我的步骤一一解决这些问题。首先,我们创建一个.NET Framework 4.6.1的WPF空项目【Dotnet9Playground】。我认为大部分人都会使用这个版本的桌面应用程序,如果不是,请在评论中告诉我。

2.1. 引入Dotnet9Games包

我已经将制作好的(虚构的)游戏发布在[NuGet](NuGet Gallery | Dotnet9Games 1.0.2)上作为第三方包使用。为了模拟一个比较真实的场景,直接安装最新版本即可:

2.2. 添加目标游戏

打开MainWindow.xaml,引入Dotnet9Games命名空间:

  1. xmlns:dotnet9="https://dotnet9.com"

MainWindow.xaml完整代码如下:

  1. <Window
  2. x:Class="Dotnet9Playground.MainWindow"
  3. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  5. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6. xmlns:dotnet9="https://dotnet9.com"
  7. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  8. Title="综合小案例:模拟.NET应用场景,综合应用反编译、第三方库调试、拦截、一库多版本兼容"
  9. Width="800"
  10. Height="450"
  11. Background="Bisque"
  12. Icon="Resources/favicon.ico"
  13. mc:Ignorable="d">
  14. <Border Padding="10">
  15. <Grid>
  16. <Grid.RowDefinitions>
  17. <RowDefinition Height="40" />
  18. <RowDefinition Height="*" />
  19. </Grid.RowDefinitions>
  20. <StackPanel
  21. Grid.Row="0"
  22. VerticalAlignment="Center"
  23. Orientation="Horizontal">
  24. <TextBlock
  25. VerticalAlignment="Center"
  26. FontSize="20"
  27. Foreground="Blue"
  28. Text="生成" />
  29. <TextBox
  30. x:Name="TextBoxBallCount"
  31. Width="50"
  32. Height="25"
  33. Margin="10,0"
  34. VerticalAlignment="Center"
  35. HorizontalContentAlignment="Center"
  36. VerticalContentAlignment="Center"
  37. FontSize="20"
  38. Foreground="Red"
  39. Text="{Binding ElementName=MyBallGame, Path=BallCount, Mode=TwoWay}" />
  40. <TextBlock
  41. Margin="0,0,10,0"
  42. VerticalAlignment="Center"
  43. FontSize="20"
  44. Foreground="Blue"
  45. Text="个气球,点击" />
  46. <Button
  47. Padding="15,2"
  48. Background="White"
  49. BorderBrush="DarkGreen"
  50. BorderThickness="2"
  51. Click="StartGame_OnClick"
  52. Content="开始游戏"
  53. FontSize="20"
  54. Foreground="DarkOrange" />
  55. </StackPanel>
  56. <dotnet9:BallGame
  57. x:Name="MyBallGame"
  58. Grid.Row="1"
  59. BallCount="8" />
  60. </Grid>
  61. </Border>
  62. </Window>

MainWindow.xaml.cs代码如下:

  1. using System.Windows;
  2. namespace Dotnet9Playground;
  3. /// <summary>
  4. /// 综合小案例:模拟.NET应用场景,综合应用反编译、第三方库调试、拦截、一库多版本兼容
  5. /// </summary>
  6. public partial class MainWindow : Window
  7. {
  8. public MainWindow()
  9. {
  10. InitializeComponent();
  11. }
  12. private void StartGame_OnClick(object sender, RoutedEventArgs e)
  13. {
  14. MyBallGame.StartGame();
  15. }
  16. }

准备操作完成,运行程序:

这个游戏比较简单,主要包含以下几个步骤:

  1. 在主界面提供一个文本输入框,用于填写生成的气球个数。可以通过数据绑定将文本框的值绑定到游戏的BallCount属性。
  2. 提供一个开始游戏按钮,点击按钮后会触发MyBallGame.StartGame()方法,用于生成气球并播放动画。

2.3. 引入第一个陷阱

气球生成8个可能太少了,让我们来生成80个气球吧:

怎么弹出一个红色的大圆,气球都消失了?这就是陷阱!

3. 使用dnSpy进行调试

3.1. 分析

输入80个气球后,我们点击开始游戏是调用了游戏的方法StartGame(), 我们打开[dnSpy](Releases · dnSpyEx/dnSpy (github.com))(这个链接提供32位和64位下载链接),拖入Dotnet9Games.dll,找到该方法代码:

  1. // Token: 0x06000022 RID: 34 RVA: 0x000022AC File Offset: 0x000004AC
  2. public void StartGame()
  3. {
  4. bool flag = this.BallCount > 9;
  5. if (flag)
  6. {
  7. this.PlayBrokenHeartAnimation();
  8. }
  9. else
  10. {
  11. this.GenerateBalloons();
  12. }
  13. }

原来是当气球个数多于9个时调用了PlayBrokenHeartAnimation()方法,这个方法干啥的呢?看代码:

大致看出来了吗?首先是清空气球控件,然后又添加了一个红色的圆动画,我们调试试试呢?

3.2. 调试验证

大致说下步骤:

  1. StartGame()方法第一行打上断点;
  2. 点击dnSpy【启动】按钮;
  3. 在弹出的【调试程序】界面里,"调试引擎"默认选择.NET Framework,"可执行程序"选择我们的WPF主程序Exe【Dotnet9Playground.exe】,再点击【确定】即将WPF程序运行起来了;
  4. 主程序界面气球个数输入超过9个,比如80?
  5. 点击“开始游戏”按钮;
  6. 进入断点了,调试看看,真的进入PlayBrokenHeartAnimation()方法

4. 使用Lib.Harmony拦截

明白了原因,我们使用Lib.Harmony拦截StartGame()方法。

4.1. 安装Lib.Harmony包

我们安装最低版本1.2.0.1

为啥是安装最低版本?

为了后面引入一库多版本兼容需求,低版本的Lib.Harmony有Bug,我们继续,哈哈。

4.2. 编写拦截类

添加拦截类“/Hooks/HookBallGameStartGame.cs”:

  1. using Dotnet9Games.Views;
  2. using Harmony;
  3. using System.Reflection;
  4. namespace Dotnet9Playground.Hooks;
  5. internal class HookBallGameStartGame
  6. {
  7. /// <summary>
  8. /// 拦截游戏的开始方法StartGame
  9. /// </summary>
  10. public static void StartHook()
  11. {
  12. var harmony = HarmonyInstance.Create("https://dotnet9.com/HookBallGameStartGame");
  13. var hookClassType = typeof(BallGame);
  14. var hookMethod =
  15. hookClassType!.GetMethod(nameof(BallGame.StartGame), BindingFlags.Public | BindingFlags.Instance);
  16. var replaceMethod = typeof(HookBallGameStartGame).GetMethod(nameof(HookStartGame));
  17. var replaceHarmonyMethod = new HarmonyMethod(replaceMethod);
  18. harmony.Patch(hookMethod, replaceHarmonyMethod);
  19. }
  20. /// <summary>
  21. /// StartGame替换方法
  22. /// </summary>
  23. /// <param name="__instance">BallGame实例</param>
  24. /// <returns></returns>
  25. public static bool HookStartGame(ref object __instance)
  26. {
  27. #region 原方法原代码
  28. //if (BallCount > 9)
  29. //{
  30. // // 播放爆炸动画效果
  31. // PlayExplosionAnimation();
  32. //}
  33. //else
  34. //{
  35. // // 生成彩色气球
  36. // GenerateBalloons();
  37. //}
  38. #endregion
  39. #region 拦截替换方法逻辑
  40. // 1、删除气球个数限制逻辑
  41. // 2、生成气球方法为private修饰,我们通过反射调用
  42. var instanceType = __instance.GetType();
  43. var hookGenerateBalloonsMethod =
  44. instanceType.GetMethod("GenerateBalloons", BindingFlags.Instance | BindingFlags.NonPublic);
  45. // 生成彩色气球
  46. hookGenerateBalloonsMethod!.Invoke(__instance, null);
  47. #endregion
  48. return false;
  49. }
  50. }

上面的代码加了相关的注释,这里再提一提:

  • StartHook()方法用于关联被拦截方法StartGame与拦截替换方法HookStartGame
  • HookStartGame是拦截替换方法,方法中注释的代码为原方法逻辑代码;
  • 替换代码你可以将气球个数改大一点,或者像站长一样直接不要if (BallCount > 9)判断,改为直接调用气球生成方法GenerateBalloons

App.xaml.cs注册上面的拦截类:

  1. public partial class App : Application
  2. {
  3. protected override void OnStartup(StartupEventArgs e)
  4. {
  5. base.OnStartup(e);
  6. // 拦截气球动画播放方法
  7. HookBallGameStartGame.StartHook();
  8. }
  9. }

现在再运行WPF程序,我们把气球个数改为80个,正常生成了:

4.3. 就这样?No,再来一陷阱

看着气球在动,我们缩放下窗体大小(这里建议Debug下尝试,因为程序会崩溃,导致操作系统会卡那么一小会儿):

程序异常了,再截图看看:

贴上异常代码:

  1. /// <summary>
  2. /// 重写MeasureOverride方法,引出Size参数为负数异常
  3. /// </summary>
  4. /// <param name="constraint"></param>
  5. /// <returns></returns>
  6. protected override Size MeasureOverride(Size constraint)
  7. {
  8. // 计算最后一个元素宽度,不需要关注为什么这样写,只是为了引出Size异常使得
  9. var lastChild = _balloons.LastOrDefault();
  10. if (lastChild != null)
  11. {
  12. var remainWidth = ActualWidth;
  13. foreach (var balloon in _balloons)
  14. {
  15. remainWidth -= balloon.Shape.Width;
  16. }
  17. lastChild.Shape.Measure(new Size(remainWidth, lastChild.Shape.Height));
  18. }
  19. return base.MeasureOverride(constraint);
  20. }

分析

  • 在拖动窗体大小时,游戏用户控件BallGameMeasureOverride方法会触发,对布局进行重新计算;
  • 方法内逻辑:
    1. 如果存在一个运动的气球,那么计算BallGame的实际宽度减去所有子气球的宽度之间的差,得到remainWidth;
    2. 使用remainWidth重新计算最后一个气球的大小;
    3. remainWidth在做减法操作,那么气球个数足够多,以致于游戏控件宽度小于这些气球宽之和时,就会为负数;
    4. 我们再看看Size构造函数代码(如果你用的VS,这里推荐大家安装ReSharper,十分方便的查看引用库方法 ),如下截图:

代码复制过来看:

  1. /// <summary>Implements a structure that is used to describe the <see cref="T:System.Windows.Size" /> of an object. </summary>
  2. [TypeConverter(typeof (SizeConverter))]
  3. [ValueSerializer(typeof (SizeValueSerializer))]
  4. [Serializable]
  5. public struct Size : IFormattable
  6. {
  7. // 这里省略N多代码
  8. /// <summary>Initializes a new instance of the <see cref="T:System.Windows.Size" /> structure and assigns it an initial <paramref name="width" /> and <paramref name="height" />.</summary>
  9. /// <param name="width">The initial width of the instance of <see cref="T:System.Windows.Size" />.</param>
  10. /// <param name="height">The initial height of the instance of <see cref="T:System.Windows.Size" />.</param>
  11. public Size(double width, double height)
  12. {
  13. this._width = width >= 0.0 && height >= 0.0 ? width : throw new ArgumentException(MS.Internal.WindowsBase.SR.Get("Size_WidthAndHeightCannotBeNegative"));
  14. this._height = height;
  15. }
  16. // 这里省略N多代码
  17. }

当宽高为负数时会抛出异常,这就能理解了,我们再使用Lib.Harmony拦截BallGameMeasureOverride方法,如法炮制。

添加/Hooks/HookBallgameMeasureOverride.cs类拦截:

  1. using Dotnet9Games.Views;
  2. using Harmony;
  3. using System.Reflection;
  4. namespace Dotnet9Playground.Hooks;
  5. /// <summary>
  6. /// 拦截BallGame的MeasureOverride方法
  7. /// </summary>
  8. internal class HookBallgameMeasureOverride
  9. {
  10. /// <summary>
  11. /// 拦截游戏的MeasureOverride方法
  12. /// </summary>
  13. public static void StartHook()
  14. {
  15. var harmony = HarmonyInstance.Create("https://dotnet9.com/HookBallgameMeasureOverride");
  16. var hookClassType = typeof(BallGame);
  17. var hookMethod = hookClassType!.GetMethod("MeasureOverride", BindingFlags.NonPublic | BindingFlags.Instance);
  18. var replaceMethod = typeof(HookBallgameMeasureOverride).GetMethod(nameof(HookMeasureOverride));
  19. var replaceHarmonyMethod = new HarmonyMethod(replaceMethod);
  20. harmony.Patch(hookMethod, replaceHarmonyMethod);
  21. }
  22. /// <summary>
  23. /// MeasureOverride替换方法
  24. /// </summary>
  25. /// <param name="__instance">BallGame实例</param>
  26. /// <returns></returns>
  27. public static bool HookMeasureOverride(ref object __instance)
  28. {
  29. // 暂时不做任何处理,返回false表示
  30. return false;
  31. }
  32. }

再在App.xaml.cs添加拦截注册:

  1. using Dotnet9Playground.Hooks;
  2. using System.Windows;
  3. namespace Dotnet9Playground
  4. {
  5. /// <summary>
  6. /// App.xaml 的交互逻辑
  7. /// </summary>
  8. public partial class App : Application
  9. {
  10. protected override void OnStartup(StartupEventArgs e)
  11. {
  12. base.OnStartup(e);
  13. // 拦截气球动画播放方法
  14. HookBallGameStartGame.StartHook();
  15. // 这是第二个拦截方法:拦截气球MeasureOverride方法
  16. HookBallgameMeasureOverride.StartHook();
  17. }
  18. }
  19. }

再运行程序:

拦截方法进入了断点,但无法获取BallGame的实例,提示无法读取内存,拦截方法返回False(不执行原方法)有下面的异常:

这时程序异常退出,我们将拦截方法返回True(继续执行原方法),又有提示:

因为继续执行原方法,取最后一个气球方法又报错var lastChild = _balloons.LastOrDefault();,好无奈呀,心酸。

经过公司专家指点:

因为Size是个结构体指针,0Harmony 1.2.0.1版本把指针当成4位,但“我们的程序”是64位,指针是8位,所有内存错了。

好,那我们使用高版本Lib.Harmony

5. 引入高版本Lib.Harmony:支持多个版本的库的兼容性使用

5.1. 新创建工程引入高版本Lib.Harmony

理由

有可能程序中使用低版本的Lib.Harmony库做了不少拦截操作,贸然全部升级,测试不到位,容易出现程序大崩溃(当前本程序只加了一个HookBallGameStartGame拦截类),而工程Dotnet9Playground直接引入同一个库多版本无法实现(网友如果有建议欢迎留言)。

添加新类库“Dotnet9HookHigh”,并使用NuGet安装2.2.2稳定最新版Lib.Harmony库:

同时也添加Dotnet9GamesNuGet包,将前面添加的HookBallgameMeasureOverride类剪切到该库,Lib.Harmony高版本用法与低版本有所区别,在代码中有注释,注意对比,升级后的HookBallgameMeasureOverride类定义:

  1. using Dotnet9Games.Views;
  2. using HarmonyLib;
  3. using System.Reflection;
  4. namespace Dotnet9HookHigh;
  5. /// <summary>
  6. /// 拦截BallGame的MeasureOverride方法
  7. /// </summary>
  8. public class HookBallgameMeasureOverride
  9. {
  10. /// <summary>
  11. /// 拦截游戏的MeasureOverride方法
  12. /// </summary>
  13. public static void StartHook()
  14. {
  15. //var harmony = HarmonyInstance.Create("https://dotnet9.com/HookBallgameMeasureOverride");
  16. // 上面是低版本Harmony实例获取代码,下面是高版本
  17. var harmony = new Harmony("https://dotnet9.com/HookBallgameMeasureOverride");
  18. var hookClassType = typeof(BallGame);
  19. var hookMethod = hookClassType!.GetMethod("MeasureOverride", BindingFlags.NonPublic | BindingFlags.Instance);
  20. var replaceMethod = typeof(HookBallgameMeasureOverride).GetMethod(nameof(HookMeasureOverride));
  21. var replaceHarmonyMethod = new HarmonyMethod(replaceMethod);
  22. harmony.Patch(hookMethod, replaceHarmonyMethod);
  23. }
  24. /// <summary>
  25. /// MeasureOverride替换方法
  26. /// </summary>
  27. /// <param name="__instance">BallGame实例</param>
  28. /// <returns></returns>
  29. public static bool HookMeasureOverride(ref object __instance)
  30. {
  31. return false;
  32. }
  33. }

区别如下图,Harmony实例获取代码有变化,其它不变:

主工程Dotnet9Playground添加Dotnet9HookHigh工程的引用,App.xaml.cs中添加引用HookBallgameMeasureOverride命名空间:using Dotnet9HookHigh;,代码如下:

  1. using Dotnet9HookHigh;
  2. using Dotnet9Playground.Hooks;
  3. using System.Windows;
  4. namespace Dotnet9Playground
  5. {
  6. /// <summary>
  7. /// App.xaml 的交互逻辑
  8. /// </summary>
  9. public partial class App : Application
  10. {
  11. protected override void OnStartup(StartupEventArgs e)
  12. {
  13. base.OnStartup(e);
  14. // 拦截气球动画播放方法
  15. HookBallGameStartGame.StartHook();
  16. // 这是第二个拦截方法:拦截气球MeasureOverride方法
  17. HookBallgameMeasureOverride.StartHook();
  18. }
  19. }
  20. }

这就完了?运行试试:

这提示是指我的新工程Dotnet9HookHigh未成功应用高版本Lib.Harmony(2.2.2),亦指主工程Dotnet9Playground未成功识别加载高版本Lib.Harmony,怎么办?看我接下来的表演!

5.2. 高低版本的库分目录存放

5.2.1. 分析程序输出目录

程序输出目录只有一个0Harmony.dll,高低2个版本应该是两个库才对,怎么办?

5.2.2. 新创建目录

低版本不变(存在位置依然放输出目录的根目录),为了兼容,我们把高版本改目录存放,比如:Lib/Lib.Harmony/2.2.2/0Harmony.dll,将库按目录结构存放在工程Dotnet9HookHigh中:

  • 并将0Harmony.dll的属性【复制到输出目录】设置为【如果较新则复制】
  • 删除Dotnet9HookHighLib.Harmony库的NuGet引用,改为本地引用(原来的配方,浏览本地路径的方式);

这就完了吗?咋还是报那个错?

5.3. 同库多版本配置

5.3.1. App.config配置多版本

修改Dotnet9PalygroundApp.config文件,添加0Harmony.dll两个版本及读取位置:

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3. <startup>
  4. <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
  5. </startup>
  6. <runtime>
  7. <assemblyBinding>
  8. <dependentAssembly>
  9. <assemblyIdentity name="0Harmony"
  10. publicKeyToken="null"/>
  11. <codeBase version="1.2.0.1" href="0Harmony.dll" />
  12. </dependentAssembly>
  13. <dependentAssembly>
  14. <assemblyIdentity name="0Harmony"
  15. publicKeyToken="null"/>
  16. <codeBase version="2.2.2.0" href="Lib\Lib.Harmony\2.2.2\0Harmony.dll" />
  17. </dependentAssembly>
  18. </assemblyBinding>
  19. </runtime>
  20. </configuration>

再运行,还是报上面的错?啊,我要晕了。。。。

5.3.2. 重点:库的强签名

上面分目录、配置文件版本配置目录也还不够,主工程还是无法区分两个版本的Lib.Harmony库,这里涉及.NET 库强签名,就是上面App.config配置中的publicKeyToken特性,加上这个主程序就认识了,关于强签名网上找到个说明[《.Net程序集强签名详解》](.Net程序集强签名详解_51CTO博客_.net 签名):

  1. 可以将强签名的dll注册到GAC,不同的应用程序可以共享同一dll。

  2. 强签名的库,或者应用程序只能引用强签名的dll,不能引用未强签名的dll,但是未强签名的dll可以引用强签名的dll。

  3. 强签名无法保护源代码,强签名的dll是可以被反编译的。

  4. 强签名的dll可以防止第三方恶意篡改。

这里,对于1.2.0.1版本的0Harmony.dll库我们依然不动,只对2.2.2高版本做强签名处理,签名步骤参考[VS2008版本引入第三方dll无强签名],我们来一起做一遍,这里会借助Everything软件搜索使用到的命令程序,建议提前下载。

注意:暂时不要用最新预览版2.3.0-prerelease.2,站长做这个示例签名用这个版本花了2个晚上没成功,换成2.2.2就可以,下面的图也重新录了,可能该版本有其他依赖的缘故,只是猜测:

  1. 创建一个新的随机密钥对0Harmony.snk

使用Everything查找一个sn.exe程序,随便使用一个,比如:"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\sn.exe",在高版本目录下生成一个密钥对文件0Harmony.snk,命令如下:

  1. "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\sn.exe" -k "F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.snk"

  1. 反编译0Harmony.dll

查找ildasm.exe,比如C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\ildasm.exe,执行以下命令生成0Harmony.dll的il中间文件:

  1. "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\ildasm.exe" "F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.dll" /out="F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.il"

  1. 重新编译,附带强命名参数

查找ilasm.exe,比如C:\Windows\Microsoft.NET\Framework64\v2.0.50727\ilasm.exe,执行以下命令做签名:

  1. "C:\Windows\Microsoft.NET\Framework64\v2.0.50727\ilasm.exe" "F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.il" /dll /resource="F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.res" /key="F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.snk" /optimize

  1. 验证签名信息
  1. "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\sn.exe" -v "F:\github_gitee\TerminalMACS.ManagerForWPF\src\Demo\MultiVersionLibrary\Dotnet9HookHigh\Lib\Lib.Harmony\2.2.2\0Harmony.dll"

也可将生成的dll拖入dnSpy查看:

做为对比,查看NuGet下载的Lib.Harmony是没做签名的:

我们将签名补充进App.Config文件。

注意:因为我们使用的随机密钥对,所以您生成的签名和我的肯定不一样:

再调试,能正常拦截MeasureOverride方法了,传入的实例也能正常显示BallGame(就这?对,我搞了2个晚上。。。。):

5.4. 一切就绪,完善最后一个拦截

代码如下:

  1. using Dotnet9Games.Views;
  2. using HarmonyLib;
  3. using System.Collections;
  4. using System.Data;
  5. using System.Linq;
  6. using System.Reflection;
  7. using System.Windows;
  8. using System.Windows.Controls;
  9. using System.Windows.Shapes;
  10. namespace Dotnet9HookHigh;
  11. /// <summary>
  12. /// 拦截BallGame的MeasureOverride方法
  13. /// </summary>
  14. public class HookBallgameMeasureOverride
  15. {
  16. /// <summary>
  17. /// 拦截游戏的MeasureOverride方法
  18. /// </summary>
  19. public static void StartHook()
  20. {
  21. //var harmony = HarmonyInstance.Create("https://dotnet9.com/HookBallgameMeasureOverride");
  22. // 上面是低版本Harmony实例获取代码,下面是高版本
  23. var harmony = new Harmony("https://dotnet9.com/HookBallgameMeasureOverride");
  24. var hookClassType = typeof(BallGame);
  25. var hookMethod = hookClassType!.GetMethod("MeasureOverride", BindingFlags.NonPublic | BindingFlags.Instance);
  26. var replaceMethod = typeof(HookBallgameMeasureOverride).GetMethod(nameof(HookMeasureOverride));
  27. var replaceHarmonyMethod = new HarmonyMethod(replaceMethod);
  28. harmony.Patch(hookMethod, replaceHarmonyMethod);
  29. }
  30. /// <summary>
  31. /// MeasureOverride替换方法
  32. /// </summary>
  33. /// <param name="__instance">BallGame实例</param>
  34. /// <returns></returns>
  35. public static bool HookMeasureOverride(ref object __instance)
  36. {
  37. #region 原方法代码逻辑
  38. //// 计算最后一个元素宽度,不需要关注为什么这样写,只是为了引出Size异常使得
  39. //var lastChild = _balloons.LastOrDefault();
  40. //if (lastChild != null)
  41. //{
  42. // var remainWidth = ActualWidth;
  43. // foreach (var balloon in _balloons)
  44. // {
  45. // remainWidth -= balloon.Shape.Width;
  46. // }
  47. // lastChild.Shape.Measure(new Size(remainWidth, lastChild.Shape.Height));
  48. //}
  49. //return base.MeasureOverride(constraint);
  50. #endregion
  51. #region 拦截替换代码
  52. var instanceType = __instance.GetType();
  53. var balloonsField = instanceType.GetField("_balloons", BindingFlags.NonPublic | BindingFlags.Instance);
  54. var balloons = (IEnumerable)balloonsField!.GetValue(__instance);
  55. var lastChild = balloons.Cast<object>().LastOrDefault();
  56. if (lastChild == null)
  57. {
  58. return false;
  59. }
  60. var remainWidth = ((UserControl)__instance).ActualWidth;
  61. foreach (object balloon in balloons)
  62. {
  63. remainWidth -= GetBalloonSize(balloon).Width;
  64. }
  65. // 注意:关键代码在这,如果剩余宽度大于0才重新计算最后一个子项大小
  66. // 这段代码可能没什么意义,可按实际开发修改
  67. if (remainWidth > 0)
  68. {
  69. var lashShape = GetBalloonShape(lastChild);
  70. lashShape.Measure(new Size(remainWidth, lashShape.Height));
  71. }
  72. #endregion
  73. return false;
  74. }
  75. private static Ellipse GetBalloonShape(object balloon)
  76. {
  77. var shapeProperty = balloon.GetType().GetProperty("Shape");
  78. var shape = (Ellipse)shapeProperty!.GetValue(balloon);
  79. return shape;
  80. }
  81. private static Size GetBalloonSize(object balloon)
  82. {
  83. var shape = GetBalloonShape(balloon);
  84. return new Size(shape.Width, shape.Height);
  85. }
  86. }

其中关键代码是:

  1. // 注意:关键代码在这,如果剩余宽度大于0才重新计算最后一个子项大小
  2. // 这段代码可能没什么意义,可按实际开发修改
  3. if (remainWidth > 0)
  4. {
  5. var lashShape = GetBalloonShape(lastChild);
  6. lashShape.Measure(new Size(remainWidth, lashShape.Height));
  7. }

其他代码就是反射的使用,不再细说,我们运行程序,现在随便缩放窗体了:

当剩余宽度小于0时跳过计算最后一个子项大小

5.4. 小优化

上面部分截图中可能您也看到了0Harmony.ref文件,我们简单说说。

Git一般是配置成不能上传可执行程序或dll文件的,但多版本dll特殊,部分库不能直接从NuGet引用,所以本文中的高版本Lib.Harmony库只能使用自己强签名版本,我们将dll文件扩展名改为“.ref"以允许上传,他人能正常使用,程序如果需要正常编译、生成,则给Dotnet9HookHigh工程添加生成前命令行,即生成时将.ref复制一份为.dll

  1. copy "$(ProjectDir)Lib\Lib.Harmony\2.2.2\0Harmony.ref" "$(ProjectDir)Lib\Lib.Harmony\2.2.2\0Harmony.dll"

6. 总结

  • 技术交流加群请添加站长微信号:dotnet9com
  • 文中示例代码:MultiVersionLibrary

文中案例写的一般,特别是第二个陷阱,有兴趣可以阅读游戏相关代码,提PR大家一起切磋,把这个案例写的更合理、更有趣、更好玩一点,能让第二个陷阱写一些好玩的特效,拦截后实现不同的效果,这才是拦截的乐趣。

本文通过一个模拟实际案例,帮助大家应用前两篇文章中涉及的技能(dnSpy调试第三方库和Lib.Harmony拦截第三方库),并且介绍一种支持多个版本的库的兼容性解决方案。

通过本文介绍支持多个版本的库的兼容性解决方案,读者可以简单了解如何反编译第三方库,以及如何使用强签名技术来保证库的兼容性(和安全性,本文未展开说,可以阅读此文[浅谈.NET程序集安全签名](浅谈.NET程序集安全签名 - 知乎 (zhihu.com)))。希望本文提供的案例能帮助读者更好地理解和应用这些技能。

谢谢您阅读到这,可以关注【Dotnet9】微信公众号,大家技术交流、保持进步:

模拟.NET应用场景,综合应用反编译、第三方库调试、拦截、一库多版本兼容方案的更多相关文章

  1. dll文件反编译,c#、vb动态库反编译

    最近开发遇到一个项目,对方提供一个c#编写的动态库,图片处理需要调用该动态库方法,发现一张图片处理起来需要5s时间,对方无法提供有效解决手段,抱着试一试的想法反编译的对方的动态库,发现其中问题. 一下 ...

  2. 通过Android反编译技术研究国内陌生人社交即时通讯的技术方案

    版权声明:本文为xing_star原创文章,转载请注明出处! 本文同步自http://javaexception.com/archives/100 即时通讯IM类App分析 这两周对国内陌生人社交领域 ...

  3. 如何保护java程序不被反编译

    Java是一种 跨平台的.解释型语言 Java 源代码编译中间“字节码”存储于class文件中.Class文件是一种字节码形式的中间代码,该字节码中包括了很多源代码的信息,例如变量名.方法名 等.因此 ...

  4. java如何防止反编译(转)

    出处: java如何防止反编译 一些防止java代码被反编译的方法 综述(写在前面的废话) Java从诞生以来,其基因就是开放精神,也正因此,其可以得到广泛爱好者的支持和奉献,最终很快发展壮大,以至于 ...

  5. Java 7 中的Switch 谈 Java版本更新和反编译知识

    Java 7 中的Switch 谈 Java版本更新和反编译知识          学习编程,享受生活,大家好,我是追寻梦的飞飞.今天主要讲述的是Java7中的更新Switch实现内部原理和JAD反编 ...

  6. Android反编译基础(apktoos)--广工图书馆APK

    更多精彩内容 :http://www.chenchuangfeng.com QQ:375061590 ------------------------------------------------- ...

  7. Android apk反编译基础(apktoos)图文教程

    本文主要介绍了Android apk反编译基础,使用的工具是apktoos,我们将用图文的方式说明apktoos工具的使用方式,你可以参考这个方法反编译其它APK试试看了 很久有写过一个广工图书馆主页 ...

  8. Java反编译工具CFR,Procyon简介

    Java反编译工具有很多,个人觉得使用最方便的是jd-gui,当然jad也不错,jd-gui主要提供了图形界面,操作起来很方便,但是jd-gui很久没有更新了,java 7出来很久了,jd-gui在反 ...

  9. Java 反编译工具 —— JAD 的下载地址(Windows版/Linux版/Mac OS 版)

    Java 反编译工具 —— JAD 的下载地址. 各种版本哦! Windows版,Linux版,Mac OS 版,等等 下载地址: http://varaneckas.com/jad/

  10. 怎么防止别人动态在你程序生成代码(怎么防止别人反编译你的app)

    1.本地数据加密 iOS应用防反编译加密技术之一:对NSUserDefaults,sqlite存储文件数据加密,保护帐号和关键信息 2.URL编码加密 iOS应用防反编译加密技术之二:对程序中出现的U ...

随机推荐

  1. 案例实践 | 某能源企业API安全实践

    随着智能电网.全球能源互联网."互联网+电力".新电改的全面实施,分布式能源.新能源.电力交易.智能用电等新型业务不断涌现,运营模式.用户群体都将发生较大变化,电力市场由相对专业向 ...

  2. 文字生成图像 AI免费工具第二弹 DreamStudio

    介绍Stable Diffution,就也要提一下DreamStudio,它是Stable Diffusion的母公司Stability AI开发的一个文字生成图像工具,邮箱注册后可以免费生成125张 ...

  3. 一种实现Spring动态数据源切换的方法

    1 目标 不在现有查询代码逻辑上做任何改动,实现dao维度的数据源切换(即表维度) 2 使用场景 节约bdp的集群资源.接入新的宽表时,通常uat验证后就会停止集群释放资源,在对应的查询服务器uat环 ...

  4. 搭建自动化 Web 页面性能检测系统 —— 实现篇

    我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值.. 本文作者:琉易 liuxianyu.cn 前段时间分享了<搭 ...

  5. pta第三阶段题目集

    (1)前言 pta第三阶段作业中,主要包含了如下的主要内容: 1.全程贯穿了课程设计的程序,每一次都是上一次的迭代和修改,难度较大,中间涉及到先是类与类之间的多态和继承关系,后面的修改中,转变为了组合 ...

  6. SQL Sever 基础语法(增)

    SQL Sever  插入(Insert)基础语法详解 在SQL中,向表中插入数据是最基础的,任何对数据处理的基础就是数据库有数据,对于SQL而言,向表中插入数据有多种方法,本文列举3种: (一) 标 ...

  7. LocalTime转String类型,如下图

  8. 即构低延迟直播产品L3,打造更优质的实时互动体验

    以短视频.直播为代表的音视频互动,正成为互联网主流的交互方式.拿直播举例,它从一种娱乐形式,逐渐融合于教育.娱乐.电商.旅游等多种生态中.未来,直播还将成为像水.电一样的基础设施. 然而,仅仅可进行音 ...

  9. 如何正确使用:has和:nth-last-child

    我们可以用CSS检查,以了解一组元素的数量是否小于或等于一个数字.例如,一个拥有三个或更多子项的grid.你可能会想,为什么需要这样做呢?在某些情况下,一个组件或一个布局可能会根据子元素的数量而改变. ...

  10. vite — 超快且方便的编译工具

    我们编写的代码,比如 ES6. TypeScript.react 等是不能被浏览器直接识别的,需要通过 webpack .rollup 这样的构建工具来对代码进行转换.编译. 但随着项目越来越大,需要 ...