获课地址:666it.top/14016/
时间限制是软件保护中常见的手段,包括试用期、软件订阅和定时自毁等。逆向新手在面对时间校验时,常试图简单修改系统时钟或暴力跳转,却往往触发暗桩或导致程序功能异常。本文将系统分析时间校验的多种形式,并提供从静态到动态的完整突破思路。
常见误区:粗暴修改系统时间与跳转指令
-
修改系统时间: 在运行软件前将系统时间调整到试用期内。这种方法对于联网校验或具有时间连续性检查的软件完全无效,且用户体验极差。
-
暴力NOP或JMP: 在反汇编中找到
GetLocalTime等API调用后的比较跳转指令,直接将其改为无条件跳转(JMP)或空指令(NOP)。这种方法极易掉入“陷阱”:- 多时间点校验: 程序在启动、保存、关键功能等多个地方进行时间检查,修补一处,别处仍会触发。
- 完整性校验: 程序会检查自身代码段是否被修改,导致触发保护。
- 加密密钥依赖: 程序的解密密钥可能来源于时间校验的结果,粗暴跳过会导致后续数据解密失败,程序崩溃。
保护机制突破思路:理解逻辑与精准打击
突破时间校验的核心在于“欺骗”,而非“破坏”。我们需要提供一个符合程序期望的、可控的“假时间”。
- API Hook(动态): 创建一个DLL,在目标进程加载时注入其中。然后钩取(Hook)关键的时间获取API(如
GetLocalTime,GetSystemTimeAsFileTime),让这些API返回我们指定的、合法的日期。 - 内存修补(动态/静态): 找到程序中存储首次运行时间、到期时间或当前时间计算结果的全局变量或内存地址,在运行时使用调试器或内存修改工具将其修改为合法值。
- 逻辑分析(静态): 彻底逆向时间校验算法,理解其校验逻辑。例如,它可能计算当前时间与一个固定基准时间的差值。我们可以通过修改基准时间(例如,注册表中存储的安装日期)来“延长”试用期。
代码实战:多种时间校验的对抗
代码1:简单的试用期检查
c
#include <windows.h>
#include <stdio.h>
// 模拟从注册表或文件读取安装日期
FILETIME get_install_time() {
SYSTEMTIME st = {2023, 10, 3, 1, 12, 0, 0, 0}; // 安装时间:2023-10-1
FILETIME ft;
SystemTimeToFileTime(&st, &ft);
return ft;
}
BOOL is_trial_expired() {
FILETIME install_ft = get_install_time();
FILETIME now_ft;
GetSystemTimeAsFileTime(&now_ft); // 获取当前系统时间
// 将FILETIME转换为64位整数进行计算
ULARGE_INTEGER install, now;
install.LowPart = install_ft.dwLowDateTime;
install.HighPart = install_ft.dwHighDateTime;
now.LowPart = now_ft.dwLowDateTime;
now.HighPart = now_ft.dwHighDateTime;
// 试用期30天 (100-nanosecond intervals)
const ULONGLONG trial_period = 30ULL * 24 * 60 * 60 * 10000000;
if (now.QuadPart - install.QuadPart > trial_period) {
printf("错误:软件试用期已过。\n");
return TRUE; // 已过期
}
printf("剩余试用期:%llu 天。\n", (trial_period - (now.QuadPart - install.QuadPart)) / (10000000ULL * 60 * 60 * 24));
return FALSE; // 未过期
}
int main() {
if (is_trial_expired()) {
return -1;
}
// 程序主逻辑
printf("软件正常运行...\n");
return 0;
}
对抗方法:
- 内存修补: 调试器在
GetSystemTimeAsFileTime返回后,找到存储now_ft的内存地址,将其修改为一个与安装时间相差小于30天的值。 - API Hook: 编写DLL Hook
GetSystemTimeAsFileTime,使其始终返回一个固定的、合法的假时间。
代码2:依赖时间结果的解密(暗桩)
c
#include <windows.h>
#include <stdio.h>
#include <string.h>
// 一个简单的异或加密函数,密钥来源于时间
void decrypt_data(char* data, int len, DWORD key) {
for (int i = 0; i < len; i++) {
data[i] ^= (key + i) & 0xFF;
}
}
void critical_function() {
// 一段被加密的关键代码或字符串
char encrypted_msg[] = {0x95, 0xA3, 0xB4, 0x87, 0x92, 0x83, 0x8A, 0x8C, 0x8F, 0x00}; // 加密的"HelloWorld"
DWORD time_seed = GetTickCount(); // 使用系统运行时间作为解密密钥的一部分
decrypt_data(encrypted_msg, strlen(encrypted_msg), time_seed);
// 如果解密密钥不对,解密出的字符串是乱码,后续比较会失败
if (strcmp(encrypted_msg, "HelloWorld") == 0) {
printf("关键功能执行成功: %s\n", encrypted_msg);
} else {
printf("程序状态异常,即将退出!\n"); // 暗桩触发!
exit(1);
}
}
int main() {
if (is_trial_expired()) {
return -1;
}
// 如果粗暴跳过了时间校验,这里的time_seed会是“错误”的值,导致暗桩触发
critical_function();
return 0;
}
对抗方法:
- 逻辑分析: 必须静态分析
critical_function,发现其解密过程依赖于GetTickCount()。如果我们要跳过主时间校验,就必须保证GetTickCount()的返回值在我们的控制范围内,或者直接修补decrypt_data函数,使其不做任何操作(NOP掉循环),或者修改比较指令。
总结
对抗时间校验是一场“信息”的战争。盲目修改跳转是最低级且危险的方法。最高效的策略是动态欺骗时间API,提供稳定的假时间源。而对于复杂的、将时间作为算法因子的保护,则必须进行深入的静态分析,理解其整体逻辑,才能进行精准的、不影响程序稳定性的修补。