Abstract
FigureBest作为一款科研绘图美化工具,它可以帮助科研人员快速美化Matlab绘图,提高绘图效率,使图像生产更适合科研论文。该工具在试用版本中会有过期问题,针对这一问题,可即通过更改系统时间来绕过试用限制。然而直接更改系统时间,可能导致浏览器等其他软件出现问题,这是因为许多应用程序依赖于客户端的时间戳验证。针对上述问题,本文提出了一个时间函数hook方案,可以仅更改Matlab获取的系统时间,而不改变其他软件获取的时间戳,解决试用限制问题的同时,能够保证系统的正常运行。这种方法同样也适用于破解其他的软件的证书有效期验证。
keywords:科研绘图,时间函数Hook,时间戳,有效期验证
Instruction
绘图风格在学术论文中的地位重中之重,一个清晰高端的图像,能够极大提高论文的观感,使你的论文更加专业、更易阅读。
单论数据可视化工具,Python、Matlab、origin、echarts、Adobe AI等工具都可以帮我们快速绘制出图像,然而这些工具输出的原始图像都达不到顶刊论文中的视觉效果,因此后续的美化工作就是非常重要的了,上述软件中Python对图像的美化能力较低,因此图像的美化上限不高,不建议使用。origin和Adobe AI虽然图像定制能力很强,但是不支持使用代码进行数据处理,在绘制一些复杂数据集的时候,我们通常会涉及到对原始数据的二次处理,使得我们观测的实验数据能够直接导出图像,而无需反复运行预处理脚本,如:用python或java进行预处理的步骤可以直接使用Matlab进行数据→绘图的一条龙服务来代替。因此综合考虑可扩展性和美化上限,Matlab和echarts,都是不错的选择。这其中,Matlab的数据处理更强,而echarts外观定制能力更强,可根据需要进行选择。
本文中主要是使用Matlab进行科研绘图美化工作,对于传统的美化工作,我们总需要运行各种代码,来调整图像边框、颜色、线条粗细等,来自代码的调整无法做到所见即所得,虽然我们可以借助Matlab官网画廊(https://www.mathworks.com/products/matlab/plot-gallery.html#)进行风格参考,但直接使用代码进行美化工作依然会花费巨大的时间成本。
针对这个痛点,b站up主【图通道】在2021年4月19日推出了一个成熟的科研绘图美化工具 FigureBest 4.0 虽然支持的功能较少,但可以直接下载进行FigureBest 4.0功能体验 。同时作者也在GitHub上开源了FigureBest-2.0 项目(https://github.com/tutusjtu/FigureBest-2.0)以供大家参考学习。最近(2023年10月14日)作者进一步推出了功能更加强大的 FigureBest 4.6版本,并在11月份开放了了FigureBest 4.6 试用版下载 。FigureBest主要是通过自动识别Matlab绘图窗口,并调用各类颜色、边框、线条调整函数,对图像进行实时调整,并内置了适合科研论文的绘图风格极大提高了绘图效率,通过该工具美化后的图像,可以轻松达到科研论文标注,只需要花费几秒钟的点击时间,真正做到了点击就送,并且软件仅占用3MB的空间,体积非常小,作为Matlab插件,即插即用。
然而该试用版本将会在11月1日过期(本人用的是10月内测版,最新公测版是11月30日过期)。而正版软件需要收费,且费用不低。
安全老油子们对这种试用过期的软件已经见怪不怪了,早在15年左右,《火哥内核VT》系列课程中6.10小节【SIDT 拦截2 反VT检测讲解】视频中17:07秒处,就提到了:使用过期证书签名的驱动,如何在非测试模式下,通过更改系统时间来启动。同理对于很多证书相关的密码学算法,不使用联网通信的情况下,只能通过获取系统时间的方式进行验证。也就是说通过更改系统时间我们就可以轻松启动此试用版本,无论当前真实的时间如何。
然而这样更改时间的方式虽然可以破解试用软件的时间限制,却同样造成了浏览器无法正常工作的情况,由于网络应用一致是安全工程师重点保护的对象,服务器经常需要校验客户端的时间戳以确定客户端的合法性,很多密码学方案的底层方案也对时间戳的有效性强依赖。这就导致,更改系统时间后,我们访问互联网的很多网站(如:chatgpt)都会发生未知错误,且浏览器只是已知的可能发生错误的重灾区,更多其他软件笔者虽然没有逐一测试,但产生错误的概率也是非常大的,比如类似【百度网盘】【英雄联盟】【原神】【MobaXterm】等这些对于网络通信强依赖的应用,都可能发生错误。
那么能否有一种方式,仅仅更改Matlab获取到的系统时间,而不改变其他软件获取到的时间戳呢?答案是,能。这也就是本文提出的解决方案:时间函数hook。这个方案广泛适用于任何时间前测类的软件,比如当年有名的ARK工具PC-Hunter其试用证书存在有效期,我们同样可以通过这种方式进行破解。
Contribution
Find WIN32 Time API Function
API Penetration Testing Process
根据我们的猜想,软件中应当是调用了Matlab时间戳函数now作为license.txt凭证的校验比对时戳,因此我们只需要找到如何劫持matlab的内置函数now,让他始终返回一个小于11月1日的固定值,比如始终返回2023年8月1日,即可使得license的校验结果始终处在有效期内。
我们首先尝试在百度上,查找C语言中如何获取系统时间,网上给出的代码为:
time_t timer0;
timer0 = time(NULL);
printf("%lld\n", timer0);
下一步我们就需要看一下,time函数是调用了哪一个系统的通用API来实现的,因为考虑到复用性问题,一般可供用户层调用的功能类似的API函数一般在5个以内,否则微软后期的维护将难以进行,用户层调用的方式越多,windows操作系统所需维护的函数越多,我估计他们应该不会想加班维护一些p用没有的东西。更何况是获取系统时间这个简单的功能。
我们搜索一下C语言的time函数是调用了哪个WIN32 API,不出意外的没有找到结果,毕竟这个问题过于底层,而且小众。接下来我们只能通过自己实验验证,来确定time函数的调用方式。
我们使用CE的Open Process → File → Create Process功能创建一个进程,CE调试器会自动在Entry Point处对程序进行断点
接下我们在PE文件的【增量链接】汇编区,找到测试程序的main入口(注意要编译成debug模式才有这些注释,否则全是地址编号,可读性很低,这应该不用多说),按空格键跳转到程序main入口处
在入口函数首行,没多远的位置,找到了我们代码的关键字符串,往前推一个函数,就是我们自己调用的 time(NULL); 按空格跳转进去。
跟进去后发现,time函数所调用的是ucrtbased.time64 动态链接库函数。熟悉C++开发的人应该很熟悉这个ucrtbased.dll,因为换到没有装Microsoft Visual Studio 2015的电脑上运行程序的时候,总是会报错找不到ucrtbased.dll。这个dll是一个服务于c++程序的二次封装dll。既然windows出产的系统文件里没有它,说明它还不够底层。也就是ucrtbased.time64应该还会调用更底层的WIN32 API,那个API才是我们的目标。
跟到time64函数中可以发现,time64进一步调用了KERNELBASE.GetSystemTimeAsFileTime,而KERNELBASE.dll是系统出场时自带的,满足我们的要求,因此我们首先尝试对KERNELBASE.GetSystemTimeAsFileTime进行修改。
我们首先使用断点hook来测试一下对GetSystemTimeAsFileTime 能否更改Matlab时间函数 now的返回值。我们首先断点该函数,并获取到时间响应值为01DA0D44C709EB77 记录下该值后
通过阅读该函数汇编可知,rax作为时间戳,将会将低位分别赋值到,rcx所指向的地址处,和+4字节的偏移地址处,我们在运行mov [rcx],eax之前对rax值进行修改,将其改为固定值,即可实现时间函数hook
此时切换到Matlab运行两侧时间戳函数,可以发现两次获取到的时间不同,这说明 GetSystemTimeAsFileTime 函数并不在 now函数的调用分支上,我们还需要收集其他可能的API函数进行测试。
Hook Code Revision
当我们还是初学者时,有时无法确定自己修改的汇编是否正确,此时可以使用chat-gpt帮我们做订正,以确定我们的写法是否正确,提示词模板如下:
经过调试发现KERNELBASE.GetSystemTime 并不在matlab now的调用路径上,KERNELBASE.GetSystemTimeAsFileTime确实调用了,但是hook之后时间没有改变,你看我hook的对吗KERNELBASE.GetSystemTimeAsFileTimemov eax,7FFE0014mov rax,[rax] mov rax,0x01DA0CAD76950693 #我的hook点返回一个固定时间戳mov [rcx],eax shr rax,20mov [rcx+04],eax 你的Hook代码似乎已经正确地目标到了KERNELBASE.GetSystemTimeAsFileTime,这是一个Windows API函数,Matlab可能会使用它来获取系统时间。你在Hook中将返回值更改为一个固定时间戳,这应该会使Matlab的now函数返回固定时间。
Collect And Test More Available APIs
当我们熟悉上述测试流程之后,就可以对每个潜在的系统API进行Hook后再测试其能否改变Matlab时间函数的行为,直到Hook后的函数可以使得两次调用 now的结果返回同一个时间戳为止。
那么如何收集API呢,以往我们只能靠看黑客相关的论坛,或者微软官方文档,来找寻可能调用的API,如今我们已经定位到了KERNELBASE中存在时间函数,那么根据软件架构的合理性,相似的功能应该会被划分到同一个dll中,由此我们可以让GPT根据我们已经找到的API去推测其他可能的API函数。提示词模板如下:
我觉得问题在于,matlab 的now没有调用GetSystemTimeAsFileTime这个函数,你还知道什么系统函数可以获取时间吗,我换一个hook函数试试 你是对的,Matlab的now函数可能并不直接调用GetSystemTimeAsFileTime,因此Hooking该函数可能不会影响Matlab的now行为。Matlab可能使用其他系统调用或机制来获取时间。 另一个常用的获取系统时间的Windows API函数是GetSystemTime,但你已经尝试过并发现它不在now的调用路径上。 你可以尝试Hook以下其他可能与时间获取相关的函数,但请注意,这仅仅是一种尝试,因为Matlab的内部行为可能因版本和配置而异。以下是一些可能与时间获取有关的Windows API函数: 请注意,Hooking这些函数仍然可能不会影响Matlab的now函数,因为Matlab可能使用自己的内部实现来获取时间。此外,修改Matlab的内部行为可能会违反MathWorks的许可协议,因此请谨慎行事,并确保你有合法的使用方式。
经过GPT的提示,我们又分别测试了GetSystemTime与GetLocalTime函数,通过分析汇编代码,我们分析对于GetLocalTime函数的代码,可以在如下位置Hook Rax寄存器实现固定时间戳的功能
对函数进行hook后我们再次运行 now函数测试修改效果:
可以看到不管我运行多少次 now 函数,其返回的时间戳均固定不变,支持我们已经能确定需要Hook的系统时间函数。
Auto Assemble DLL Development
Time Function Reconstruction
通过之前的分析不难看出,GetLocalTime函数的汇编行数较多,虽然修改起来并不复杂,但是由于对于rax的修改是在汇编中部进行的,且由于系统函数的infinite hook通常涉及到 16字节左右的代码空间,这回覆盖大量现有的汇编代码,导致通过汇编Hook重构该函数的工作量非常大。
相比之下,根据微软对于函数的官方说明我们不难看出,如果只是返回固定时间戳的话,通过C代码重写该函数是非常简单的,只需填充一下LPSYSTEMTIME 结构就行了。
void GetLocalTime( [out] LPSYSTEMTIME lpSystemTime);[out] lpSystemTime A pointer to a SYSTEMTIME structure to receive the current local date and time. typedef struct _SYSTEMTIME { WORD wYear; WORD wMonth; WORD wDayOfWeek; WORD wDay; WORD wHour; WORD wMinute; WORD wSecond; WORD wMilliseconds;} SYSTEMTIME, *PSYSTEMTIME, *LPSYSTEMTIME;
因此综合考虑时间成本,本文选择使用API infinite hook进行实现。
我们首先通过如下代码填充了SYSTEMTIME结构体,重写了自定义时间函数:
void myGetLocalTime(_Out_ LPSYSTEMTIME lpSystemTime) {
lpSystemTime->wYear = 2023;
lpSystemTime->wMonth = 8;
lpSystemTime->wDayOfWeek = 2;
lpSystemTime->wDay = 1;
lpSystemTime->wHour = 21;
lpSystemTime->wMinute = 13;
lpSystemTime->wSecond = 20;
lpSystemTime->wMilliseconds = 895;
}
接下来我们使用之前编写的自动化注入类进行API 无限hook操作:
void mySystemTimeHook()
{
HANDLE handle = GetCurrentProcess();
ULONG_PTR myGetLocalTimeAddr = (ULONG_PTR)myGetLocalTime;
injectHelper::apiHook(handle, "KERNELBASE", "GetLocalTime", myGetLocalTimeAddr);
}
Far Jump Hardcode
这里面的重点是升级我们的generateJmpCode方法,该方法在之前破解AxMath的时候用过,但是只适配了x86汇编,本次更新将对该函数进一步升级以适应x64的远跳转:
SmartBytes generateJmpCode(LONG_PTR from, LONG_PTR to)
{
LONG_PTR offset = to - (from + 2);
if (abs(offset) <= MAXINT8)
{
byte tempCode[2] = { 0xEB,0x00 };//0xEA
tempCode[1] = (byte)offset;
SIZE_T length = sizeof(tempCode) / sizeof(tempCode[0]);
return getDynamicBytes(tempCode, length);;
}
offset = to - (from + 3);
if (abs(offset) <= MAXINT16) {
byte tempCode[3] = { 0xE9,0x00,0x00 };//0xEA
bit_converter::i16_to_bytes(offset, false, tempCode + 1);
SIZE_T length = sizeof(tempCode) / sizeof(tempCode[0]);
return getDynamicBytes(tempCode, length);;
}
offset = to - (from + 5);
if(abs(offset) <= MAXINT32) {
byte tempCode[5] = { 0xE9,0x00,0x00,0x00,0x00 };//0xEA
bit_converter::i32_to_bytes(offset, false, tempCode + 1);
SIZE_T length = sizeof(tempCode) / sizeof(tempCode[0]);
return getDynamicBytes(tempCode, length);;
}
else {
offset = to;
//jmp 0x7FF6C4580000
byte tempCode[14] = { 0xFF,0x25,0x00,0x00,0x00,0x00, 0x00,0x00,0x58,0xC4,0xF6,0x7F,0x00,0x00 };
bit_converter::i64_to_bytes(offset, false, tempCode + 6);
SIZE_T length = sizeof(tempCode) / sizeof(tempCode[0]);
return getDynamicBytes(tempCode, length);;
}
}
这里需要注意的是,远跳转硬编码 FF 25的编码规则为:
FF 25 + 4个字节00 + 8个字节绝对地址 (也就是int64)
相比之下,元函数调用的硬编码 FF 15 的编码更怪了:
FF 15 + 02 00 00 00 +EB 08 + 8个字节绝对地址
我找GPT问了一下编码规则,但是它给出的答复给我绕晕了。可以暂时理解为 02 00 00 00 和 EB 08 都是固定值。
Auto Assemble DLL Injection
最后就是如何将编写好的Hook DLL 赶在 FigureBest运行之前,注入到Matlab进程里,如果像之前Axmath破解那样直接修改Matlab的导入表来注入dll,可能会造成外来某些需要正常调用时间函数的代码运行不正常,为了避免这种情况,最好在常规状态下不对matlab进行注入,只有运行Figure Best的时候才运行注入程序。
经过观察发现,FigureBest存在一个入口脚本 FB.m,这个脚本是未被加密的可以随意编辑,因此我们在脚本首行添加一段dll加载代码,即可赶在FigureBest之前进行注入:
loadlibrary('userplugins/dllpoc_64.dll');
%% show msg
disp('FB is starting...')
%% encoding check
% ---------------------------
% PASS: UTF8 OR GBK
% WARNING: 'OTHERS'
DefaultCharacterSet = feature('DefaultCharacterSet');
locale = feature('locale');
encoding = locale.encoding;
if ~strcmp(DefaultCharacterSet,'GBK') | ~strcmp(encoding,'GBK')
disp(['[DefaultCharacterSet]' DefaultCharacterSet '; ' '[encoding]' encoding])
disp('Chaos character MAY occur if were NOT GBK, while main functions wonnot be affected!')
disp('Please change the encoding IF possible')
end
%% set path
% ---------------------------
folderOfThis = fileparts(mfilename('fullpath')); % get the folder of current .m
addpath(genpath(folderOfThis)); % add path and subpath (temporary)
savepath % add path and subpath (permanent)
cd(folderOfThis) % change current folder
clear folderOfThis
%% open
FigureBest_v4
最后需要注意的是,Matlab加载dll需要配一个.h文件才能正常加载,这个文件只需要和dll名称一样即可(如:dllpoc_64.h),文件内容可以是空的!