今晚,我想來點奇技淫巧 - 竄改 .NET 系統方法
2 | 3,678 |
【警告】本文要介紹的技巧很有趣,在某些情境可以巧妙解決刁鑽問題,令人拍案叫絕。
但對開發來說這類手法如同雙面刃,帶有嚴重副作用,常導致程式邏輯不易理解且難以維護。想像一下,接手程式的人追了三天三夜,百思不得其解,最後才發現有人他 X 的偷改某個 System.* 方法的傳回值,下一秒就算沒有抄起棒球棍,也會拿出稻草人跟大頭針。
遇問題時應優先考量其他正規解法(改用自訂 API、繼承改造、Fork 改寫...),此等非常手段宜謹慎使用。
在討論將 Playwright 程式部署問題時,由於每支程式需自帶一份 68MB .playwright 資料夾,內容重複頗浪費空間。但 Playwright 程式庫不提供參數自訂 .playwright 路徑,無法多支程式共用一份。若要解決,要嘛 Fork Microsoft.Playwright 專案寫個可傳參數版本,要嘛用點駭客手法,強迫 Playwright 程式庫共用路徑。
前者沒什麼技術含量也不有趣,於是我情不自禁研究起怎麼用駭客技巧解決,結果發現好東西。先來看一段展示:
如上圖,施展某段邪惡魔法後,原本該輸出亂數的 Random.Next(),居然傳回 1, 2, 3, 4, 5。
我們都知道 .NET 程式會編譯成通用中間語言 CIL (過去稱為 MSIL),再由 JIT 編譯器轉成機器碼交給 CPU 執行。
在 System.Reflection.Emit 命名空間有一套 API 可動態生成 CIL 程式碼, 透過一些特殊技巧將 Random.Next() 導向我們寫的 CIL 函式,我們就能接管 Random.Next() 換上自訂邏輯。概念不難,但實作細節很多,而且還要懂 IL 語言才能成功。
於是網路上有高手將這些操作包成簡單易用的程式庫,讓一般開發者不懂 IL 也能輕鬆改掉 .NET 系統 API 或外部程式庫方法。其中有個 Harmony 開源程式庫,得到近 3.8K 顆星星,還有蠻完整的文件網站, 有 System.Reflection 基礎的開發者應能很快上手。
Harmony 可以在執行階段對 .NET 方式進行修補(Patch)、置換或加料(Decorate),運作概念如下:
- Prefix
在原始方法執行前執行,可以修改參數、設定結果、跳過原始方法、設定自訂狀態變數給 Postfix 使用 - Postfix
在原始方法執行後執行 - Transpiler
接收原始方法的 IL Code,加工產生新的 IL Code。要處理 IL Code 難度較高但彈性最大,通常是 Prefix/Postfix 無法處理才上場。 - Finalizer
處理 Exception - Reverse Path
在你的方法中呼叫原始方法
在一般情況下用 Prefix 及 Postfix 就能滿足需求,回到一開始偷改 Random.Next() 範例,來看看 HackRandomNextApi() 是怎麼做的:
internal class Demo
{
public static void HackRandomNextApi()
{
// 建立 Harmony 物件,傳入反向順序網域名稱作為識別,供後續管理之用
var harmony = new HarmonyLib.Harmony("net.darkthread.blog.harmony.example");
// 對 Random.Next() 方法進行修補,加上 Prefix 方法
harmony.Patch(
typeof(Random).GetMethod(nameof(Random.Next), BindingFlags.Instance | BindingFlags.Public, new Type[] {}),
prefix: new HarmonyLib.HarmonyMethod(typeof(Demo).GetMethod(nameof(MyRandomNext),
BindingFlags.Static | BindingFlags.NonPublic))
);
// 可使用 AccessTools Helper 簡化寫法
// https://harmony.pardeike.net/articles/utilities.html
/*
harmony.Patch(
AccessTools.Method(typeof(Random), nameof(Random.Next), new Type[] { }),
prefix: new HarmonyLib.HarmonyMethod(AccessTools.Method(typeof(Demo), nameof(MyRandomNext)))
);
*/
// 或使用 [HarmonyPatch(typeof(Math))] [HarmonyPatch(nameof(Math.Max))]
// https://harmony.pardeike.net/articles/annotations.html
}
static int i = 1;
static bool MyRandomNext(ref int __result)
{
// __result 為原始方法的回傳值
__result = i++;
// 傳回 false 不執行原始方法
return false;
}
}
呼叫 Patch() 指定要修改的方法,原始方法、Prefix、Postfix 都需要傳入 MethodInfo 當參數,AccessTools Helper 可簡化 Reflection 取得 MethodInfo 的寫法。除了透過 Reflection 取 MethodInfo,Harmony 也支援 或使用 [HarmonyPatch(typeof(Random))] [HarmonyPatch(nameof(Random.Next))] 或 [HarmonyPatch(typeof(Math), nameof(Math.Max))] 等標註方式自動對映。
參考
Prefix 部分可透過 ref int __result 參數修改回傳值,傳回 false 則可跳過不執行原始方法。
夠簡單吧!
下面是另一種應用方式,我們讓 Prefix、Postfix 搭配運作。像是在 Prefix 建立 Stopwatch 開始計時,並將其當成參數傳給 Postfix 計算執行時間;另外,Prefix 也可以偷改傳入參數,以下是一個範例,同時加入 Prefix 及 Postfix,二者用 Stopwatch 計算 File.WriteAllText() 執行時間,Prefix 還會將原本要寫入 "d:\" 的檔案重導到 "d:\redirect" 目錄下。
static void Main(string[] args)
{
Action<string> printTitle = (msg) => {
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(msg);
Console.ResetColor();
};
var harmony = new Harmony("net.darkthread.blog.harmony.example");
printTitle("Prefix & Postfix Example");
harmony.Patch(
original: AccessTools.Method(typeof(File), nameof(File.WriteAllText), new Type[]
{
typeof(string), typeof(string)
}),
prefix: new HarmonyMethod(AccessTools.Method(typeof(Program), nameof(BeforeWriteFile))),
postfix: new HarmonyMethod(AccessTools.Method(typeof(Program), nameof(AfterWriteFile)))
);
File.WriteAllText("d:\\short.txt", "Hello World");
File.WriteAllText("d:\\long.txt", new string('A', 1024*1024*16));
}
private static void BeforeWriteFile(ref string path, string contents, out Stopwatch __state)
{
// 宣告自訂狀態變數給 Postfix 使用
__state = Stopwatch.StartNew();
// 示範竄改參數,要修改的參數宣告加上 ref
var dir = Path.GetDirectoryName(path);
if (dir == "d:\\")
{
Directory.CreateDirectory("d:\\redirect");
path = Path.Combine("d:\\redirect", Path.GetFileName(path));
}
}
static void AfterWriteFile(string path, string contents, Stopwatch __state)
{
__state.Stop();
Console.WriteLine($"File.WriteAllBytes({path}, {contents.Length:n0} chars) took {__state.ElapsedTicks:n0} ticks");
}
原本要寫入 d:\ 的檔案被放到 d:\redirect\ 下,並會計算寫入耗費的時間:
是不是很像黑魔法?它的確像黑魔法,真要動用記得明顯標示妥善提醒,否則就是將接手者推入火坑,小心造業損陰德。
不過,修改底層屬較進階的技巧,精巧但也偏脆弱,常會因一些條件失敗(例如:目前不支援 .NET 7,且不適用 Interop 非 IL Body 方法、內容過短 Inlined 化的方法、泛型相關方法... 等),錯誤訊息又不直覺,要偵錯頗有難度。故 Harmony 雖已降低使用門檻,但不能算是傻瓜型工具,能不能用它玩出新花樣也看緣份吧。
Introduction the library to patching and replacing .NET method in runtime - Harmony library.
Comments
# by Victor Tseng
黑暗大終於要一頭栽進資安之坑了嗎?
# by Jimmy
這是某種惡搞前公司的新手法嗎.jpg