目录
Crackme逆向实例67
所用工具
程序界面
预备知识—类型特征
预备知识—破解思路
步骤一 :静态分析
步骤二:动态调试分析
步骤三:序列号验证函数
步骤四:算法分析
步骤五:Python编写注册机
验证有效性
程序是一个Serial/Name,需要输入用户名和注册码才能通过。
Name\Serial:这种类型的一般是用Name进行一些运算,然后得到了Serial,接着Serial和自己输入的相比较。如果追码的话定位到程序最后比较处就可以了,如果写注册机的话就需要在程序取出Name的时候分析它的计算过程。
常用断点:GetWindowText、GetDlgItemText、SendMessage(编辑框句柄,WM_GETTEXT,长度,缓冲区地址)。
Nag:这种类型,无论是什么语言编写的,他最后总会调用windows API,可以从API断点入手,然后patch掉创建的窗口。
常用断点:MessageBox、CreateWindow、DialogBoxParam、CreateDialogParam
KeyFile:文件校验类型总会读文件,只有一条路走。
常用断点:CreateFile、ReadFile、GetFileSize
Serial:此类参考上边的Name\Serial类型,也是需要取出文本内容然后校验,简单的就是调用字符串比较函数,复杂的就是对Serial固定位或某几位运算求值,需要进行跟踪分析。
Cd_Check:这种会获取磁盘的类型,然后在进行校验,例如磁盘名、磁盘内文件。
常用断点:GetDriveType
Matrix:多选框矩阵类型的,有两种校验:1、每个多选框有对应的值,根据选中的多选框,获取每个多选框的值然后校验。2、将多选框当做按钮处理,点击后会触发按钮事件,符合后才会进行下次的校验,这种校验方法大多需要按顺序点击。
常用断点:IsDlgButtonChecked、按钮事件(不同语言按钮事件不同,通过反编译工具VB Decompiler、DeDe等查看)
VC程序:1、CreateWindow创建的窗口,向上找RegisterClass注册窗口类,这个API中有窗口的回调函数。2、DialogBoxParam创建的窗口,在参数中就有窗口回调函数。
找到窗口回调以后一般会有比较代码,例如与0x110、0x111,0x110是WM_INITDIALOG负责初始化操作,0x111是WM_COMMAND,COMMAND消息处理,程序的事件如按钮事件就在这里边。
VB程序:使用反编译工具查看反编译的代码或事件位置,我常用的工具:VB Decompiler、VBExplorer。
Delphi程序:也是使用反编译查看代码和事件,工具:DeDeDark。
MASM程序:如果没有加花指令的话这类型的是最简单了,没有编译器的附加代码,程序一目了然,又是上下浏览一下代码就能发现关键位置。因为也是调用的API,可以参考VC程序的思路。
运行程序,随意输入一个Name和Serial:
Name:figugegl
Serial:5573821
点击Check,弹出对话框"Wrong Serial"。
将软件程序拖入IDA中,对其进行静态分析。按F5查看其反汇编的C代码,根据Name\Serial特征来看,里面都会调用GetWindowText、GetDlgItemText、SendMessage等windows API函数,如图为IDA静态分析结果:其入口地址为:0x004011CB。
2、根据代码分析,0x0040121D之前的为主函数的栈帧空间的准备工作,我们在0x0040121D调用了段内函数,地址为:0x004015B0,此时设置断点进行调试。
按F7、F8进行调试,看到有很多API函数调用,如GetCommandLine函数,它是获得指向当前命令行缓冲区的一个指针,返回值为Long类型,是命令行缓冲区在内存中的地址。strchr函数功能为在一个串中查找给定字符的第一个匹配之处。一直单步调试,发现在0x004015B0处调用了段内函数,设置断点,并且在调试到该位置时发现该函数调用就是软件程序输入参数的入口函数。按F7进入该函数调用进行查看。3、分析0x0040123C处的段内函数,,看到了许多api函数调用,比如LoadCursorA,RegisterClassA,DialogBoxParamA,LoadIconA,SetClassLongA,SendDlgItemMessageA,SetDlgItemTextA,GetDlgItemTextA,MessageBoxA等等。
LoadCursor:
说明:从指定的模块或应用程序实例中载入一个鼠标指针。LoadCursorBynum是LoadCursor函数的类型安全声明
返回值:Long,执行成功则返回已载入的指针的句柄;零表示失败。在Windows 95 和Win16环境中,这个函数只能载入标准尺寸的图标
参数表
参数 | 类型及说明 |
---|---|
hInstance | Long,一个DLL的模块句柄;或者一个实例句柄,指定包含了鼠标指针的可执行程序 |
lpCursorName | String,作为一个字串,指定欲载入的指针资源。作为一个长整数值,指定欲载入的资源ID;或者设置一个常数,代表某幅固有系统指针。 |
RegisterClassA:
函数功能:该函数注册在随后调用CreateWindow函数和CreateWindowEx函数中使用的窗口类。 RegisterClass函数己经由函数RegisterClassEx函数来代替,但是,如果不需要设置类的小目标则仍然可以使用RegisterClass函数。
返回值:如果函数成功,返回值是唯一标识已注册的类的一个原子;如果函数失败,返回值为0。
参数:
参数 | 类型及说明 |
---|---|
lpWndClass | 指向一个WNDCLASS结构的指针。在将它传递给函数之前,必须在该结构中填充适当的类属性。 |
DialogBoxParamA:
函数功能:该函数根据对话框模板资源创建一个模态的对话框。在显示对话框之前,函数把一个应用程序定义的值作为WM_INITDIALOG消息的IParam参数传到对话框过程,应用程序可用此值来初始化对话框控制。
返回值:如果函数调用成功则返回值为在对函数EndDialog的调用中的nResult参数,该EndDialog函数用于中止对话框。如果函数调用失败,则返回值为C1。若想获得错误信息,请调用GetLastError函数。
参数:
参数 | 类型及说明 |
---|---|
hlnstance | 标识一个模块的事例,该模块的可执行文件含有对话框模板。 |
IpTemplateName | 标识对话框模板。此参数可以指向一个以NULL结尾的字符串的指针,该字符串指定对话框模扳名,或是指定对话框模板的资源标识符的一个整型值。如果此参数指定了一个资源标识符,则它的高位字一定为零,且低位字一定含有标识符。一定用MAKEINTRESOURDE宏指令创建此值。 |
hWndParent | 指定拥有对话框的窗口。 |
IpDirlogFunc | 指向对话框过程的指针。有关更详细的关于对话框过程的信息,请参见DialogProc。 |
dwlnitaram | 指定传递到WM_INITDIALOG消息的IParam参数中的对话框过程的值。 |
LoadIconA:
说明:从指定的模块或应用程序实例中载入一个图标。其中,LoadIconBynum是LoadIcon函数的类型安全声明
返回值:Long,执行成功则返回已载入的图标的句柄;零表示失败。会设置GetLastError
参数表
参数 | 类型及说明 |
---|---|
hInstance | Long,一个DLL的模块句柄;或者一个实例句柄,指定包含了图标的可执行程序 |
lpIconName | String,作为一个字串,指定欲载入的图标资源。作为一个长整数值,指定欲载入的资源ID;或者设置一个常数,代表某幅固有系统图标。如装载的是一个固有系统图标,注意hInstance参数应设为零。在api32.txt文件中以前缀IDI_ 作为标志 |
SetClassLongA:
说明:为窗口类设置一个Long变量条目
返回值:Long,由nIndex指定的的类信息的前一个值。零表示出错。会设置GetLastError
参数表
参数 | 类型及说明 |
---|---|
hwnd Long,欲为其设置类信息的那个窗口的句柄 nIndex Long,参考GetClassLong函数的nIndex参数说明 dwNewLong Long,类信息的新值,具体取决于nIndex
SendDlgItemMessageA:
函数功能:该函数把一个消息发送给指定的对话框中的控制。
返回值:返回值指定消息处理的结果,且依赖于发送的消息。
参数:
参数 | 类型及说明 |
---|---|
hDlg | 指定含有控制的对话框。 |
nlDDigltem | 指定接收消息的控制的标识符。 |
Msg | 指定将被发送的消息。 |
wParam | 指定消息特定的其他信息。 |
IParam | 指定消息特定的其他信息。 |
SetDlgItemTextA:
函数功能:该函数设置对话框中控制的文本和标题。
返回值:如果函数调用成功,则返回值为非零值。如果函数调用失败,则返回值为零。若想获得更多的错误信息,请调用GetLastError函数。
参数:
参数 | 类型及说明 |
---|---|
hDlg | 指定含有控制的对话框。 |
nlDDlgltem | 标识带有将被设置的标题和文本的控制。 |
IpString | 指向一个以NULL结尾的字符串指针,该字符串指针包含了将被复制到控制的文本。 |
GetDlgItemTextA:
函数功能:该函数获取对话框中与控制有关的文本或标题。
返回值:如果函数调用成功,则返回值表示被复制缓冲区的字符串的长度,不包括以NULL结尾的字符串。如果函数调用失败,则返回值为零。若想获得更多错误信息,请调用GetLastError函数。 参数:
参数 | 类型及说明 |
---|---|
hDlg | 指向含有控制的对话框的句柄。 |
nlDDlgltem | 指定标题或文本将被检索的控制的标识符。 |
IpString | 指向获取标题或文本的缓冲器的指针。 |
nMaxCount | 指定被复制到lpString参数指向的缓冲区的字符串的最大长度。如果字符串的字符最大长度超过范围,则该字符串被截断。 |
MessageBoxA:
函数功能:该函数创建、显示、和操作一个消息框。消息框含有应用程序定义的消息和标题,加上预定义图标与Push(下按)按钮的任何组合。
参数:
参数 | 类型及说明 |
---|---|
hWnd | 标识将被创建的消息框的拥有窗口。如果此参数为NULL,则消息框没有拥有窗口。 |
IpText | 指向一个以NULL结尾的、含有将被显示的消息的字符串的指针。 |
IpCaption | 指向一个以NULL结尾的、用于对话框标题的字符串的指针。 |
uType | 指定一个决定对话框的内容和行为的位标志集。此参数可以为下列标志组中标志的组合。 |
GetDlgItemTextA函数是获取对话框中与控制有关的文本或标题,浏览代码,发现有两个地方调用了GetDlgItemTextA和GetDlgItemTextA,我们猜测这是用来获取Name和Serial的,在这两个地方设置断点。
memset函数:复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。 程序使用memset函数将内存中从地址0x0019FEDC开始的内存单元中填充了40个0。
跟踪堆栈发现,调用第一个GetDlgItemTextA函数时,此时EAX=0019F7A9,调用完该函数后,会将对话框中的内容存放到EAX的内存单元中,查看该内存单元,发现存储的是Serial。调用完该函数后,EAX存放的是输入字符的长度。
调用第二个GetDlgItemTextA函数时,此时EAX=0019F7D2,发现该函数获得的是输入的Name。调用完该函数后,EAX存放的是输入字符串的长度。
在获取Name和Serial之后,发现该程序还有一个对Name字符串长度判断的校验方法,如果Name长度大于等于8,则执行0x004013C1地址处的代码,如果小于8的话会调用MessageBoxA函数,提示用户输入的名字长度太短。
我们继续向下查看代码,有一行代码cmp edi,esi。它是比较edi和esi的值,如果edi小于esi,则会循环向上执行。而我们想要的是edi大于esi,然后让代码顺序向下执行,最终得到破解注册成功的提示。
继续分析,发现在错误提示框之前会有一个跳转语句,它会比较内存单元中的两个值,如果相等,则可以跳过这个错误提示函数。这里是将自己输入的Serial值和程序计算的Serial值进行比较,不相等,则提示错误信息;相等,则跳转到指定地址代码处。
进行校验的代码如下:
004013C3 |. EB 6E jmp short 00401433
004013C5 |> 89F0 mov eax, esi
004013C7 |. 29F8 sub eax, edi
004013C9 |. 0FB6543D AE movzx edx, byte ptr [ebp+edi-52]
004013CE |. 31FA xor edx, edi
004013D0 |. 89D9 mov ecx, ebx
004013D2 |. 31F9 xor ecx, edi
004013D4 |. 01CA add edx, ecx
004013D6 |. 885405 D6 mov byte ptr [ebp+eax-2A], dl
004013DA |. 89F0 mov eax, esi
004013DC |. 29F8 sub eax, edi
004013DE |. 8A4405 D6 mov al, byte ptr [ebp+eax-2A]
004013E2 |. 3C 20 cmp al, 20
004013E4 |. 73 0B jnb short 004013F1
004013E6 |. 89F0 mov eax, esi
004013E8 |. 29F8 sub eax, edi
004013EA |. 8D4405 D6 lea eax, dword ptr [ebp+eax-2A]
004013EE |. 8000 20 add byte ptr [eax], 20
004013F1 |> 89F0 mov eax, esi
004013F3 |. 29F8 sub eax, edi
004013F5 |. 0FB64405 D6 movzx eax, byte ptr [ebp+eax-2A]
004013FA |. 3D 80000000 cmp eax, 80
004013FF |. 7C 09 jl short 0040140A
00401401 |. 89F0 mov eax, esi
00401403 |. 29F8 sub eax, edi
00401405 |. C64405 D6 20 mov byte ptr [ebp+eax-2A], 20
0040140A |> 89F0 mov eax, esi
0040140C |. 29F8 sub eax, edi
0040140E |. 8A5405 84 mov dl, byte ptr [ebp+eax-7C]
00401412 |. 3A5405 D6 cmp dl, byte ptr [ebp+eax-2A]
00401416 |. 74 1A je short 00401432
开始时,ESI中存放Name的字符串长度,EBX中存放Serial的字符串长度。用到了密码的位数,就从密码位数入手吧。从1-8位都试了一遍,发现只有8位的时候取出了密码作比较,看来这个是验证密码位数的手段。填写Name=figugegl,Serial=12345678。
将Name的长度值复制到eax中,初试时eax=8。[ebp+edi-52]内存单元存放的是Name数据,刚开始edi=0,之后inc edi会使查找的内存单元往后移位。第一个取出来的是字符'f'(66),将其存放到edx中。然后将edx('f')和edi(从0开始增加)做异或操作,ecx(Name字符串长度)和edi做异或操作,然后将这两个异或操作的结果做加法操作,然后将相加的结果6E('n')存放在[ebp+eax-2A]内存单元中,此时eax=eax-edi。然后再将[ebp+eax-2A]中的值与0x20比较,如果大于等于32(0x20),则跳转到地址0x004013f1。之后会将计算的6e('n')与0x80进行比较,如果小于0x80则直接将计算的值和用户输入的Serial值进行比较;如果大于0x80,则该Serial位置是一个空格(0x20)。内存单元[ebp+eax-7C]中的值赋给al,该内存单元中存放的值是38(8),是Serial中的第八位数据,然后和根据Name第一个字符‘f’计算的数据6e('n')进行比较,如果两个数据相等,则edi+1,计算Name第二个字符对应的Serial第七位的数值。可以看出来Serial是从后往前依次验证的,必须等于8位。
我们重复上面这个操作,就可以破解出默认用户名figugegl对应的Serial值。将每次破解的字符填写到Serial中对应的位置。最终逐位破解的结果是:'n','q','o',' ','o','m','o','z'。故最终破解的结果是:默认用户名figugegl的Serial是zomo oqn。
004013C3 |. EB 6E jmp short 00401433
004013C5 |> 89F0 mov eax, esi
004013C7 |. 29F8 sub eax, edi
004013C9 |. 0FB6543D AE movzx edx, byte ptr [ebp+edi-52]
004013CE |. 31FA xor edx, edi
004013D0 |. 89D9 mov ecx, ebx
004013D2 |. 31F9 xor ecx, edi
004013D4 |. 01CA add edx, ecx
004013D6 |. 885405 D6 mov byte ptr [ebp+eax-2A], dl
004013DA |. 89F0 mov eax, esi
004013DC |. 29F8 sub eax, edi
004013DE |. 8A4405 D6 mov al, byte ptr [ebp+eax-2A]
004013E2 |. 3C 20 cmp al, 20
004013E4 |. 73 0B jnb short 004013F1
004013E6 |. 89F0 mov eax, esi
004013E8 |. 29F8 sub eax, edi
004013EA |. 8D4405 D6 lea eax, dword ptr [ebp+eax-2A]
004013EE |. 8000 20 add byte ptr [eax], 20
004013F1 |> 89F0 mov eax, esi
004013F3 |. 29F8 sub eax, edi
004013F5 |. 0FB64405 D6 movzx eax, byte ptr [ebp+eax-2A]
004013FA |. 3D 80000000 cmp eax, 80
004013FF |. 7C 09 jl short 0040140A
00401401 |. 89F0 mov eax, esi
00401403 |. 29F8 sub eax, edi
00401405 |. C64405 D6 20 mov byte ptr [ebp+eax-2A], 20
0040140A |> 89F0 mov eax, esi
0040140C |. 29F8 sub eax, edi
0040140E |. 8A5405 84 mov dl, byte ptr [ebp+eax-7C]
00401412 |. 3A5405 D6 cmp dl, byte ptr [ebp+eax-2A]
00401416 |. 74 1A je short 00401432
Name = input('Name:')
Len = len(Name)
print('Name Length:', Len)
# print(chr(0x20))
result=[]
# 计算用户名逐位字符对应的Serial数值
for i in range(Len):
tmp = (ord(Name[i])^i) + (Len^i) # 步骤(1)操作
if tmp >= 0x80: # 步骤(2)操作
tmp = chr(0x20) # 32空格
else:
tmp = chr(tmp)
result.append(tmp)
print(result)
# 输出注册码
result.reverse() # list中元素反向
print('Serial:', "".join(result))
破解码一:
Name:12345678
Serial:N?@=B;<9
破解码二:
Name:jianpeng
Serial:ovm xmqr