利用Mono-cecil实现.NET程序的重新签名,重新链接相关库的引用
Mono是一个.NET框架类库集(准确来说是一个"非官方的.NET框架"),Mono已经发展得比较成熟了,在LINUX下也可以使用该框架直接运行.NET程序而无需重新编译.关于Mono的其它信息,可以在互联网随便搜索到.
Mono的开源社区:http://groups.google.com/group/mono-cecil
Mono的出现,给分析/修改.NET程序带来很多方便之处,利用其配合.NET的对象映射,可以做很多有趣的东西,比如动态指令注入.....
.NET程序的强签名验证机制,很多人都认为这个是一种加密技术,防止被恶意修改文件.凡事也有善恶之分,很多时候我们可能需要修改一下别人的程序来满足自己的要求,比如出于善意的软件汉化,或者为了方便,需要将未签名的程序注册到全局命名空间(未签名其实也算是一种"签名"的手段,未签名的程序是无法注册到全局命名空间的).
但一旦修改之后,可能会导致整个程序无法使用,.NET的程序加载,会检查程序的完整性,进行HASH比较,被修改过的程序是无法完成加载的.如果是单个独立程序的话还好办,如果是带有其它库的话,那要修改后正常使用,有以下几个办法:
1.将程序在注册表添加"跳过检测程序强签名处理",这个也是最常用的手段了.
2.另外一种做法,就是通过ILDASM,将程序重新反编译为IL汇编文件,然后加入签名,重新编译为新的程序.这个做法是最笨的,也是最多人使用的.如果一个程序关联的库有10个以上的话,那是一个很吃力的工作,某些程序使用过混淆处理,将所有的标识符都以不可打印字符重新命名,在ILASM下面是无法直接编译通过的.
3.在原程序"内容"不变的情况下,实现所有相关的类库重新签名,并且自动RELINK的话,才是最好的解决办法.
针对第二个方法处理,很多人选择了直接移除强签名的方法来替代重新签名.就是将所有相关的库一律移除强签名,嗯,这样运行是可以通过的,但如果这是一个可以重用的第三方类库的话,被移除强签名的类库,是无法在VS里面直接进行打包发布的,如果你的程序打算使用ONECLICK方式发布的话,那就没撤了.
微软为程序集签名提供了一个SN工具,这个工具可以将签名的程序经行重新签名,当然,需要你有公私钥匙对的情况下,钥匙对就肯定没的了.直接让其签名就肯定无望了.被移除公钥的程序也是直接被拒绝签名的.
重新签名也有现成的工具.比如RE-SIGN这个软件:
可以实现单个文件重新签名.但是单纯改了签名,不将相关的库重新连接的话,重新签名就完全没有意义了.
本来打算修改一下RE-SIGN,让其可以在签名的过程中,重新链接所有相关的库,但RE-SIGN这个软件的作者很小气,将RE-SIGN经行了高强度的混淆处理,
面对这样的程序,我不喜欢强人所难,人家没诚意,我不想去搞他.既然免费提供给别人使用,为何.....
到这里的话,Mono可以派上用场了,其实也是牛刀杀鸡了.
用Mono加载一个程序集,拿你自己的钥匙对生成PublicKey替换掉原来程序的PublicKey(当然,如果之前没有的话,也可以直接这样加上去),然后将替换掉的程序集重新保存,再使用微软提供SN工具就可以直接重新签名,PublicKey和PublicKeyToken可是两个不同的东西,这里要理解清楚.代码如下:
public byte[] GetNewKey(string keyFileName)
{
using (FileStream keyPairStream = File.OpenRead(keyFileName))
{
return new StrongNameKeyPair(keyPairStream).PublicKey;
}
}
public void ReSign()
{
AssemblyDefinition asm = AssemblyFactory.GetAssembly(ASSEMBLYNAME);
asm.Name.PublicKey = GetNewKey(KEYFILENAME);
AssemblyFactory.SaveAssembly(asm, ASSEMBLYNAME);
//用KEY文件建立密钥容器
byte[] pbKeyBlob = File.ReadAllBytes(KEYFILENAME);
string wszKeyContainer = Guid.NewGuid().ToString();
StrongNameKeyInstall(wszKeyContainer, pbKeyBlob, pbKeyBlob.Length);
//使用新建的密钥容器对程序集经行签名
StrongNameSignatureGeneration(ASSEMBLYNAME, wszKeyContainer, IntPtr.Zero, 0, 0, 0);
//删除新建的密钥容器
StrongNameKeyDelete(wszKeyContainer);
}
经过重新保存的程序集,现在可以直接使用微软提供的SN工具经行重新签名了:
sn /Ra RelinkLib.dll mykey.snk
签名可以使用直接启动进程的方式调用SN工具:
public void RunSnTool()
{
//SN工具进程
Process snProcess = new Process
{
StartInfo =
{
FileName = "SN.exe",
Arguments = string.Format("/Ra {0} {1}", ASSEMBLYNAME, KEYFILENAME)
}
};
snProcess.Start();
snProcess.WaitForExit();
}
这样,RE-SIGN这个工具所做的工作,也就基本实现了.
你是否觉得这样不好看,有没有办法内部调用SN工具的类似操作啊?答案是有的,
StrongName库作为一项资源包含在 MsCorEE.dll 中,其一系列API包含有:
GetHashFromAssemblyFile 函数
使用指定的哈希算法获取指定程序集文件的哈希值。
GetHashFromAssemblyFileW 函数
使用指定的哈希算法获取指定为 Unicode 字符串的程序集文件的哈希值。
GetHashFromBlob 函数
使用指定的哈希算法获取指定内存地址处的程序集的哈希值。
GetHashFromFile 函数
依据指定文件的内容生成哈希代码。
GetHashFromFileW 函数
根据一个 Unicode 字符串指定的文件的内容生成哈希代码。
GetHashFromHandle 函数
使用指定的哈希算法并根据具有指定文件句柄的文件的内容生成哈希代码。
StrongNameCompareAssemblies 函数
只通过强名称签名确定两个程序集是否不同。
StrongNameErrorInfo 函数
获取由某个强名称函数引发的上一个错误代码。
StrongNameFreeBuffer 函数
释放上一次调用强名称函数(如 StrongNameGetPublicKey、StrongNameTokenFromPublicKey 或 StrongNameSignatureGeneration)时分配的内存。
StrongNameGetBlob 函数
通过位于指定地址的可执行文件的二进制表示形式填充指定的缓冲区。
StrongNameGetBlobFromImage 函数
获取位于指定内存地址的程序集映像的二进制表示形式。
StrongNameGetPublicKey 函数
从私钥/公钥对中获取公钥。
StrongNameHashSize 函数
使用指定的哈希算法获取哈希值所需要的缓冲区大小。
StrongNameKeyDelete 函数
删除指定的密钥容器。
StrongNameKeyGen 函数
创建一个供强名称使用的新公钥/私钥对。
StrongNameKeyGenEx 函数
生成具有指定密钥大小的新的公钥/私钥对,以供强名称使用。
StrongNameKeyInstall 函数
向容器中导入一个公钥/私钥对。
StrongNameSignatureGeneration 函数
生成指定程序集的强名称签名。
StrongNameSignatureGenerationEx 函数
基于指定标志为指定的程序集生成强名称签名。
StrongNameSignatureSize 函数
返回强名称签名的大小。
StrongNameSignatureVerification 函数
获取一个值,该值指示所提供的路径中的程序集清单是否包含强名称签名(根据指定标志进行验证)。
StrongNameSignatureVerificationEx 函数
获取一个值,该值指示在提供的路径处的程序集清单是否包含强名称签名。
StrongNameSignatureVerificationFromImage 函数
验证已映射到内存的程序集对关联的公钥是否有效。
StrongNameTokenFromAssembly 函数
从指定的程序集文件中创建强名称标记。
StrongNameTokenFromAssemblyEx 函数
从指定的程序集文件创建强名称标记,并返回公钥。
StrongNameTokenFromPublicKey 函数
获取表示公钥的标记。
这个你可以在头文件StrongName.h里面找到相关的函数原型.
C#的声明如下:
[DllImport("mscoree.dll", EntryPoint = "StrongNameKeyDelete", CharSet = CharSet.Auto)]
public static extern bool StrongNameKeyDelete(string wszKeyContainer);
[DllImport("mscoree.dll", EntryPoint = "StrongNameKeyInstall", CharSet = CharSet.Auto)]
public static extern bool StrongNameKeyInstall([MarshalAs(UnmanagedType.LPWStr)] string wszKeyContainer,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2,
SizeConst = 0)] byte[] pbKeyBlob, int arg0);
[DllImport("mscoree.dll", EntryPoint = "StrongNameSignatureGeneration", CharSet = CharSet.Auto)]
public static extern bool StrongNameSignatureGeneration(string wszFilePath, string wszKeyContainer,
IntPtr pbKeyBlob, int cbKeyBlob, int ppbSignatureBlob,
int pcbSignatureBlob);
[DllImport("mscoree.dll", EntryPoint = "StrongNameErrorInfo", CharSet = CharSet.Auto)]
public static extern uint StrongNameErrorInfo();
[DllImport("mscoree.dll", EntryPoint = "StrongNameTokenFromPublicKey", CharSet = CharSet.Auto)]
public static extern bool StrongNameTokenFromPublicKey(byte[] pbPublicKeyBlob, int cbPublicKeyBlob,
out IntPtr ppbStrongNameToken, out int pcbStrongNameToken);
[DllImport("mscoree.dll", EntryPoint = "StrongNameFreeBuffer", CharSet = CharSet.Auto)]
public static extern void StrongNameFreeBuffer(IntPtr pbMemory);
签名过程:首先建立一个容器,使用容器对程序集进行签名.
一个重新签名的过程就这样完成了.对于RELINK的话,其实也简单:
private byte[] tryGetPublicKeyToken(string keyFileName)
{
try
{
byte[] newPublicKey;
using (FileStream keyPairStream = File.OpenRead(keyFileName))
{
newPublicKey = new StrongNameKeyPair(keyPairStream).PublicKey;
}
int pcbStrongNameToken;
IntPtr ppbStrongNameToken;
StrongNameTokenFromPublicKey(newPublicKey, newPublicKey.Length, out ppbStrongNameToken,
out pcbStrongNameToken);
var token = new byte[pcbStrongNameToken];
Marshal.Copy(ppbStrongNameToken, token, 0, pcbStrongNameToken);
StrongNameFreeBuffer(ppbStrongNameToken);
return token;
}
catch (Exception)
{
return null;
}
}
public void ReLink()
{
byte[] publicKeyToken = tryGetPublicKeyToken(ASSEMBLYNAME);
if (publicKeyToken == null)
{
return;
}
//初始化一个要重新签名的程序信息列表
AssemblyInfo[] assemblyInfoList = new[]
{
new AssemblyInfo {FileName = @"D:\Test\Test1.dll"},
new AssemblyInfo {FileName = @"D:\Test\Test2.dll"},
new AssemblyInfo {FileName = @"D:\Test\Test2.dll"}
};
//获得每个程序集的名称
foreach (AssemblyInfo assemblyInfo in assemblyInfoList)
{
assemblyInfo.FullName = AssemblyFactory.GetAssembly(assemblyInfo.FileName).Name.FullName;
}
//检查是否被引用,是的话,就替换PublicKeyToken
foreach (AssemblyInfo assemblyInfo in assemblyInfoList)
{
AssemblyDefinition assembly = AssemblyFactory.GetAssembly(assemblyInfo.FileName);
foreach (ModuleDefinition module in assembly.Modules)
foreach (AssemblyNameReference reference in module.AssemblyReferences)
if (assemblyInfoList.Any(a => a.FullName == reference.FullName))
{
reference.PublicKeyToken = publicKeyToken;
AssemblyFactory.SaveAssembly(assembly,assemblyInfo.FileName);
}
}
}
就是判断相关的一组程序集里,如果有引用当前操作的程序集,就将它的PublicKeyToken改为重新签名后的PublicKeyToken.
从一个KEY文件获取PublicKeyToken有2个方式:
1.使用新KEY对一个程序集签名,然后读取该程序集,获取PublicKeyToken,这个办法不推荐.
2.使用StrongNameTokenFromPublicKey可以直接从KEY文件提取PublicKeyToken,使用完毕别忘记使用StrongNameFreeBuffer释放内存.
写到这里,一个重新签名和重新链接的过程就完毕了.Mono是一个很强大类库,我会继续写一个Mono的方面的文章.
to be continue.....