ODP.NET 參照版本暴力修改工具(終極版)
2 |
最近又被 ODP.NET 版本問題搞得很煩。
故事是我有顆公用程式元件最近改版(假設名稱叫 MyLibrary.dll 好了),專案參照的 ODP.NET 已升級到 2.122.1.0。移交給同事使用時,同事的 ASP.NET 為配合其他元件仍在使用 ODP.NET 2.112.2.0,這種狀況下,即使加 bindingRedirect 統一導向 2.112.2.0,仍會噴出 CS1705: Assembly 'MyLibrary, Version=3.5.0.0, Culture=neutral, PublicKeyToken=null' uses 'Oracle.DataAccess, Version=2.122.1.0, Culture=neutral, PublicKeyToken=89b483f429c47342' which has a higher version than referenced assembly 'Oracle.DataAccess, Version=2.112.2.0, Culture=neutral, PublicKeyToken=89b483f429c47342'
錯誤。
對我來說,這不是什麼新鮮問題。為了省去為此修改參照版本重新編譯的工夫,我之前就研究過修改參數版號再 ilasm 編譯回 DLL 的奧步妙招(前題是程式未使用新舊版本不相容的 API),還寫了懶人工具簡化操作,最後想出直接修改 .DLL 二進位資料的大絕,簡單、粗暴但絕對有效,呵。
參考資料:
- 組件繫結重新導向功能的實地觀察--以ODP.NET為例
- has a higher version than referenced assembly 案例
- 組件繫結重新導向鬼問題-版號高低之謎
- ODP.NET 9.2版相容問題的暴力解法
- ODP.NET版本暴力解法之懶人工具
- 核武級ODP.NET版本暴力破解工具
這回再遇上老問題,一不做二不休,決定寫成通用工具,DLL 名稱、新舊版號可任意指定,未來管它 2.112.1、2.112.2、2.122.1 或 2.122.2,愛怎麼改就怎麼改。另外,這次再加上顯示現有參照版本的功能,用起來會像這樣:
第一參數是 DLL 路徑,必填。若不輸入版號參數,工具會透過 System.Reflection 列舉該組件參照的其他組件及版號。若要修改版號,則需同時輸入舊版號及新版號,程式會將舊版號轉成 byte[8] 比對 DLL 的二進位資料。若比對吻合且只有一處符合時,將其置換成新版號的 byte[8],並在原 DLL 路徑檔名後方加上 .patched 另存新檔。比對如超過一處符合則有錯換疑慮,即放棄修改。理論上它也可以用來置換 ODP.NET 以外的程式庫參照版號,但實務上舊版號必須夠特殊才可行。ODP.NET 慣用 2.112.1.0 這種細到三節的版號,轉成 byte[] 會是 0x02 0x00 0x70 0x00 0x01 0x00 0x00 0x00 不容易跟 DLL 其他內容重複;像是 Json.NET 的 12.0.0.0 轉成 byte[] 0x0C 0x00 0x00 0x00 0x00 0x00 0x00 0x00,在 DLL 裡很常出現,除非更進一步去解讀 DLL 檔案結構否則無法確認是哪一段才是 Json.NET 版號,就無法適用。現階段這樣子已可解決 ODP.NET 版號問題,不打算多花時間深入,否則有違「省事」的初衷。
程式其實很短,100 行打死,附上完整程式碼提供大家參考:
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
namespace SetOdpNetRefVer
{
class Program
{
static byte[] ConvVerStrToBytes(string verString)
{
var m = Regex.Match(verString, @"(?<n0>\d+)\.(?<n1>\d+)\.(?<n2>\d+)\.(?<n3>\d+)");
if (!m.Success) throw new ApplicationException($"Invalid version format - {verString}");
byte[] buff = new byte[8];
for (var i = 0; i < 4; i++)
BitConverter.GetBytes(short.Parse(m.Groups["n" + i].Value)).CopyTo(buff, i * 2);
return buff;
}
static void Main(string[] args)
{
if (args.Length != 1 && args.Length != 3)
{
Console.WriteLine("Syntax: SetAsmRefDllVer.exe AssemblyName.dll");
Console.WriteLine("Syntax: SetAsmRefDllVer.exe AssemblyName.dll 2.122.1.0 2.112.2.0");
return;
}
var dllPath = args[0];
if (!File.Exists(dllPath))
{
Console.WriteLine("File not found - " + dllPath);
return;
}
if (args.Length == 3)
ChangeRefVersion(dllPath, args[1], args[2]);
else
ShowRefVersions(dllPath);
}
private static void ShowRefVersions(string dllPath)
{
var asm = Assembly.LoadFrom(dllPath);
asm.GetReferencedAssemblies().ToList().ForEach(o =>
{
Console.WriteLine($"{o.Name}, {o.Version}");
});
}
private static void ChangeRefVersion(string dllPath, string oldVerStr, string newVerStr)
{
try
{
var oldVerBytes = ConvVerStrToBytes(oldVerStr);
var newVerBytes = ConvVerStrToBytes(newVerStr);
byte[] buff = File.ReadAllBytes(dllPath);
var idx = 0;
var foundCount = 0;
while (idx < buff.Length - 8)
{
var match = true;
for (var offset = 0; offset < 8; offset++)
{
if (buff[idx + offset] != oldVerBytes[offset])
{
match = false;
break;
}
}
if (match)
{
for (var offset = 0; offset < 8; offset++)
buff[idx + offset] = newVerBytes[offset];
foundCount++;
}
idx++;
}
if (foundCount == 0)
Console.WriteLine("Not found");
else if (foundCount > 1)
Console.WriteLine("More than one matched result");
else if (foundCount == 1)
{
var newFileName = dllPath + ".patched";
File.WriteAllBytes(newFileName, buff);
Console.WriteLine("Done. Saved as " + newFileName);
}
}
catch (Exception ex)
{
Console.WriteLine("*** ERROR ***");
Console.WriteLine(ex.ToString());
}
}
}
}
另外,專案我會放上 Github,有需要的同學請自取。
A hacking tool to modify .NET assembly binary directly to change the version number of referenced ODP.NET.
Comments
# by Hung Yang
之前因為IIS主機問題,將ODP版本升級,指定路徑,結果發生因為與Oracle Instance版本不一致,導致CLOB型別資料存取出現異常,不知道黑大這方式會不會產生相似的問題,謝謝。
# by Jeffrey
to Hung Yang,若問題出在 ODP.NET 跟 Oracle Client 版本不相容,不適用這種偷吃步的方法。