《加密与解密》-演示版保护技术
序列号保护方式
序列号/注册码:用户把自己信息(用户名、邮件地址、机器特征码等)给公司,公司根据信息利用预先编写的一个用于计算注册码的程序(注册机/keygen)算出一个序列号,通过邮件等形式发给用户。用户使用注册信息和序列号完成验证,软件就会取消限制(时间、功能等)
序列号保护机制
软件验证序列号,其实就是验证用户名和序列号之间的数学映射关系
- 用户名自变量,函数$F$求注册码:$序列号=F(用户名)$,很脆弱
破解思路:提取函数$F$;修改比较指令绕开检查
- 通过注册码验证用户名正确性:仍然是$序列号=F(用户名)$,但F是一个可逆变换,软件检查注册码时,利用$F$的逆变化$F^{-1}$对用户输入的注册码进行变换,结果与用户名相同则说明是正确的注册码
破解思路:修改比较指令;逆向$F^{-1}$得到$F$;穷举法
- 通过对等函数检查注册码:$F_{1}(用户名)=F_{2}(序列号)$
- 同时将用户名和注册码作为自变量:$特定值=F_3(用户名,序列号)$
注册码的复杂性问题归根到底是一个数学问题
如何攻击序列号保护机制
- 跟踪输入注册码后的判断找到注册码:软件会调用一些标准的API将用户输入的注册码字符串复制到自己的缓冲区中,常用的包括GetWindowTextA(W)、GetDlgItemTextA(W)、GetDlgItemInt、hmemcpy等,此外对话框也是切入点,如MessageBoxA(W)、MessageBoxExA(W)、ShowWindow、MessageBoxIndirectA(W)、CreateDialogParamA(W)、CreateDialogIndirectParamA(W)、DialogBoxParamA(W)、DialogBoxIndirectParamA(W)等
- 跟踪程序启动时对注册码的判断过程:程序每次启动时都需要将注册码读出并加以判断,根据序列号的位置,如注册表,使用RegQueryValueExA(W);INI文件,使用GetPrivateProfileStringA(W)、GetPrivateProfileIntA(W)、GetProfileIntA(W)、GetProfileStringA(W);一般文件,使用CreateFileA(W)、_lopen()等
数据约束性:明文比较注册码的保护方式中,正确的注册码会在某个时刻出现在内存中
利用消息断点:按下和释放鼠标时将发送消息,可以用这个消息下断点找事件代码
利用提示信息:就是关键字符串定位
字符串比较形式
- 寄存器直接比较
- 函数比较,通常是bool函数
- 串比较:repz cmpsd
警告窗口(Nag)
Nag窗口用来提醒用户购买正式版本,去除Nag的方法
- 修改程序的资源:设置Nag窗口属性为不可见变相去除,若要完全去除要找到创建窗口的代码并跳过,显示窗口的常用函数有MessgaeBoxA(W)、MessageBoxExA(W)、DialogBoxParamA(W)、ShowWindow、CreateWIndowExA(W)等
- 静态分析
- 动态分析
时间限制
两种限制:限制每次运行的时长;每次运行的时长时间不限,但有时间限制
计时器
在Windows中,计时器的选择如下
setTimer()函数
UINT SetTimer (HWND hWnd, UINT nIDEvent, UINT uElaspe, TIMERPROC lpTimerFunc)
- hWnd:窗口句柄。若计时器到时,系统将向这个窗口发送WM_TIMER消息
- nIDEvent:计时器标识
- uElapse:指定计时器时间间隔(毫秒单位)
- TIMERPROC:回调函数。若计时器超时,将调用此函数。若参数为NULL,超时时将向相应的窗口发送WM_TIMER消息
- 高精度的多媒体计时器
应用程序可以跳过调用timeSetEvent()函数来启动一个媒体计时器
- GetTickCount()函数:返回的是系统自成功启动以来所经过的时间(毫秒)
- timeGetTime()函数:返回Windows自启动后所经过的时间(毫秒)
时间限制
考虑周全软件需要保存两个时间值:
- 安装时间:第一次运行/安装软件时记录,最好存储在多个地方,防止解密者通过修改或删除,使得软件可以无限使用
- 最近一次运行的日期:防止用户将机器日期改回去。每次退出取出该日期与当前比较,若当前日期大于该日期,则替换。每次启动也要读取该日期,与当前日期比较,若该日期大于当前日期,说明用户修改了机器时间
拆解时间限制保护
- 直接跳过SetTimer函数(jmp)
- 利用WM_TIMER消息(0x0113),搜索113字串,定位跳转函数
菜单功能限制
这类程序一般是Demo版的,菜单或窗口的部分选项为灰色,无法使用。这种受限程序一般分为2种
- 试用版和正式版完全不同:G了,没法获取正式版
- 同一个文件,注册后就可以用正式版:破解启动!
相关函数
EnableMenuItem():
BOOL EnableMenuItem(HMENU hMenu, UINT uIDEnableItem, UINT uEnable)
- hMenu:菜单句柄
- uIDEnableItem:想要允许或禁止的一个菜单条目的标识符
- uEnable:控制标志,包括MF_ENABLED、MF_GRAYED、MF_DISABLED、MF_BYCOMMAND、MF_BYPOSITION
EnableWindow():
BOOL EnableWindow(HWND hWnd, BOOL bEnable)
- hwnd:窗口句柄
- hEnable:TRUE允许,FALSE禁止
拆解菜单限制保护
可以在上述函数调用时修改enable的标志,比如push 1改为push 0
KeyFile保护
KeyFile是一种利用文件来注册软件的保护方式,其内容是一些加密或未加密的数据,其中可能包含用户名、注册码等信息
网络验证
这种做法可以将一些关键数据放到服务器上。破解网络验证的思路是拦截服务器返回的数据包,分析程序如何处理数据包的
相关函数
send():
int send( SOCKET s, // 套接字描述符 const char FAR *buf, // 缓冲区 int len, // 实际要发送数据的字节数 int flags // 附加标志,一般为0 );
recv():
int recv( SOCKET s, // 套接字描述符 char FAR *buf, // 缓冲区 int len, // 缓冲区buf的长度 int flags // 附加标志,一般为0 )
破解网络验证的一般思路
如果网络验证的数据包内容固定,可以将数据包抓取,写一个本地服务端来模拟服务器;否则需要分析结构,找到相应算法
光盘检测
光盘检测是为了防止用户将正版拷贝安装在多台机器上且同时使用
相关函数
- GetDriveType():获取磁盘驱动器的类型
返回值:0-驱动器不能识别;1-根目录不存在;2-移动存储器;3-固定驱动器;4-远程驱动器;5-CD-ROM驱动器;6-RAM disk
- GetLogicalDrives():获取逻辑驱动器符号
GetLogicalDriveStrings():获取当前所有逻辑驱动器的根驱动器路径
DWORD GetLogicalDriveStrings( DWORD nBufferLength, // 缓冲区大小 LPTSTR lpBuffer // 缓冲区地址,成功返回结果形式为“c:\d:\” )
- GetFileAttributes():判断指定文件的属性
只运行一个实例
Windows多任务OS,应用程序可以多次运行形成多个运行实例,但基于安全性等考虑,要求程序只运行1个实例
实现方法
- 查找窗口法:FindWindowA、GetWindowText查找具有相同窗口名和标题的窗口
- 使用互斥对象:CreateMutexA函数实现
- 使用共享区块