开场
嘿耶耶耶~~~~ c↗s→d↗n→的朋↗↘友
非常~非常~期待 我们很快就要见~面~~~
序列号保护机制
序列号保护机制在软件中很常见,一些软件如果没有经过验证会有使用时间和使用功能的限制,需要用户输入个人信息,或者付费解锁。例如使用正版的mathtype需要到官网购买序列号,不然只能使用一个月,而且还不能使用全部的功能。
破解serial.exe的详细步骤
先打开序列号验证程序,观察程序验证流程
随便输入用户名和序列号后,出现了错误提示Incorrect!Try Again
打开od,右键->查找->所有参考文本字串
双击Incorrect!,Try Again
,跳转到对应的代码处。
这段代码用于弹出失败窗口,那么就向上翻阅,查找关键代码
GetDlgItemText
这个API用于获取用户输入的字符串,这个函数被调用了两次,获取的内容分别放在0040218E
和0040217E
中,可以推测存放的是用户名以及序列号。注意:这里的count对输入字符的最大个数有规定
接着向上查找,很容易找到序列号验证算法
00401228 . 68 8E214000 push serial.0040218E ; ASCII "sorry"
0040122D . E8 4C010000 call serial.0040137E
00401232 . 50 push eax
00401233 . 68 7E214000 push serial.0040217E ; ASCII "penny"
00401238 . E8 9B010000 call serial.004013D8
0040123D . 83C4 04 add esp,0x4
00401240 . 58 pop eax ; serial.00401232
00401241 . 3BC3 cmp eax,ebx ; serial.WndProc
00401243 . 74 07 je short serial.0040124C
00401245 . E8 18010000 call serial.00401362
0040124A .^ EB 9A jmp short serial.004011E6
0040124C > E8 FC000000 call serial.0040134D
00401251 .^ EB 93 jmp short serial.004011E6
这段代码将40218E
和40217E
处的内容弹如栈,基本可以确定是验证算法了
该处代码先将40137E
的返回值k1
放入寄存器eax
,利用寄存器将k1
作为004013D8
处函数的参数,这个函数应该会将结果放入ebx
,这里将结果记为k2
。然后就是比较k1
和k2
的值(eax 和ebx的值),一样的话就成功验证了:)
按下f2
打下断点,运行程序
点击OK,程序运行到了断点处,如下图
进入到40137E
函数中
看第一段代码mov esi,dword ptr ss::[esp + 0x4]
,首先要明白[esp+0x4]
是个什么东西。看右侧的寄存器窗口,esp的值为0019FDB0
。观察右下角的窗口,可以观察到0019FDB4
对应我们输入的用户名
这段验证代码可以分成几个部分来看
第一部分:验证输入的字符是不是大写字母(大写字母的ASCII编码对应十六进制为0x41–0x5A)
0040137E /$ 8B7424 04 mov esi,dword ptr ss:[esp+0x4] ; serial.0040218E
00401382 |. 56 push esi
00401383 |> 8A06 /mov al,byte ptr ds:[esi]
00401385 |. 84C0 |test al,al
00401387 |. 74 13 |je short serial.0040139C
00401389 |. 3C 41 |cmp al,0x41
0040138B |. 72 1F |jb short serial.004013AC
0040138D |. 3C 5A |cmp al,0x5A //如果是小写字母,要特殊处理,这里没有专门分析
0040138F |. 73 03 |jnb short serial.00401394
00401391 |. 46 |inc esi
00401392 |.^ EB EF |jmp short serial.00401383
00401394 |> E8 39000000 |call serial.004013D2
00401399 |. 46 |inc esi
0040139A |.^ EB E7 \jmp short serial.00401383
0040139C |> 5E pop esi ; serial.00401232
0040139D |. E8 20000000 call serial.004013C2
分析一下这串代码:
- 使用
mov al,byte ptr ds:[esi]
存下esi
寄存器的第一个字符,为什么是一个字符呢?因为al是一个8位的通用寄存器,而扩展的ASCII码用8个二进制位表示字符。 - 因为
al
指向的是esi
寄存器的第一个字符,所以当esi
没有内容时,test al al
会将零标志位设为1,然后程序跳转到0040139C
,结束比较 inc esi
的作用是去掉esi
的第一个字符(可以看OD右侧的寄存器窗口验证)。在这个过程中,esi
寄存器的值是一直变化的。
接下来分析调用的004013C2
的函数
004013C2 /$ 33FF xor edi,edi
004013C4 |. 33DB xor ebx,ebx
004013C6 |> /8A1E /mov bl,byte ptr ds:[esi]
004013C8 |. |84DB |test bl,bl
004013CA |. |74 05 |je short serial.004013D1
004013CC |. |03FB |add edi,ebx
004013CE |. |46 |inc esi ; serial.0040218F
004013CF |.^\EB F5 \jmp short serial.004013C6
004013D1 \> C3 retn
这段代码把用户名每一个字符对应的十六进制ASCII编码加到edi
上
然后将这个数异或0x5678
,结果放在eax
寄存器中。
接下来看一看处理序列号的函数
和上面的处理差不多:)
这里要注意顺序:imul的操作对象是edi,sub的操作对象是bl
使用书中的keygen
完成解密
int keygen(char *name)
{
int i,k1=0,k2=0;
char ch;
for(i=0;name[i]!=0&&i<=9;i++)
{
ch=name[i];
if(ch<'A') break;
k1+=(ch>'Z')?(ch-32):ch; //异或运算是可逆的
}
k2=k1^0x5678^0x1234;//这里的k2是以十进制形式表示的,直接将其输出,就得到了序列号
return k2;
}
休息一下
↓ 班级里那位最喜欢抢风头的人 b e l i k e : ~~~~~~~~~~~~~~↓班级里那位最喜欢抢风头的人be~like: ↓班级里那位最喜欢抢风头的人be like:
研究春山学,不比研究ctf有趣?
休息结束,还是得继续看逆向:(
去掉警告窗口
先尝试运行程序吧
这次的目标是要去掉弹出的窗口
目前我们遇到了两个能够弹出窗口的API。一个是
MessageBox
,另外一个是这次遇到的DialogBoxParam
API文档是这样介绍
DialogBoxParam
的第二个参数IpTemplateName
的:该字符串指定对话框模扳名,或是指定对话框模板的资源标识符的一个整型值。
由于这个程序是使用资源来显示对话框的,可以使用resource hacker
观察这个对话框。0x79的十进制数为121
但我们需要修改代码,并不仅仅满足于观察对话框。可以使用OD修改代码。思路就是使用jmp
跳过弹出窗口的函数。由于DialogBoxParam
是和EndDialog
配套出现的,所以我们要把整个过程都跳过。
但是这里出现了两个EndDialog
函数,为了确认哪一个是关闭这个窗口的,可以尝试调试代码看看
我分别在004010A2
处和004010D9
处下断点,接着运行程序,点击弹出窗口多的ok
按钮,程序断在D9处
那么将“00401051 push 00000000”改成“00401051 jmp 4010E5"。
右键->汇编,输入jmp 004010E5
然后保存流程如下
这个窗口就不会弹出来啦:)
拆除时间限制
先运行程序看看
看起来程序只能运行20秒,过了就会自动关闭
这个函数的参数Timeproc
为NULL,说明超时了会向对应窗口发送WM_TIMER
信息。
那只要跳过这个函数的调用就行了:)
更改如上,这个软件的时间限制就被我们去掉了。
keyFile保护
这竟然是上个世纪的软件,长见识了
首先还是运行软件看看
这个对话框里面的内容没办法更改,点击Check
按钮也没有反应
打开od看看,可以根据下方对话框的名称确定弹出窗口的API
这次弹出窗口的API是CreatWindowEx
,文档说这是一个用来弹出窗口的函数。打了断点验证,确实如此,后面的ShowWindow
等函数用来确定窗口的样式。
有一个问题。。我按下f8
单步步过调试程序,这个程序一直在40129A
和401275
之间跳转,我不知道怎么确定4016D9
的CreatFileA
就是关键代码
那么就按照书中的操作来吧:在4016D9
打下断点,接着运行程序,点击Check
按钮,之后OD果然在这里停住了
cmp eax -0x1
是什么意思呢?在msdn里查找CreatFile
的文档
INVALID_HANDLE_VALUE
是一个宏定义,值为-1。
如果我们没有创建KwazyWeb.bit
文件,程序会跳转到401747
处,验证失败。
用WinHex
创建一个十六进制文件,将其保存到程序目录下,尽量写的有规律一点,这样用比较容易通过代码找到规律
我已经在4016D9
打下断点了,接下来尝试分析程序
CreatFile
函数运行成功后,返回KwazyWeb.bit
的可执行句柄,放在内存403444
处(这个地址可能会变),值为5B4。
中间的ReadFile
函数有参数hFile=005B4
,表示读取这个文件的内容。这段代码将KwazyWeb.bit
的第一个字节放在Buffer
处,并使用test语句判断是否为0。
所以这段代码用于判断文件是否为空,如果为空就跳转 (两个十六进制数表示一个字节)
以此类推,这次的Buffer缓冲区存放从第二个开始,共eax
个字节(这里是02 03 04 05)(因为我重启了od,所以存放数据的位置改变了,不过函数功能和位置不会改变)
分析401000
的函数
00401006 |. 8A0D FA344000 mov cl,byte ptr ds:[0x4034FA] //存放文件的第一个字节
0040100C |. BE 88324000 mov esi,PacMe.00403288 //文件第二---eax-1字节
00401011 |> AC /lods byte ptr ds:[esi]
00401012 |. 03D0 |add edx,eax
00401014 |.^ E2 FB \loopd short PacMe.00401011
00401016 |. 8815 FB344000 mov byte ptr ds:[0x4034FB],dl
0040101C \. C3 retn
从KwazyWeb.bit
文件的第二个字节开始读取数据并求和,数量为第一个字节的值,然后把它放到4034FB
中
看下一个ReadFile
函数
这个函数读取接下来的18个十六进制字符到内存004034E8
中。然后调用关键函数004010C9
,分析一下这个函数
004010C9 /$ 55 push ebp
004010CA |. 8BEC mov ebp,esp
004010CC |. 83C4 FC add esp,-0x4
004010CF |. 68 65334000 push PacMe.00403365 ; /String2 = "****************C*......*...****.*.****...*....*.*..**********.*..*....*...*...**.****.*.*...****.*....*.*******..*.***..*.....*.*..***.**.***.*...****....*X..*****************"
004010D4 |. 68 BC314000 push PacMe.004031BC ; |String1 = PacMe.004031BC
004010D9 |. E8 3A070000 call <jmp.&KERNEL32.lstrcpyA> ; \lstrcpyA
004010DE |. C705 84314000>mov dword ptr ds:[0x403184],PacMe.004031>; ASCII "C*......*...****.*.****...*....*.*..**********.*..*....*...*...**.****.*.*...****.*....*.*******..*."
//403184存放字符C的地址004031CC
004010E8 |. E8 30FFFFFF call PacMe.0040101D //处理第三次读取的十八个字节
004010ED |. C645 FE 00 mov byte ptr ss:[ebp-0x2],0x0
004010F1 |. 33C0 xor eax,eax ; PacMe.004031BC
004010F3 |. 33C9 xor ecx,ecx
(存放字符C的地址)
接着分析40101D
函数吧
0040101D /$ 8A15 FB344000 mov dl,byte ptr ds:[0x4034FB]
00401023 |. B9 12000000 mov ecx,0x12
00401028 |. B8 E8344000 mov eax,PacMe.004034E8
0040102D |> 3010 /xor byte ptr ds:[eax],dl
0040102F |. 40 |inc eax ; PacMe.004031BC
00401030 |.^ E2 FB \loopd short PacMe.0040102D
00401032 \. C3 retn
这段代码就是简单的异或,将第三次读取的十八个字节异或上401000
函数中累加的值。
再分析一下函数返回后的代码
004010ED |. C645 FE 00 mov byte ptr ss:[ebp-0x2],0x0 //x=0
004010F1 |. 33C0 xor eax,eax ; PacMe.004034FA
004010F3 |. 33C9 xor ecx,ecx
004010F5 |> C645 FF 08 /mov byte ptr ss:[ebp-0x1],0x8 // len = 8
004010F9 |> 806D FF 02 |/sub byte ptr ss:[ebp-0x1],0x2 // len -=2
004010FD |. 0FB64D FE ||movzx ecx,byte ptr ss:[ebp-0x2] //ecx = x
00401101 |. 81C1 E8344000 ||add ecx,PacMe.004034E8 // ecx为004034E8后移x字节的值
00401107 |. 8A01 ||mov al,byte ptr ds:[ecx]
00401109 |. 8A4D FF ||mov cl,byte ptr ss:[ebp-0x1]
0040110C |. D2E8 ||shr al,cl // al>>cl
0040110E |. 24 03 ||and al,0x3 //al &&11 看起来是要处理eax的数据
00401110 |. E8 1EFFFFFF ||call PacMe.00401033
00401115 |. 85C0 ||test eax,eax ; PacMe.004034FA
00401117 |. 74 11 ||je short PacMe.0040112A // 退出循环
00401119 |. 0FB655 FF ||movzx edx,byte ptr ss:[ebp-0x1]
// tmp的初值为8,每一次都会-2,所以应该会循环4次,这是小循环
0040111D |. 85D2 ||test edx,edx ; PacMe.0040340E
0040111F |.^ 75 D8 |\jnz short PacMe.004010F9
00401121 |. FE45 FE |inc byte ptr ss:[ebp-0x2] // x++
00401124 |. 807D FE 12 |cmp byte ptr ss:[ebp-0x2],0x12 // 大循环要循环18次
00401128 |.^ 75 CB \jnz short PacMe.004010F5
0040112A |> C9 leave
0040112B \. C3 retn
这是401033
函数的代码
00401033 $ 55 push ebp
00401034 . 8BEC mov ebp,esp
00401036 . 83C4 F8 add esp,-0x8
00401039 . 8B15 84314000 mov edx,dword ptr ds:[0x403184] ; PacMe.004031CC
0040103F . 8955 FC mov dword ptr ss:[ebp-0x4],edx ; PacMe.0040340E
//字符`c`的地址保存在[ebp-0x4]中
// al =0
00401042 . 0AC0 or al,al ; Switch (cases 0..2)
00401044 . 75 09 jnz short PacMe.0040104F
//-16
00401046 . 832D 84314000>sub dword ptr ds:[0x403184],0x10 ; Case 0 of switch 00401042
0040104D . EB 1F jmp short PacMe.0040106E
0040104F > 3C 01 cmp al,0x1
00401051 . 75 08 jnz short PacMe.0040105B
//+1
00401053 . FF05 84314000 inc dword ptr ds:[0x403184] ; PacMe.004031CC; Case 1 of switch 00401042
00401059 . EB 13 jmp short PacMe.0040106E
0040105B > 3C 02 cmp al,0x2
0040105D . 75 09 jnz short PacMe.00401068
//+16
0040105F . 8305 84314000>add dword ptr ds:[0x403184],0x10 ; Case 2 of switch 00401042
00401066 . EB 06 jmp short PacMe.0040106E
//-1
00401068 > FF0D 84314000 dec dword ptr ds:[0x403184] ; PacMe.004031CC; Default case of switch 00401042
0040106E > 8B15 84314000 mov edx,dword ptr ds:[0x403184] ; PacMe.004031CC
00401074 . 8A02 mov al,byte ptr ds:[edx]
00401076 . 3C 2A cmp al,0x2A
00401078 . 75 06 jnz short PacMe.00401080
0040107A . 33C0 xor eax,eax
0040107C . C9 leave
0040107D . C3 retn
这段代码开始我们就能推测出程序加密原理了。[0x403184]
中存放字符C
的地址,那么地址+1 -1可以分别认为C
向右移动和向左移动。那么+16 -16就理所当然是向下和向上移动了。回头看之前出现过的奇怪字符串,如果把16个字符分为一行,结果就是这样:
****************
C*......*...****
.*.****...*....*
.*..**********.*
..*....*...*...*
*.****.*.*...***
*.*....*.*******
..*.***..*.....*
.*..***.**.***.*
...****....*X..*
****************
如果把这个看成一个迷宫,C当成玩家,盲猜.
表示可以走的路,X
表示终点。(使用瞪眼法秒了!)
路径如下:下下下右下下下左下下右右上右上上右右右上上左左左上左上上右右右右右下右右上右右下右右右下下左左下左左上左左下下下左下下右右右上上右右右右下下左左(一共是72步,正好是18*4)
第三次获取的数据有18个字节,每一个字节都确定接下来4步怎么走。第一个字节决定1至4步的走法,第二个字节决定5至8步的走法,以此类推。(18个字节都是被异或过的)
将异或之后的某一字节用x
表示,用(x>>6)&0x3
决定第一步的走法 ,结果为x的最高两位(7,8位)
用(x>>4)&0x3
决定第二步的走法 ,结果为x的第5,6两位,剩下的以此类推。每一步都可以对应一个两位的二进制数(转换成十进制范围为0--3
):0表示向上,1表示向右,2表示向下,3表示向左
将路径用四进制表示,结果如下:
变成十六进制,结果如下 A9 AB A5 10 54 3F 30 55 65 16 56 BE F3 EA E9 50 55 AF
写一个python代码得出结果
data = [0xA9, 0xAB, 0xA5, 0x10, 0x54, 0x3F, 0x30, 0x55, 0x65, 0x16, 0x56, 0xBE, 0xF3, 0xEA, 0xE9, 0x50, 0x55, 0xAF]
for i in data:
print(hex(i^0x1),end= " ")
成功验证:)
本文结束,网络验证就不看了,等之后学习了网络编程再说吧
接下来是与本文无关的一些废话:)
后记
好难。。真的好难。。导师说寒假要看《加密与解密》的,现在寒假快结束了,连一半都没看完。。。
照例的meme时间
发现马上要开学了,不能一觉睡到中午十二点的我be like↓
猫猫可爱,猫好
人发猫猫丑图,人坏!