演示版保护技术
0x01 序列号保护方式
1.1 序列号保护机制
1.1.1 将用户名等信息作为自变量,通过F变换之后得到注册码
公式
序列号=F(用户名)
将注册码和用户输入的注册码进行比较,由于负责验证注册码合法性的程序在用户机器上运行,所以用户可以用调试器分析函数F过程。并且计算出来的序列号是以明文形式出现在内存中。也可以通过修改比较指令来绕过注册码检查。
1.1.2通过注册码验证用户名的正确性
公式
用户名=F(序列号)
这里要求F是一个可逆变化。在软件检查注册码的时候,利用可你变换F^-1对用户输入的注册码继续变换。如果最后计算结果与用户名相同,则说明是正确的注册码。
与上一个想必,正确注册码的明文未出现在内存中,注册函数F也未出现在内存中。
对于这类检查办法,除了修改比较指令。还有以下考虑
- 由于F^-1出现在了软件中,可以找出其逆变化即F。这样也可以得出正确的注册码
- 给定一个用户名,穷举找出注册码。适合穷举难度不大的函数
- 给定一个序列号,变化出一个用户名(用户名中可能存在不可打印字符)
1.1.3通过对等函数检查注册码
F1(用户名)=F2(注册码)
如果F2是一个可逆函数,则本方法是第二种方法的扩展,解密方法也类似(这里其实可以不考虑输入的用户名,而是将F1(用户名)当作上述所输入的用户名来看待。)
上面三种方法的自变量都只有一个,自变量是用户名或者注册码。
1.1.4同时将用户名和注册码当作自变量
特定值=F3(用户名,序列号)
此时用户名和序列号的关系没有那么清晰了,但是这样也可能失去了用户名和序列号之间一一对应的关系,软件开发者可能无法写出注册机。
注册码的复杂性问题归根结底是一个数学问题。设计难以求逆的算法,软件作者需要一定的数学基础。当然,注册码的算法在复杂,如果软件可以被随意修改也不行,通过修改跳转指令很容易变成注册版。所以有好的算法是不够的。话要结合软件完整性检查。
2.如何攻击序列号保护机制
像要找到序列号或者修改判断序列号的跳转指令,首先是要定位代码段。
通常用户会在编辑框中输入注册码,软件需要通过一些标准API将用户输入复制到缓冲区。如GetWindowTextA,GetDlgItemTextA,GetDlgItmeInt,hmemcpy(仅windwos9x/Me)等。完成注册码的判断后会显示一个i对话框告诉用户注册码是否正确。MessageBoxA,MessageBoxExA,Showwindow,MessageBoxIndirectA,CreateDialogParamA,CreateDiaglogIndirectA,DialogBoxParamA等API常用于显示对话框。
另一种方法是跟踪程序启动时对注册码的判断流程(程序每次启动时,都会将注册码读出进行判断)从而决定是否以注册版的模式工作。根据序列号的存放位置不同,可以使用不同的API断点。如果序列号存在注册表中可以使用RegQueryValueExA函数;如果序列号存放在INI文件中,可以使用GetPrivateProfileStringA,GetPrivateProfileIntA,GetProfileIntA等函数;如果序列号存放在一般的文件中可以使用CreateFileA,_lopen等函数。
2.1数据约束性的秘诀
这个概念由+ORC提处,只在用明文比较注册码的保护方式。在大多数序列号保护程序中,真正的注册码会出现在某个时刻的内存中。
数据约束性(Data constraint)或者密码相邻性(Password proximity)的一居室:加密者在编程的时候需要留意保护功能是否”工作“,必须看到用户输入的数字,以及用户输入的转换结果和真正密码之间的关系。通常,它们共同位于一小块栈区域中。
2.2hmemcpy函数
hmemcpy函数(俗称”万能断点“)是windows 9x系统的内部函数,它的作用是将内存的一块区域复制到另一个地方。windows 9x系统频繁使用该函数处理各种字符串。
2.3利用消息断点
许多序列号保护软件都有一个按钮,当按下和释放鼠标的时候,将发送WM_LBUTTONDOWN和WM_LBUTTONUP消息,对此消息下断点很容易找到按钮的事件代码。
2.4利用提示消息
大多数软件设计时采用了人机对话方式。所谓人机对话,就是在软件执行一段程序之后显示一串提示信息,,以反映该段程序运行后的状态。(仔细想想就和调试信息差不多,但是这个面向的普通用户,代表某段代码执行完或开始执行)
3.字符串比较形式
为了防止解密者修改跳转指令,往往会采取一些技巧,从而迂回比较字符串。
3.1寄存器直接比较
mov eax,[] ;一般是16进制数
mov ebx,[]
cmp eax,ebx
jz(jnx) xxx
3.2函数比较a
mov eax,[] ;可能时16进制数,也可能是地址
mov ebx,[]
call xxxx ;可能是api函数也可能是自己写的比较函数
test eax,eax
jz(jnz) xxx
这种情况下,call的函数一般是个BOOL函数。分析时主要关注call指令返回时对eax的处理。
3.3函数比较b
push xxxx ;可能是寄存器也可能时地址
push xxxx
call xxxxxx ;可能是api函数也可能是自己写的比较函数
test eax,eax
jz(jnz)
3.4串比较
lea edi,[]
lea esi,[]
repz cmpsd ;比较字符串a和b
jz(jnz)
4.制作注册机
4.1.对明码比较软件的攻击
只要正确序列号在内存中以明文形式出现,就都属于这一类。
4.2.非明码比较
详细看第五章实验
0x02警告窗口
警告(Nag)窗口时软件设计者用来不时提醒用户购买正式版本的窗口。
去除警告窗口的三种方法是:
- 修改程序资源
- 静态分析
- 动态分析
修改程序资源可以将警告窗口的属性设为透明或者不可见变相的去除窗口;若要完全的去除只需要找到创建该窗口的代码将其跳过。显示窗口的函数有MessageBoxA,MessageBoxEXA,showWindow,CreateWindowEX等。如果这些窗口对某些警告窗口无效,这时可以尝试利用消息设置断点拦截,如WM_DESTORY。
0x03时间限制
一类是限制使用时长;另一类是限制使用时间。
1.计时器
1.1setTimer函数
应用程序在初始化时调用这个API函数,向系统申请一个定时器并指定计时器的时间间隔,同时获得一个处理计时器的回调函数。若计时器超时,系统会向该计时器的窗口发送消息WM_TIMER,或者调用程序提供的回调函数。函数原型如下
UINT SetTimer(HWND hWnd,UINT nIDEvent,UINT uElapse,TIMEPROC lpTimerFunc);
- hWnd:窗口句柄, 若计时器到时,系统向该窗口发送WM_TIMER消息
- nIDEvent:计时器标识
- uElapse:指定计时器时间间隔(以毫秒为单位)。
- TIMERPROC:回调函数,若计时器超时,系统调用该函数,如果该参数为NULL,发送消息。
回调函数原型
void CALLBACK TimerProc(HWND hwnd,UINT uMsg,UINT idEvent,DWORD dwTime);
1.2高精度的多媒体计时器
多媒体计时器的精度可达1毫秒。应用程序可以通过timeSetEvent函数启动一个多媒体计时器。原型如下
MMRESULT timeSetEvent(UINT uDelay,UINT uResolution,LPTIMECALLBACK lpTimeProc,DWORD_PTR dwUser,UINT fuEvent);
1.3GetTickCount函数
该函数返回的是系统自启动以来经过的时间(以毫秒为单位)。将该函数两次返回值相减,就知道程序运行了多少时间。这个函数的精度取决于系统的设置。
1.4timeGetTime函数
多媒体计时器函数timeGetTime也可以返回Windows自启动后所经过的时间(以毫秒为单位)
2.时间限制
演示版软件一般都有使用时间限制,例如试用30天。其保护方式大致如下
在安装软件的时候由安装程序取得当前系统日期,或者由主程序在第一次运行的时候记录系统日期。将其记录在某个地方。程序每次运行的时候取得当前的系统日期,并与之前记录的安装日期进行比较,如果超出了允许的时间就停止运行。
如果考虑周全可以保存两个时间。一个是安装时间,一个是最近运行时间,如果系统时间小于最近运行时间说明修改了机器时间,软件可以拒绝运行。
用于获取时间的API函数有GetSytemTime,GetLocalTime和GetFileTime。还有一种方法就是读取需要频繁修改的系统文件(如windows注册表user.dat等)的最后修改时间。
采用时间限制的软件必须能防范RegMon,FileMon之类的监视软件,否则时间的存放位置很容易被找到。
3.拆解时间限制保护
cmp eax,13是比较随后退出程序的函数,nop掉即可。
跳过SetTimer函数也可。
辅助变速工具也可以加快和减慢程序的时间,一般与动态分析配合使用。
0x04菜单功能限制
这种程序一般是Demo版,某些功能无法使用,一般有两种情况。
一种是试用版和正式版是两种完全不同的版本,试用版没有部分功能的代码。
另一种是试用版和正式版是同一文件,里面有功能的代码,注册后就能使用全部功能。
1.相关函数
1.1EnableMenuItem函数
BOOL EnableMenuItem(HMENU hMEnu,UINT uIDEnableItem,UINT uEnbale)
- hMenu:菜单句柄
- uIDEnableItme:欲禁止或允许的一个菜单条目标识符
- uEnable:控制标志,包括MF_ENABLE,MF_GRAYED,MF_DISABLE,MF_BYCOMMAND和MF_BYPOSITION
- 返回值:返回菜单项以前的状态,若菜单项不存在,返回FFFFFFFFh。
1.2EnableWindow函数
允许或禁止指定窗口
BOOL EnableWindow(HWND hWnd,BOOL bEnable)
- hWnd:窗口句柄
- bEnbale:”TRUE“为允许,”FALSE“为禁止
- 返回值:非0成功,0失败
2.拆解菜单限制保护
uEnable参数改成0或者直接jmp过去都行
0x05KeyFile保护
KeyFile是一种利用文件来注册软件的保护方式。KeyFile一般是一个小文件,可以是纯文本文件也可以是不显示字符的二进制文件。其内容是一些加密或者未加密的数据。软件每次启动的时候,从该文件读取数据然后利用某种算法进行处理,根据处理的结果判断是否为正确的注册文件。
1.相关API函数
所有与文件操作相关的API函数都可作文动态跟踪破解的断点。
API | 作用 |
---|---|
FindFirstFileA | 确定注册文件是否存在 |
CreateFileA | 确定文件是否存在,打开文件获得句柄 |
GetFileSize | 获得注册文件大小 |
GetFileAttributesA | 获得注册文件属性 |
SetFilePointer | 移动文件指针 |
ReadFile | 读取文件内容 |
2.拆除KeyFile保护
Process Monitor设置过滤器
这个就是路径,由于是CreateFileA函数,请保证文件名没有中文字符
随后设置条件断点分析,详细请看第五章实验。
0x06网络验证
网络验证的优点是将一些关键数据放到服务器上,软件必须从服务器中取得数据才能运行。拆解网络验证的思路是拦截服务器返回的数据包,分析程序是如何处理数据包的。
1.相关函数
当一个连接建立后,就可以传输数据了。常用的数据传送函数有seed()和recv()两个socker函数,以及微软扩展函数WSASend和WSARecv。
1.1send函数
客户端一般使用send发送请求,服务器使用send发送应答。
int send(
SOCKER s, //套接字描述符
const char FAR *buf, //缓冲区
int len, //实际要发送数据的字节数
int flags //附加标志一般为0
)
1.2recv函数
不论是客户还是服务器应用程序,都是用recv函数从TCP连接的另一端接受数据
int recv(
SOCKET s, //套接字描述符
char FAR *buf, //缓冲区
int len, //缓冲区buf长度
int flags //附加标志一般为0
)
2.破解网络验证的思路
如果网络验证的数据包内容固定,可以将数据包抓取,写一个本地服务端来模拟服务器,如果严重的数据包内容不固定就要分析其结构,找出相应的算法。
详细见实验
0x07光盘检测
一些以光盘形式发行的游戏,在使用时要检查光盘是否插在光驱中,如果没有则拒绝运行。
最简单最常见的光盘检测就是在程序启动的时候判断光驱的光盘里是否存在特定文件,如果不存在,则认为用户没有使用正版光盘。Window中的具体表现就是用GetLogicDriveStrings或者GetLogicalDrives函数得到安装的所有驱动器的列表,然后用GetDriveType函数检查每一个驱动器,如果是光驱,则用CreateFile或FindFirstFile函数检查特定的文件是否存在,进一步检查文件的属性大小。这种只需要找到检查光驱的地方直接跳过即可。
还有一种将程序运行的关键数据放在光盘中,这种可以靠刻录工具复制多份应对。
1.相关函数
1.1GetDriverType函数
UINT GetDriveType{
LPCTSTR lpRootPathName //根目录地址
}
返回值:
- 0:驱动器不能识别
- 1:根目录不存在
- 2:移动存储器
- 3:固定驱动器(硬盘)
- 4:远程驱动器(网络)
- 5:CD-ROM驱动器
- 6:RAM disk
1.2GetLogicDrives函数
该函数用于获取逻辑驱动器符号,没有参数
返回值:如果失败就返回0,否则返回由位掩码表示的当前可用驱动器
bit 0 drive A
bit 1 drive B
bit 2 drive C
1.3GetLogicalDriveStrings函数
该函数用于获取当前所有逻辑驱动器的根驱动器路径
DWROD GetLogicalDriveStrings(
DWROD nBufferLenth, //缓冲区大小
LPTSTR lpBuffer //缓冲区地址 如果成功返回形式为“C\: D\:”
)
如果成功就返回实际的字符数,否则返回0
1.4GetFileAttributes函数
用于判断指定文件的属性
DWORD GetFileAttributes(
LPCTSTR lpFileName //指定一个欲获取其属性的文件的名字
)
2.拆解光盘保护
0x08只运行一个实例
Windows是一个多任务操作系统,应用程序可用多次运行以形成多个运行实例。但有时基于对某些方面的考虑,要求程序只能运行一个实例。
1.实现方法
1.1查找窗口法
在程序运行前,用FindWindowA,GetWindowText函数查找具有相同类名和标题的窗口。
HWND FindWindowA(
LPCTSTR lpClassName, //指向窗口类名
LPCTSTR lpWindowName, //指向窗口文本
)
返回值:如果未找到相符窗口,返回0。
1.2使用互斥对象
互斥对象通常用于同步连接。一般用CreateMuteA函数实现,它的作用是创建有名或无名的互斥对象。
HANDLE CreateMutexA(
LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全属性
BOOL bInitialOwner, //指定互斥对象的初始身份
LPCTSTR lpName //指定互斥对象名
)
1.3使用共享区块
创建一个共享区块,该区块拥有读取,写入和共享保护属性,可用让多个实例共享同一内存块。将一个变量作为计数器放到该区块中,该应用程序的所有实例可以共享该变量,从而通过该变量知道有没有正在运行的实例。
文章评论