通dnSpy的内存搜索去除Spire.XLS的PDF水印
本帖最后由 千人千面 于 2022-9-22 14:22 编辑根据大佬@cdj68765 提供的思路和方法!的方法,直接跳过了以下的Debug过程,详细请看结尾。
1、最近在用这个Spire.XLS把XLS表格生成PDF,但是免费版的居然也有水印!这我能忍?
2、不废话,由于是.NET平台写的,我们直接把它拖进DnsPy先给他PY一下看看.常规操作,先搜索下字符串,这里搜索的时候不建议一上来就搜索完整字符串,因为八成没啥结果。下面搜索出来了几个方法,我们挨个点进去看看
从这几个方法的上下文来推断,虽然里面包含了关键字:Evaluation Warning,但是,我可以肯定,和水印上的EvaluationWarning : The document was created with Spire.XLS for .NET没有半毛钱关系,至于我为什么这么肯定,因为我已经NOP掉来调试过了。那么遇到这种情况的时候,字符串搜索不出来什么结果,那么可以肯定的是,字符串八成是被加密了。你以为我就那你没办法了吗!太小看我了,不管你再怎么加密,始终要解密出来,既然要解密出来,那我们怎么知道它啥时候解密,解密的字符串又去哪儿找?当然是去内存里面找,这里写了个简单的demo来生成一个表格并且保存为PDF,经过分析,水印是在保存这一步被添加上去的,我们在保存的这行代码上打个断点让程序跑起来。
现在程序已经断下来了,接下来打开调试工具栏-窗口-内存-内存1
接下来我们让程序跑起来,然后在内存中搜索字符串,可以看见,内存中的确存在该字符串,接下来大家可能会问,我也看见了,问题是他是从哪儿出来的额?
file:///C:/Users/y15/AppData/Local/Temp/msohtmlclip1/01/clip_image016.jpg
我们在代码上单击SaveToFile进入详细代码
往下找到SaveToPdf方法,我们继续深入他
进来以后可以看见,数行代码,我们重点关注黄色的方法调用,可以肯定的是,水印一定是在其中的某个方法中被加上去的,但是我咋知道是那个方法?当然是打断点调试,这里采用二分法打断点,先在中间打一个断点,然后去内存中搜索,逐步缩小范围,直到精确定位到具体方法。
上面没有,这里按F10逐过程进行排查。
当运行到倒数第二行代码的时候,内存中出现了字符串。我们单击该方法进入。发现里面调用了一个method_28的方法,继续深入。。。
代码有点长,我们先在第一行打个断点,然后重新运行程序,在断点处F10逐过程调试
经过一系列操作。。。我们定位到了该方法调用
再次经过一顿操作,最终最终我定位到了字符串解密的地方
难怪搜索不到字符串,它被序列化成数组了。
那么接下来的方法就简单了,我们只需要干掉这个判断就行~
最后保存模块就收工了,生成的PDF已经没有水印了。
感谢大佬@cdj68765 提供的思路和方法!
2022年9月22日更新了一下方法,C#用户直接写一个拓展方法调用即可
调用方法为:
var wb = new Workbook();
wb.Crack();
拓展类为:
public static class SpireOfficeHelpers
{
public static void Print(string path)
{
var wb = new Workbook();
wb.Crack();
wb.LoadFromFile(path);
var p = wb.PrintDocument;
#pragma warning disable CA1416 // 验证平台兼容性
p.Print();
#pragma warning restore CA1416 // 验证平台兼容性
}
public static void Print(byte[] bytes)
{
MemoryStream memeStream = new(bytes);
var wb = new Workbook();
wb.Crack();
wb.LoadFromStream(memeStream);
var p = wb.PrintDocument;
#pragma warning disable CA1416 // 验证平台兼容性
p.Print();
#pragma warning restore CA1416 // 验证平台兼容性
}
/// <summary>
/// 注入激活信息
/// </summary>
/// <param name="workbook"></param>
public static void Crack(this Workbook workbook)
{
CrackLicense(workbook);
}
/// <summary>
/// 注入激活信息
/// </summary>
/// <param name="document"></param>
public static void Crack(this Document document)
{
CrackLicense(document);
}
/// <summary>
/// 注入激活信息,并返回该类型
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="t"></param>
/// <returns></returns>
public static T CrackLicense<T>(T t) where T : class
{
var InternalLicense = t.GetType().GetProperty("InternalLicense", BindingFlags.NonPublic | BindingFlags.Instance);
var TypeLic = InternalLicense.PropertyType.Assembly.CreateInstance(InternalLicense.PropertyType.GetTypeInfo().FullName);
foreach (var item in TypeLic.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
{
if (item.FieldType.IsArray)
{
item.SetValue(TypeLic, new string[] { "Spire.Spreadsheet", "Spire.DocViewer.Wpf" });
}
else if (item.FieldType.IsEnum)
{
item.SetValue(TypeLic, 3);
}
}
InternalLicense.SetValue(t, TypeLic);
return t;
}
}
本帖最后由 cdj68765 于 2022-9-9 23:14 编辑
给楼主两端代码,是之前研究Spire留下的
针对Word的
Spire.Doc.Document document = new Spire.Doc.Document(Mem);
var Lic = new Spire.License.InternalLicense();
Lic.LicenseType = Spire.License.LicenseType.Runtime;
Lic.AssemblyList = new string[] { "Spire.DocViewer.Wpf" };
var InternalLicense = document.GetType().GetProperty("InternalLicense", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
InternalLicense.SetValue(document, Lic);
针对Excel的
var Xls = new Spire.Xls.Workbook();
var Lic = new Spire.License.InternalLicense();
Lic.LicenseType = Spire.License.LicenseType.Runtime;
Lic.AssemblyList = new string[] { "Spire.Spreadsheet" };
var InternalLicense = Xls.GetType().GetProperty("InternalLicense", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
InternalLicense.SetValue(Xls, Lic);
这两段代码都可以在不用修改库的情况下,直接使用,原理就是在实例化后,将激活信息通过反射直接赋值到类里面
前面的分析过程和楼主的基本一样,包括字符串那里,后面的方向不太一样。楼主在找到判断是否加水印那里,好像没有继续分析下去
什么条件下能够不进入加水印这一步,而是选择了爆破吧。不过我有理由相信,楼主能够继续跟下去,然后直到找到激活信息那里,一定能找到跟我一样的结论的
PS:此方式仅适用于V10开头的版本,最新的V12并不适用,请注意 zhanglei1371 发表于 2022-9-14 08:44
您好,之前下载了最新的,用新建的文档测试,确实没有水印了。
不过还有两个问题待解惑:
1.测试发现 ...
说实话,你好会玩,哈哈哈
每次实例化或者载入文档的时候,验证信息都会被重置,因此在载入文档之后,紧跟着
InternalLicense.SetValue(Xls, TypeLic)
就行了,或者在保存之前赋值一次也行,反正代码上也只在保存的时候才验证。
Dim Xls = New Spire.Xls.Workbook()
Dim InternalLicense = Xls.GetType().GetProperty("InternalLicense", System.Reflection.BindingFlags.NonPublic Or System.Reflection.BindingFlags.Instance)
Dim TypeLic = InternalLicense.PropertyType.Assembly.CreateInstance(InternalLicense.PropertyType.GetTypeInfo().FullName)
For Each item In TypeLic.GetType().GetFields(System.Reflection.BindingFlags.NonPublic Or System.Reflection.BindingFlags.Instance)
If item.FieldType.IsArray Then
item.SetValue(TypeLic, New String() {"Spire.Spreadsheet"})
ElseIf item.FieldType.IsEnum Then
item.SetValue(TypeLic, 3)
End If
Next item
InternalLicense.SetValue(Xls, TypeLic)
Dim worksheets As WorksheetsCollection = Xls.Worksheets
worksheets.Add("sheetA")
Dim xlrg As XlsRange = worksheets("sheetA").Range("A1")
xlrg.Value2 = "这个没有水印!"
Xls.SaveToFile("textzl.pdf")
Xls.LoadFromFile("test.xlsx", ExcelVersion.Version2010)
InternalLicense.SetValue(Xls, TypeLic)
MsgBox(Xls.ActiveSheet.Range(1, 1).Text)
Xls.ActiveSheet.Range(1, 19).Text = "这个有水印!"
Xls.SaveToFile("textzl02.pdf")
关于交互问题,说实话这个没办法,这个是作者他本身的问题,我们作为使用者是无能为力的
真要用的话,我想也只能开两个线程交互通信了,摊手
cdj68765 发表于 2022-9-11 11:36
你把你项目里的引用spire dll全都清空了,然后从Nuget重新下载spire试试就可以了。
刚才我试了下,用你 ...
您好,之前下载了最新的,用新建的文档测试,确实没有水印了。
不过还有两个问题待解惑:
1.测试发现,若是打开现有的文档,而不是新建,还是有水印;
Dim XLS As New Workbook
XLS.LoadFromFile("test.xlsx", ExcelVersion.Version2010)
MsgBox(XLS.ActiveSheet.Range(1, 1).Text)
XLS.ActiveSheet.Range(1, 19).Text = "savetime"
XLS.Save()
2.若在同一个工程里,希望同时有xls和doc交互的话,此时就无法初始化,因为二者附带的pdf的dll是相同的。这种共存问题如何解决呢?
谢谢!
附件:https://wwi.lanzoup.com/iMbTu0bnfvne 本帖最后由 cdj68765 于 2023-1-8 21:20 编辑
330201818 发表于 2023-1-7 17:41
var Xls = new Spire.Xls.Workbook();
var...
using Spire.Xls;
using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SpireHook
{
internal class Program
{
internal class Natives
{
public static extern bool FlushInstructionCache(IntPtr hProcess, IntPtr lpBaseAddress, UIntPtr dwSize);
public static extern IntPtr GetCurrentProcess();
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
public enum PageProtection : uint
{
PAGE_NOACCESS = 0x01,
PAGE_READONLY = 0x02,
PAGE_READWRITE = 0x04,
PAGE_WRITECOPY = 0x08,
PAGE_EXECUTE = 0x10,
PAGE_EXECUTE_READ = 0x20,
PAGE_EXECUTE_READWRITE = 0x40,
PAGE_EXECUTE_WRITECOPY = 0x80,
PAGE_GUARD = 0x100,
PAGE_NOCACHE = 0x200,
PAGE_WRITECOMBINE = 0x400
}
}
private static void Main(string[] args)
{
uint VirtualProtect(IntPtr address, uint size, uint protectionFlags)
{
uint oldProtection;
if (!Natives.VirtualProtect(address, (UIntPtr)size, protectionFlags, out oldProtection))
{
throw new Win32Exception();
}
return oldProtection;
}
void FlushInstructionCache(IntPtr address, uint size)
{
if (!Natives.FlushInstructionCache(Natives.GetCurrentProcess(), address, (UIntPtr)size))
{
throw new Win32Exception();
}
}
var Xls = new Spire.Xls.Workbook();
Func<object, object, bool> replacemethod = (a0, a1) => true;
var replacement = replacemethod.GetMethodInfo();
foreach (var item in Xls.GetType().Assembly.DefinedTypes)
{
if (item.DeclaredFields.Count() == 4 && item.DeclaredMembers.Count() == 11 && item.DeclaredMethods.Count() == 5)
{
if (item.GetMethods(BindingFlags.Static | BindingFlags.NonPublic).Length == 5)
{
foreach (var item2 in item.GetMethods(BindingFlags.Static | BindingFlags.NonPublic))
{
if (item2.ReturnParameter.ParameterType.Name == "Boolean")
{
if (item2.GetParameters().Length == 2)
{
if (item2.GetParameters().ParameterType.Name == "Object")
{
RuntimeHelpers.PrepareMethod(item2.MethodHandle);
RuntimeHelpers.PrepareMethod(replacement.MethodHandle);
IntPtr originalSite = item2.MethodHandle.GetFunctionPointer();
IntPtr replacementSite = replacement.MethodHandle.GetFunctionPointer();
var is64 = IntPtr.Size != sizeof(int);
uint offset = (is64 ? 13u : 6u);
byte[] originalOpcodes = new byte;
unsafe
{
//segfault protection
uint oldProtecton = VirtualProtect(originalSite, (uint)originalOpcodes.Length, (uint)Natives.PageProtection.PAGE_EXECUTE_READWRITE);
//get unmanaged function pointer to address of original site
byte* originalSitePointer = (byte*)originalSite.ToPointer();
//copy the original opcodes
for (int k = 0; k < offset; k++)
{
originalOpcodes = *(originalSitePointer + k);
}
if (is64)
{
//mov r11, replacementSite
*originalSitePointer = 0x49;
*(originalSitePointer + 1) = 0xBB;
*((ulong*)(originalSitePointer + 2)) = (ulong)replacementSite.ToInt64(); //sets 8 bytes
//jmp r11
*(originalSitePointer + 10) = 0x41;
*(originalSitePointer + 11) = 0xFF;
*(originalSitePointer + 12) = 0xE3;
}
else
{
//push replacementSite
*originalSitePointer = 0x68;
*((uint*)(originalSitePointer + 1)) = (uint)replacementSite.ToInt32(); //sets 4 bytes
//ret
*(originalSitePointer + 5) = 0xC3;
}
FlushInstructionCache(originalSite, (uint)originalOpcodes.Length);
VirtualProtect(originalSite, (uint)originalOpcodes.Length, oldProtecton);
}
break;
}
}
}
}
break;
}
}
}
Xls.LoadFromFile(@"ExceltoImage.xlsx", ExcelVersion.Version2010);
Worksheet sheet2 = Xls.Worksheets;
sheet2.SaveToImage("ExceltoImage.jpg");
}
}
}
给你一段相对复杂一些的方案吧
使用该方案需要项目属性里开启运行不安全代码,毕竟直接操作了内存不得不这么做
代码使用了类的几个关键点的匹配,所以不能保证以后的版本也能用这个方法来操作 使用webapi调用的时候会出错。 spire.xls还有50个表的限制呢? NOP是啥 后面怎么操作的有视频教程吗? 只是去掉了水印,一些看不到的限制应该还在吧 学习一下
正需要,支持楼主大人了! 感谢楼主分享 接下来我们让程序跑起来,然后在内存中搜索字符串,可以看见,内存中的确存在该字符串,接下来大家可能会问,我也看见了,问题是他是从哪儿出来的额?
file:///C:/Users/y15/AppData/Local/Temp/msohtmlclip1/01/clip_image016.jpg
@千人千面 最后多出来的图是不是插入这里的,好像丢了。