【破解作者】 Bmzy
【作者主页】 http://www.9ycn.com/
【使用工具】 OllyDbg v1.10 , PEiD v0.92
【破解平台】 WinXP SP2
【软件名称】 PopChar Win V1.2
【下载地址】 http://www.skycn.com/soft/20579.html
【软件简介】 这个工具让你插入各种字体的字和 HTML 标识,到你的文件中。
【软件大小】 995 KB
【破解声明】 学习交流并快乐着......
--------------------------------------------------------------------------------
【破解内容】 PEiD检查,程序的编程语言是Microsoft Visual C++ 7.0,无壳。
od载入,软件开始,到注册处,填入1234abcd。设断MessageBoxA,按确定按钮,停下后按Ctrl+F9往上返,返到405D2D,
向上拉判断应是确定按钮的处理函数,开始分析。
----------------------------------------------------------------------------------------------------------------------------
00405BAC push 100 ; /Count = 100 (256.)
00405BB1 push PopChar.0042D4D8 ; |Buffer = PopChar.0042D4D8
00405BB6 push 3F0 ; |ControlID = 3F0 (1008.)
00405BBB push esi ; |hWnd
00405BBC call dword ptr ds:[<&USER32.GetDlgIte>; \GetDlgItemTextA 这里是函数调用获取字串
00405BC2 push PopChar.0042D4D8 ; ASCII "1234abcd"
00405BC7 call PopChar.0040EDB0 复制一个字串,eax返回指针
00405BCC push eax
00405BCD call PopChar.0040CBE0 ; 将注册码存注册表,后面验证如错误,
00405BD2 push 0 还会将注册表中的键值清空
00405BD4 call PopChar.0040CC40 ---------- ------------分析发现,这就是验证函数
00405BD9 mov eax,dword ptr ds:[42D7E8]
00405BDE add esp,0C
00405BE1 test eax,eax
00405BE3 je PopChar.00405C7A 并不是真正的判断跳转,
00405BE9 mov eax,PopChar.0042D4D8 ;ASCII "1234abcd" 真正的在405C7A处
00405BEE lea edx,dword ptr ds:[eax+1]
00405BF1 mov cl,byte ptr ds:[eax]
00405BF3 inc eax
00405BF4 test cl,cl
00405BF6 jnz short PopChar.00405BF1
00405BF8 sub eax,edx
00405BFA mov dword ptr ss:[esp+C],eax
00405BFE jnz short PopChar.00405C7A
00405C00 push 0B4 ; /Result = B4 (180.)
00405C05 push esi ; |hWnd
00405C06 call dword ptr ds:[<&USER32.EndDialog>; \EndDialog
00405C0C push 0
00405C0E push esi
00405C0F call PopChar.004058B0
00405C14 push 0
00405C16 call PopChar.0040CBE0
00405C1B call PopChar.004022C0
00405C20 call PopChar.0040FFB0
00405C25 push 272D
00405C2A call PopChar.0040F480
00405C2F mov ecx,dword ptr ds:[42E9D0]
00405C35 add esp,10
00405C38 push eax ; /Text
00405C39 push ecx ; |hWnd => 0004046A ('PopChar - Unregistered',class='PCW')
00405C3A call dword ptr ds:[<&USER32.SetWindow>; \SetWindowTextA
00405C40 call PopChar.00409660
00405C45 mov edx,dword ptr ds:[42BBF8] ; PopChar.0042057C
00405C4B push edx
00405C4C push 2725
00405C51 call PopChar.0040F480
00405C56 add esp,4
00405C59 push eax
00405C5A push 2711
00405C5F call PopChar.0040F480
00405C64 add esp,4
00405C67 push eax
00405C68 call PopChar.00406D80
00405C6D add esp,0C
00405C70 pop edi
00405C71 mov eax,1
00405C76 pop esi
00405C77 retn 10
00405C7A call PopChar.004015B0 这里是检查注册码验证完的结果,
00405C7F test eax,eax 然后分支
00405C81 je short PopChar.00405CD6
--------------------------------------------------------------------
4015B0函数很简单,在这里说明。函数判断[42DC94]中的值如果是
7或6,函数返回真,否则返回假,代表注册码错误。
--------------------------------------------------------------------
00405C83 push 0B4 ; /Result = B4 (180.)
00405C88 push esi ; |hWnd
00405C89 call dword ptr ds:[<&USER32.EndDialog>; \EndDialog 如果注册码正确,将关闭注册对话框
00405C8F push 0
00405C91 push esi
00405C92 call PopChar.004058B0
00405C97 call PopChar.004022C0 这里还会再验证一遍,也是调用40CC40
00405C9C call PopChar.0040DCA0
00405CA1 call PopChar.0040FFB0
00405CA6 push 272C
00405CAB call PopChar.0040F480
00405CB0 add esp,0C 如果注册码正确,将会将主窗口标题
00405CB3 push eax ; /Text 改为“PopChar”,并做其它相应处理
00405CB4 mov eax,dword ptr ds:[42E9D0] ; |
00405CB9 push eax ; |hWnd => 0004046A ('PopChar - Unregistered',class='PCW')
00405CBA call dword ptr ds:[<&USER32.SetWindow>; \SetWindowTextA
00405CC0 call PopChar.00409660
00405CC5 mov ecx,dword ptr ds:[42BBF8] ; PopChar.0042057C
00405CCB push ecx
00405CCC push 2726
00405CD1 jmp PopChar.00405C51
00405CD6 push 0 如果注册码错误,主窗口标题在下面
00405CD8 call PopChar.0040CBE0 仍设为“PopChar - Unregistered”
00405CDD call PopChar.0040FFB0
00405CE2 push 272D
00405CE7 call PopChar.0040F480
00405CEC mov edx,dword ptr ds:[42E9D0]
00405CF2 add esp,8
00405CF5 push eax ; /Text
00405CF6 push edx ; |hWnd => 0004046A ('PopChar - Unregistered',class='PCW')
00405CF7 call dword ptr ds:[<&USER32.SetWindow>; \SetWindowTextA
00405CFD call PopChar.00409660
00405D02 push 2727
00405D07 call PopChar.0040F480
00405D0C add esp,4
00405D0F push eax
00405D10 push 2711
00405D15 call PopChar.0040F480
00405D1A add esp,4
00405D1D push eax
00405D1E call PopChar.00406EE0
00405D23 add esp,8
00405D26 pop edi
00405D27 mov eax,1
00405D2C pop esi
00405D2D retn 10
----------------------------------------------------------------------------------------------------------------------------
在这一层分析后发现:
1. 验证函数是405BD4处调用的40CC40。
2. 验证后的结果标志放在[42DC94]中,函数4015B0调用判断正确的值应该是7或6,但通过实际改内存发现:7才是正确无限制的。
3. 注册码验证通过的后,明码存在注册表HKEY_CURRENT_USER\Software\ergonis\PopChar\user下名为license的键值中。
实际刚获得注册码就会存到该键值,但如果验证码错误,就会将该值清空。
4. 通过在[42DC94]处下内存读写断点,发现程序会多次调用检查,每次调用主窗口时都会检查。
分析认为是,程序的限制在未注册时部分字符不允许使用,并灰色显示,因此在主窗口刷新的时候和点击字符的时候
都要检查这个值,以确定如何显示,是否限制,如点击灰色的字符,将跳出注册对话框。
下面进入验证函数40CC40进行分析。
----------------------------------------------------------------------------------------------------------------------------
函数40CC40中:
0040CC10 mov edx,dword ptr ds:[42DC90]
0040CC16 sub esp,8
0040CC19 push 0
0040CC1B lea eax,dword ptr ss:[esp+4]
0040CC1F push eax
0040CC20 lea ecx,dword ptr ss:[esp+C]
0040CC24 push ecx
0040CC25 push edx
0040CC26 call PopChar.0040CA50 验证函数
0040CC2B mov dword ptr ds:[42DC94],eax 验证完后将把结果存入[42DC94]
0040CC30 add esp,18
0040CC33 retn
0040CC34 nop
0040CC35 nop
0040CC36 nop
0040CC37 nop
0040CC38 nop
0040CC39 nop
0040CC3A nop
0040CC3B nop
0040CC3C nop
0040CC3D nop
0040CC3E nop
0040CC3F nop
函数进入后在这里,下面jmp之前的几个调用是始化几个值,在后面验证中使用。这几个函数很简单,未列出,下面进行了说明。
0040CC40 push 5B31
0040CC45 call PopChar.0040CCC0 常数0x5B31存[42BB20]
0040CC4A push 40
0040CC4C push PopChar.0042BA20
0040CC51 call PopChar.0040CCD0 常数0x40存[42DCA4],地址42BA20存[42DC20],42BA20是一个码表的地址
0040CC56 push PopChar.004203D4 ;ASCII "FORUSEBYMACDINGQWTZLKJHPXV"
0040CC5B call PopChar.0040CCF0 字串地址4203D4存[42BB24]
0040CC60 push PopChar.0040C9F0
0040CC65 call PopChar.0040CD00 地址40C9F0(用于验证的函数的地址)存[42BB2C]
0040CC6A push PopChar.0040CA40
0040CC6F call PopChar.0040CD10 地址40CA40(用于验证的函数的地址)存[42BB30]
0040CC74 add esp,18
0040CC77 jmp PopChar.0040CC10
----------------------------------------------------------------------------------------------------------------------------
在这一层分析后发现:
1. 验证函数仍在下一层调用中,是40CC26处调用的40CA50。
2. 确认验证后的结果是标志放在[42DC94]中。
3. 40CA70处调用的40CFF0中,先将[42DCA8]中写入0,然后启动一个线程,线程执行seelp,暂停2000ms后,将[42DCA8]写入3,在验证过程中
将使用这个变量,在验证函数执行后在40CA8F处调用40CC90,再将[42DCA8]恢复为0。这似乎是软件作者的保护措施,但实际上这个机制不起作
用,在验证的时候[42DCA8]将总为0,有关此,将在文章最后讨论。
下面进入验证函数40CA50进行分析。
----------------------------------------------------------------------------------------------------------------------------
函数40CC50中:
0040CA50 sub esp,18
0040CA53 push esi
0040CA54 mov esi,dword ptr ss:[esp+20]
0040CA58 test esi,esi
0040CA5A je short PopChar.0040CA69
0040CA5C push esi
0040CA5D call PopChar.0040EC40 查长度
0040CA62 add esp,4
0040CA65 test eax,eax
0040CA67 jnz short PopChar.0040CA70
0040CA69 xor eax,eax
0040CA6B pop esi
0040CA6C add esp,18
0040CA6F retn
0040CA70 call PopChar.0040CFF0 启动一个线程,有意思的一个地方,涉及时间锁,最后讨论
0040CA75 lea eax,dword ptr ss:[esp+20]
0040CA79 push eax
0040CA7A lea ecx,dword ptr ss:[esp+C]
0040CA7E push ecx
0040CA7F lea edx,dword ptr ss:[esp+C]
0040CA83 push edx
0040CA84 push esi
0040CA85 call PopChar.0040CE20 验证函数
0040CA8A add esp,10
0040CA8D mov esi,eax
0040CA8F call PopChar.0040CC90 涉及时间锁,后面讨论
0040CA94 test esi,esi
0040CA96 jnz short PopChar.0040CAA2 如注册码错误则不跳,正确跳,继续验证与时间有关的信息
0040CA98 mov eax,1
0040CA9D pop esi
0040CA9E add esp,18
0040CAA1 retn
0040CAA2 lea eax,dword ptr ss:[esp+C]
0040CAA6 push eax ; /pLocaltime 获得时间,注册码中的信息与时间有关,
0040CAA7 call dword ptr ds:[<&KERNEL32.GetLoca>; \GetLocalTime 40CA85调40CE20验证完还要在下面进行验证
0040CAAD movzx eax,word ptr ss:[esp+C] 取出时间的年数
0040CAB2 movzx edx,word ptr ss:[esp+E] 取出时间的月数
0040CAB7 lea ecx,dword ptr ds:[eax+eax*2] 这句和下句等同于 年数*0xC+月数
0040CABA lea eax,dword ptr ds:[edx+ecx*4-19]
0040CABE mov ecx,dword ptr ss:[esp+20] 40CE20中计算的t3*0xc+t2存在这里,取出
0040CAC2 cmp ecx,eax 比较,等同于要求t3和t2代表的年月不能
0040CAC4 jl short PopChar.0040CADB 大于当前时间,否则错误
0040CAC6 mov eax,dword ptr ds:[42BC00] 为0x7D5
0040CACB lea edx,dword ptr ds:[eax+eax*2]
0040CACE mov eax,dword ptr ds:[42BC04] 为0x2
0040CAD3 lea edx,dword ptr ds:[eax+edx*4+23] 如上面的方法计算,后比较等同于要求
0040CAD7 cmp ecx,edx t3和t2代表的年月不能大于这个年月
0040CAD9 jle short PopChar.0040CAE5 [42BC00],[42BC04]这两个地址里的量不是
0040CADB mov eax,8 程序动态写入的,可能是安装程序写入的
0040CAE0 pop esi 代表程序安装的时间,我的是2005年2月
0040CAE1 add esp,18
0040CAE4 retn
0040CAE5 mov eax,dword ptr ss:[esp+24]
0040CAE9 test eax,eax
0040CAEB je short PopChar.0040CAF3
0040CAED mov edx,dword ptr ss:[esp+4]
0040CAF1 mov dword ptr ds:[eax],edx
0040CAF3 mov eax,dword ptr ss:[esp+28]
0040CAF7 test eax,eax
0040CAF9 je short PopChar.0040CB01
0040CAFB mov edx,dword ptr ss:[esp+8]
0040CAFF mov dword ptr ds:[eax],edx
0040CB01 mov eax,dword ptr ss:[esp+2C]
0040CB05 test eax,eax
0040CB07 je short PopChar.0040CB0B
0040CB09 mov dword ptr ds:[eax],ecx
0040CB0B mov eax,7
0040CB10 pop esi
0040CB11 add esp,18
0040CB14 retn
----------------------------------------------------------------------------------------------------------------------------
在这一层分析后发现:
1. 40CA85处调40CE20先进行验证,通过后40CA96处跳40CAA2,继续验证与时间有关的信息,时间信息正确后40CAD9处跳40CAE5然后走到
40CB0B,给eax送值7然后返回,上一层将这个值送[42DC94]中。如不正确将在40CA98处eax送值1后返回,或在40CADB处eax送值8后返回。
2. 40CA85处调用40CE20验证后,如正确在40CA96处将跳40CAA2,后面还有和时间相关的验证。
下面进入验证函数40CE20进行分析。
----------------------------------------------------------------------------------------------------------------------------
函数40CE20中:
0040CE20 push ecx
0040CE21 xor eax,eax
0040CE23 mov dword ptr ds:[42EA2C],eax
0040CE28 mov dword ptr ds:[42EA28],eax
0040CE2D mov eax,dword ptr ss:[esp+8]
0040CE31 mov dword ptr ds:[42DC9C],eax
0040CE36 call PopChar.0040CD20
0040CE3B call PopChar.0040CD90
--------------------------------------------------------------------
函数40CD20和函数40CD90两个一组调用,实际上是一个循环,为看得清楚,将这两个函数
40CD20的代码插在这里,并加以说明。
0040CD20 mov ecx,dword ptr ds:[42DC9C]
0040CD26 mov al,byte ptr ds:[ecx]
0040CD28 test al,al
0040CD2A mov byte ptr ds:[42DC98],al
0040CD2F jle short PopChar.0040CD52
0040CD31 cmp al,20 (0x20,是空格)
0040CD33 jle short PopChar.0040CD3D
0040CD35 cmp al,22 (0x22,是英文双引号)
0040CD37 je short PopChar.0040CD3D
0040CD39 cmp al,27 (0x27,是英文单引号)
0040CD3B jnz short PopChar.0040CD45
0040CD3D mov al,byte ptr ds:[ecx+1]
0040CD40 inc ecx
0040CD41 test al,al
0040CD43 jg short PopChar.0040CD31
0040CD45 mov byte ptr ds:[42DC98],al
0040CD4A mov dword ptr ds:[42DC9C],ecx
0040CD50 test al,al
0040CD52 je short PopChar.0040CD5B
0040CD54 inc ecx
0040CD55 mov dword ptr ds:[42DC9C],ecx
0040CD5B retn
函数40CD20从字串中取一个字符(不能是空格、单引号和双引号),放到AL中返
回注册码字串地址在[42DC9C],这个函数取一个字符后,同时将[42DC9C]中的字
串地址加1。
0040CD90 mov dl,byte ptr ds:[42DC98]
0040CD96 push esi
0040CD97 push edi
0040CD98 xor edi,edi edi初始化为0,作为累加器
0040CD9A call PopChar.0040CD60 查dl传入的字符在字串表的位置,eax返回位置
0040CD9F mov esi,eax 如果字符不在表中,返回0
0040CDA1 test esi,esi
0040CDA3 je short PopChar.0040CDCC
0040CDA5 mov eax,dword ptr ds:[42BB24]
0040CDAA push eax
0040CDAB call PopChar.0040EC40 查表字串表的长度,是0x1A,就是26
0040CDB0 imul eax,edi eax(总是0x1A)乘以edi
0040CDB3 add eax,esi 加上字符在字串表的位置
0040CDB5 add esp,4
0040CDB8 mov edi,eax 存入累加器edi
0040CDBA call PopChar.0040CD20 取字符
0040CDBF mov dl,al
0040CDC1 call PopChar.0040CD60 查位置
0040CDC6 mov esi,eax
0040CDC8 test esi,esi
0040CDCA jnz short PopChar.0040CDA5 循环,如果字符不在表中,退出循环
0040CDCC mov eax,edi
0040CDCE pop edi
0040CDCF pop esi
0040CDD0 retn
字串表在前面40CC5B处初始化的时候放入,字串表是“FORUSEBYMACDINGQWTZLKJHPXV”,
26个大字母乱序排列的。
这两个函数连在一起可以如下描述:
char sn[]; //注册码
int j=0; //累加器,循环结束后,j就是结果
int temp=0;
for(int i=0;;i++)
{
temp=40CD60(sn[i]); //调用40CD60查字符在字串表的位置,如字符不在表中,返回0
if(temp==0)
break;
j=0x1A*j+temp;
}
--------------------------------------------------------------------
0040CE40 mov dword ptr ss:[esp+8],eax 调用后将结果存入临时变量
0040CE44 cmp byte ptr ds:[42DC98],2D 查上面调用后第一个不在码表中的字符是否是“-”,
0040CE4B je short PopChar.0040CE51 否则不跳,码错
0040CE4D xor eax,eax 分析到此可以初步认定注册码前面应是“ABCD-”的形式
0040CE4F pop ecx “-”前的必须是大写字母,几个没有限制
0040CE50 retn
0040CE51 push ebx
0040CE52 push ebp
0040CE53 push esi
0040CE54 push edi
0040CE55 call PopChar.0040CD20 又调用这两个函数,进行循环
0040CE5A call PopChar.0040CD90 结果存入临时变量,可以认定注册码是前面是“ABCD-ABCD”
0040CE5F mov dword ptr ss:[esp+10],eax 的形式
--------------------------------------------------------------------
下面连续调用40CDE0五次,40CDE0的代码插在这里,并加以说明。
0040CDE0 mov al,byte ptr ds:[42DC98]
0040CDE5 xor edx,edx edx是累加器,初始化为0x0
0040CDE7 cmp al,2D 查字符是否是“-”,否则码错,返回
0040CDE9 je short PopChar.0040CDEE
0040CDEB xor eax,eax
0040CDED retn
0040CDEE call PopChar.0040CD20 调用40CD20取一个字符
0040CDF3 mov al,byte ptr ds:[42DC98]
0040CDF8 cmp al,30 检查字符的ASCII值是否小于0x30
0040CDFA jl short PopChar.0040CE1C
0040CDFC lea esp,dword ptr ss:[esp]
0040CE00 cmp al,39 检查字符的ASCII值是否大于0x39
0040CE02 jg short PopChar.0040CE1C
0040CE04 lea ecx,dword ptr ds:[edx+edx*4]
0040CE07 movsx edx,al
0040CE0A lea edx,dword ptr ds:[edx+ecx*2-30]
0040CE0E call PopChar.0040CD20
0040CE13 mov al,byte ptr ds:[42DC98]
0040CE18 cmp al,30 检查字符的ASCII值是否大于0x39
0040CE1A jge short PopChar.0040CE00
0040CE1C mov eax,edx
0040CE1E retn
40CDE0这个函数负责取字符,查是“-”后开始循环取字符,检查字符是否
是数字字符,转换为数字进行运算和累加,当字符不是数字时,循环结束。
循环计算实际上就是将连续的数字字符(如“-1234-”)转换为数字1234。
--------------------------------------------------------------------
0040CE63 call PopChar.0040CDE0 第一次调用40CDE0,检查是否“-”开始,
0040CE68 mov ebp,eax 并取连续的数字字符变为数字,存入临时变量
0040CE6A call PopChar.0040CDE0 第二次调用40CDE0
0040CE6F mov esi,eax
0040CE71 call PopChar.0040CDE0 第三次调用40CDE0
0040CE76 mov ebx,dword ptr ss:[esp+1C]
0040CE7A mov dword ptr ds:[ebx],eax
0040CE7C call PopChar.0040CDE0 第四次调用40CDE0
0040CE81 mov edi,eax
0040CE83 call PopChar.0040CDE0 第五次调用40CDE0
0040CE88 mov ecx,dword ptr ss:[esp+20]
0040CE8C mov dword ptr ds:[ecx],eax
至此,可以认定码为“ABCD-EFGH-1234-5678-9012-3456-7890”的形式,为下面叙述和描述算法方便,以下我将注册码中“-”隔开的小码转换
后的结果按顺序称为sn1,sn2,sn3,sn4,sn5,sn6,sn7,同时定义四个代表中间结果的变量t1,t2,t3,t4,t5。
0040CE8E mov ecx,dword ptr ds:[ebx]
0040CE90 test ecx,ecx sn5是否为空
0040CE92 je PopChar.0040CFC5
0040CE98 test esi,esi sn4是否为空
0040CE9A je PopChar.0040CFC5
0040CEA0 test edi,edi sn6是否为空
0040CEA2 je PopChar.0040CFC5
0040CEA8 test eax,eax sn7是否为空
0040CEAA je PopChar.0040CFC5
0040CEB0 mov al,byte ptr ds:[42DC98] 查整个注册码字串后面是否还有字符
0040CEB5 test al,al
0040CEB7 jnz PopChar.0040CFC5
0040CEBD mov eax,edi
0040CEBF cdq
0040CEC0 idiv dword ptr ds:[42DCA4] sn6除以0x40,[42DCA4]在开始初始化是被写入常数0x40
0040CEC6 mov eax,dword ptr ds:[42DCA0] [42DCA0]初始化时写入42BA20,是一个码表
0040CECB sub ecx,dword ptr ds:[eax+edx*4] sn5减去码表中取的数 :sn5-码表[(sn6 mod 0x40)*0x4]
0040CECE mov eax,dword ptr ds:[42DCA8] 变量,与时间锁有关,可以认为总为0x0
0040CED3 mov edx,dword ptr ds:[42BB20] [42BB20]在开始初始化时被写入常数0x5B31
0040CED9 sub ecx,eax 减去0x0
0040CEDB sub ecx,edx 减去0x5B31,
0040CEDD mov eax,ebp
0040CEDF mov dword ptr ds:[ebx],ecx 临时结果存入变量,我用t1表示这个中间结果
0040CEE1 cdq
0040CEE2 mov ebx,2710
0040CEE7 idiv ebx eax是sn3,就是sn3除以0x2710
0040CEE9 test ecx,ecx
0040CEEB mov ebp,edx 余数送ebp,我用t3表示这个中间结果
0040CEED mov ebx,eax 商送ebx,我用t2表示这个中间结果
0040CEEF jle PopChar.0040CFC5 检测ecx不能为0,也就是t1不能为0
0040CEF5 cmp ebp,7D2 要求余数>=0x7D2
0040CEFB jl PopChar.0040CFC5
0040CF01 cmp ebp,802 要求余数<=0x802
0040CF07 jg PopChar.0040CFC5
0040CF0D test ebx,ebx
0040CF0F jle PopChar.0040CFC5 要求商<0xD
0040CF15 cmp ebx,0D
0040CF18 jge PopChar.0040CFC5 以上几个比较0x7D2是2002,0x802是2050,0xD是13
0040CF1E mov edx,dword ptr ss:[esp+20] 可以推测认定,sn3中的信息为年和月
0040CF22 mov eax,dword ptr ds:[edx] 取sn7送eax,并检测不能为0
0040CF24 test eax,eax
0040CF26 jle PopChar.0040CFC5
0040CF2C cmp eax,esi si中是sn4,这里比较,要求sn7<=sn4
0040CF2E jg PopChar.0040CFC5
0040CF34 mov edx,dword ptr ss:[esp+18] 取sn1送eax
0040CF38 lea eax,dword ptr ds:[ecx+ecx*2] 这句和下句加起来等同于eax=eax*0xC+t2(这时ebx中是
0040CF3B lea eax,dword ptr ds:[ebx+eax*4] 上面40CEE7除运算的商)
0040CF3E imul eax,eax,7 上面计算的结果乘以0x7
0040CF41 add eax,esi 上面计算的结果加上sn4
0040CF43 lea edx,dword ptr ds:[edx+eax*2] 这句和下句加起来等同于eax=eax*2+sn3(这时edx中是
0040CF46 add eax,edx sn3)
0040CF48 mov edx,dword ptr ss:[esp+10] sn2送edx
0040CF4C lea eax,dword ptr ss:[ebp+eax*2-7A5] eax=t3+eax*2-0x7A5,(ebp中是上面40CEE7除运算的余数)
0040CF53 lea edx,dword ptr ds:[edx+eax*4] 这句和下句加起来等同于eax=eax*5+sn2
0040CF56 add eax,edx
0040CF58 cdq
0040CF59 mov esi,0D59D3
0040CF5E idiv esi 上面计算的结果除以0xD59D3
0040CF60 mov esi,edx 余数送esi
0040CF62 add esi,2D187 余数加上0x2d187
0040CF68 mov eax,esi 结果送eax,这个中间结果我用t4表示
0040CF6A cdq
0040CF6B idiv dword ptr ds:[42DCA4] t4除以0x40,[42DCA4]在开始初始化是被写入常数0x40
0040CF71 mov eax,dword ptr ds:[42DCA0] 码表开始,[42DCA0]初始化时写入的42BA20
0040CF76 mov edx,dword ptr ds:[eax+edx*4] 用t4除以0x40的余数在码表中取数: 码表[(t4 mod 0x40)*0x4]
0040CF79 add edx,dword ptr ds:[42DCA8] 加上变量,与时间锁有关,可以认为总为0x0
0040CF7F add edx,esi 加上t4
0040CF81 cmp edx,edi 比较结果(我用t5表示)是否等于sn6,此时edi中是sn6
0040CF83 jnz short PopChar.0040CFC5 如果不等,注册码错误,跳走
0040CF85 push ecx t1入栈传给函数
0040CF86 call dword ptr ds:[42BB2C] ; PopChar.0040C9F0
0040CF8C mov esi,eax
0040CF8E mov eax,dword ptr ss:[esp+1C]
0040CF92 push eax sn1入栈传给函数
0040CF93 call dword ptr ds:[42BB30] ; PopChar.0040CA40
0040CF99 add esp,8
0040CF9C cmp esi,eax
0040CF9E jge short PopChar.0040CFC5
--------------------------------------------------------------------
40CF86处调用的40C9F0和40CF93处调用的40CA40的很简单,在这里说明。
40C9F0中判断传入的t1,如果(t1<=1)or(t1==0x4ED1)or(t1==0x5609)or(t1==0x5bF7)
为真,则eax返回1,否则返回0。
40CA40中判断传入sn1,如果(sn1==0x408F)为真,则eax返回1,否则返回0。
和调用后的比较合起来,等于要求t1必须大于1并且不能等于0x4ED1、0x5609和0x5bF7,
同时,sn1必须等于0x408F,否则错误。
--------------------------------------------------------------------
0040CFA0 mov eax,dword ptr ss:[esp+24]
0040CFA4 lea ecx,dword ptr ss:[ebp+ebp*2] 这句加上40CFA9这句,等同于t3*0xC+t2
0040CFA8 pop edi
0040CFA9 lea edx,dword ptr ds:[ebx+ecx*4-1]
0040CFAD mov dword ptr ds:[eax],edx 结果放入堆栈,这层出去后,还有验证
0040CFAF pop esi
0040CFB0 mov dword ptr ds:[42EA2C],ebp t3存入变量,这层出去后,还有验证
0040CFB6 pop ebp
0040CFB7 mov dword ptr ds:[42EA28],ebx t2存入变量,这层出去后,还有验证
0040CFBD mov eax,1 eax送1,代表本层函数验证正确
0040CFC2 pop ebx
0040CFC3 pop ecx
0040CFC4 retn
0040CFC5 pop edi 前面验证一旦错误就会跳到这里
0040CFC6 pop esi
0040CFC7 pop ebp
0040CFC8 xor eax,eax eax送0,代表本层函数验证错误
0040CFCA pop ebx
0040CFCB pop ecx
0040CFCC retn
----------------------------------------------------------------------------------------------------------------------------
总结一下验证算法:
1. 注册码应该是“ABCD-EFGH-1234-5678-9012-3456-7890”的形式,前两串必须是大写字母,后边五串是数字字符串,否则错。
2. 将注册码“-”隔开的七串分别转换为数字。
前两串大写字母串调用如下的算法变成数字:
字串表是“FORUSEBYMACDINGQWTZLKJHPXV”
char sn[]; //注册码
int j=0; //累加器,循环结束后,j就是结果
int temp=0;
for(int i=0;;i++)
{
temp=40CD60(sn[i]); //调用40CD60查字符在字串表的位置,如字符不在表中,返回0
if(temp==0)
break;
j=0x1A*j+temp;
}
后五串数字字符串,转换为相应的数字。
注册码转换得出的7个数字用sn1,sn2,sn3,sn4,sn5,sn6,sn7表示。
3. 算法:
码表
unsigned int table[64]={
000054EA 0000AE16 00005BA6 0000650A
00006D1E 000163F7 00011B80 000161E6
00016C5B 00012F07 0000F7B3 0000E844
0000A35A 000074EF 00001B92 000150B7
0000D9AA 00007140 0000FF6D 00017AAA
00008EE1 0000D1C4 0000A971 00008F78
0001685D 00002C25 00006078 0000F13E
0001224E 0000FE81 00007BAD 0000696C
0001467A 0000085A 00004B5A 00001A5A
00011830 0000A1E1 00000B06 00004955
00004DD4 0000FA05 00007525 000060F1
0001550E 000069E9 00014D45 0000589D
0000D6EF 00011E7F 000127C1 00002262
00005B86 0001611A 0000DF49 00001805
000088F9 000093E4 0000235A 00015823
00015894 0001486C 00012BFF 0000355B
}
unsigned int t1,t2,t3,t4,t5 //中间变量
t1 = sn5-table[(sn6 mod 0x40)*4]-0x5B31;
t2 = sn3/0x2710;
t3 = sn3 mod 0x2710;
t4 = (((((t1*0xc)+t2)*7+sn4)*3+sn1)*2+t3-0x7A5)*5+sn2) mod 0xD59D3+0x2D187;
t5 = table[(t4 mod 0x40)*4]+t4;
如果t5==sn6,验证通过。
同时要求:
1. t2<0xD
2. t3>=0x7D2
3. t3<=0x802
4. sn7<=sn4
5. (t1>1)and(t1!=0x4ED1)and(t1!=0x5609)and(t1!=0x5BF7)
6. sn1==0x408F
7. t3、t2、所表示的年和月要小于软件安装时间的年和月
以及当前时间的年和月
算法就是这样,有7个变量,分析发现:
1. sn1必须等于0x408F,通过猜测验证,发现软件的内部名称“PCW”按它取字母转换算法得出的就是0x408F,
因此确定注册码第一小串是“PCW”。
2. sn2可以是任何大写字母串,但不能太长(有溢出的问题),我用的是我们小组织的名字“PCG”。
3. sn3分析后可以知道,sn3/0x2710(十进制10000)后,余数代表年,商代表月,且这个年和月不能大于软件安装的
年月且不能大于当前时间的年月,我直接用12004,既2004年1月。
4. sn4可以是任何整数(也要考虑溢出的问题,不能太大),参考sn3,我用12005。
5. sn7不参与运算,只要小于等于sn4就可以了,我用12002。
6. 这样就只剩sn5和sn6两个变量,也就是两个变量的方程了。
按以上分析,我计算出的一个注册码是:PCW-PCG-12004-12005-109424-621821-12002
我数学不太好,以上算法,我觉得应该有无限多个注册码(如不考虑溢出的问题),还望高手指点。
本程序的注册码与用户名、硬件码无关,有一个注册码就可以了。
----------------------------------------------------------------------------------------------------------------------------
有关程序中时间锁的讨论:
追踪过程中发现软件作者使用了这样一个机制进行保护:
1. 在执行验证函数前调用40CFF0,代码如下:
0040CFF0 sub esp,8
0040CFF3 lea eax,dword ptr ss:[esp+4]
0040CFF7 push eax
0040CFF8 push 0
0040CFFA lea ecx,dword ptr ss:[esp+8]
0040CFFE push ecx
0040CFFF push PopChar.0040CFD0
0040D004 push 0
0040D006 push 0
0040D008 mov dword ptr ds:[42DCA8],0 [42DCA8]中置0
0040D012 mov dword ptr ss:[esp+18],1
0040D01A call dword ptr ds:[<&KERNEL32.CreateT> 启动线程
0040D020 test eax,eax
0040D022 jnz short PopChar.0040D02C
0040D024 add esp,8
0040D027 jmp PopChar.0040CC80
0040D02C push eax
0040D02D call dword ptr ds:[<&KERNEL32.CloseHa>
0040D033 add esp,8
0040D036 retn
这个函数先将[42DCA8]置0,然后启动线程(执行40CFD0),再结束。
40CFD0处的代码:
0040CFD0 push 7D0 ; /Timeout = 2000. ms
0040CFD5 call dword ptr ds:[<&KERNEL32.Sleep>] ; \Sleep
0040CFDB call PopChar.0040CC80 这里很简单,就是将[42DCA8]写入3
0040CFE0 xor eax,eax
0040CFE2 retn 4
2. 执行验证函数,验证过程中使用[42DCA8]中的值参与计算。
3. 验证函数执行后,在40CA8F处调用40CC90,再将[42DCA8]恢复为0。
我想作者是这样考虑的:如果验证函数被单步中断跟踪调试,将至少超过2000ms,这样,另一线程将[42DCA8]写入3,将导致计算变化,和程序自己执行时[42DCA8]中为0不一样,以此进行保护。
但我认为这种方法是无效的,因为调试器在单步追踪的情况下会将被调试程序的其它线程都挂起的(在od中是这样,在softice中,所有其它线程都被挂起的,其它调试器我不是很清楚),因此在单步追踪验证函数的时候,这个修改[42DCA8]的线程不会被执行,验证函数执行过程中,[42DCA8]中总是等于0,同程序自己执行时一样。
这是我对此的分析,还望各位高手大侠给予指点。
----------------------------------------------------------------------------------------------------------------------------
【版权声明】 本文纯属技术交流, 转载请注明作者并保持文章的完整, 谢谢!
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)