160 个 CrackMe 之 056 Elraizer.2 解码代码破解、注册分析及注册机实现
本帖最后由 solly 于 2019-6-28 15:57 编辑160 个 CrackMe 之 056 Elraizer.2 是 Elraizer 第2个 CrackMe,第1个已破解,见我这个贴子:
https://www.52pojie.cn/thread-953152-1-1.html
这两个 CrackMe 都比较复杂,而且都会通过注册码来计算的结果来确定内存地址或汇编代码。如果注册码输入不正确,CrackMe是无法正常运行的。
这个 CrackMe 的反调试比较强,而且代码和相关字符串资源都是加密的,虽然没有加壳,但与120 - Cronos CrackMe 一样,需要在代码执行前动态解密,字符串在使用前也需要先解码。
更BT的是,其利用 WinMain() 的最后一个参数 nShowCmd 作为解密密钥,而 nShowCmd 参数,通过调试器打开时,在 Windows 9x 下,传入的值与双击直接打开时是不一样的,会导致解密实际上失败,但你还不一定看得出来,因为解密过程没有错,只是密钥有问题。
先还是看看文件信息吧,也没什么特别的:
首先説一下WinMain(),WinMain()函数是Microsoft的一个传统函数命名,它是提供给用户的Windows应用程序的入口点。它的函数声明如下:
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
);
关键在于 nShowCmd 参数,CrackMe 使用这个参数的值来解密大多数代码(还会与其它值进行运算后才当密钥使用),nShowCmd 有很多取值,下面有4个可能的值,常用也就前两种吧 :
SW_SHOWDEFAULT = 10
SW_SHOWNORMAL= 1
SW_HIDE = 0
SW_SHOW = 5
在 Win9x 下,双击打开时,传入是 SW_SHOWNORMAL,当用 OD 之类的调试器载入时,传递给 CrackMe 的是 SW_SHOWDEFAULT ,而在 WinNT 系列内核的系统,两种打开方式都是传入的 SW_SHOWDEFAULT ,而 CrackMe 需要传入的是 SW_SHOWNORMAL ,所以,在最新系统中都不能正常运行 CrackMe。为了解决这个问题,我们只有先在其启动函数 start() 中处理好这个参数问题,然后再调用 WinMain()函数。
如下图所示,在其 start() 部分,有一段这样的代码,正好用来处理 nShowCmd 的问题:
具体代码如下:
00401B76 8975 D0 mov dword ptr , esi ; 初始化 StartupInfo.dwFlags = 0
00401B79 8D45 A4 lea eax, dword ptr
00401B7C 50 push eax
00401B7D FF15 78914000 call dword ptr [<&KERNEL32.GetStartupInfoA>] ; KERNEL32.GetStartupInfoA
00401B83 E8 EF030000 call 00401F77 ; _wincmdln
00401B88 8945 9C mov dword ptr , eax
00401B8B F645 D0 01 test byte ptr , 1 ; StartupInfo.dwFlags = 0x81 = STARTF_FORCEOFFFEEDBACK(0x80) | STARTF_USESHOWWINDOW(0x01),如果这里是0x01,则表示是通过其它进程启动的,并传递了 nCmdShow 参数,因此后面会将nShowCmd改成错误值,导致WinMain()内部代码解码出错,程序无法继续执行。
00401B8F 74 06 je short 00401B97
00401B91 0FB745 D4 movzx eax, word ptr ; 检查 nShowCmd参数,StartupInfo.wShowWindow = 0x0A = SW_SHOWDEFAULT,正常启动应为 0x01 (SW_SHOWNORMAL);
00401B95 EB 03 jmp short 00401B9A ; 这里改成 2 个 nop,就可以进行调试了。
00401B97 6A 0A push 0A ; 如果检测到了是间接启动的,改成0x0A,正确值应是, 0x01,这里可改成 push 01,就可用进行调试了。
00401B99 58 pop eax
00401B9A 50 push eax ; nShowCmd,正确值应为 0x01
00401B9B FF75 9C push dword ptr ; lpCommandLine
00401B9E 56 push esi ; hPrevInstance
00401B9F 56 push esi
00401BA0 FF15 74914000 call dword ptr [<&KERNEL32.GetModuleHandleA>] ; KERNEL32.GetModuleHandleA
00401BA6 50 push eax ; hInstance
00401BA7 E8 D4FAFFFF call 00401680 ; WinMain() 程序主过程入口
00401BAC 8945 A0 mov dword ptr , eax ; eax = 0
其中有处理参数 nShowCmd 的代码,我们改动一下就可以了,如下图:
具体改动代码好下:00401B91|.0FB745 D4 movzx eax, word ptr
00401B95 90 nop
00401B96 90 nop
00401B97 6A 01 push 1 ;强行修改为 SW_SHOWNORMAL,即 0x01
00401B99|.58 pop eax
这样,不管什么方式启动 CrackMe,都会传递给 WinMain() 一个正确的 nShowCmd。另外,也激活了 OD 的 patch 功能,记录代码的变化,包括 CrackMe 自解密的代码变化,便于后面我们保存这些变化。
完成这一步后,我们就可以进行正常调试和跟踪了。
首先 F7 进入上面的 WinMain()函数,如下位置,需按 F7 进入:
00401BA6|.50 push eax
00401BA7|.E8 D4FAFFFF call 00401680 ;WinMain(),F7 进入该函数
上图位置 F7 进入 WinMain()。
进入WinMain()后如下图所示:
标为蓝色底的两行代码就是处理 nShowCmd 参数,就是从参数变量中取出来,存入到全局变量中,便于后面各处的解码过程使用这个数据,代码如下:
004016AD .8B45 14 mov eax, dword ptr ;取出 nShowCmd 参数
004016B0 .A3 20784000 mov dword ptr , eax ;保存 nShowCmd,留待后面继续使用
执行到这一行,我们可以看到 EAX = 0x00000001,即 SW_SHOWNORMAL,如下图所示:
接下来就是第一次代码解密操作,如下图蓝色底色的代码:
我们先看看解码函数,如下图:
具体代码如下,同时我们也可以看到,至少有6个地方会调用这个函数。
;////////////////////////////// 加/解密函数 /////////////////////////////////////////
004017E0/$8B4C24 04 mov ecx, dword ptr ;密文地址
004017E4|.8B4424 0C mov eax, dword ptr ;长度
004017E8|.56 push esi
004017E9|.33F6 xor esi, esi ;初始化较验和
004017EB|.8D1401 lea edx, dword ptr ;n
004017EE|.3BCA cmp ecx, edx
004017F0|.73 1E jnb short 00401810
004017F2|.8A4424 0C mov al, byte ptr ;取密钥
004017F6|.53 push ebx
004017F7|.57 push edi
004017F8|>8A19 /mov bl, byte ptr ;读取密文
004017FA|.32D8 |xor bl, al ; 加/解密
004017FC|.0FBEFB |movsx edi, bl
004017FF|.8819 |mov byte ptr , bl ;保存明文
00401801|.03F7 |add esi, edi ;计算较验和
00401803|.41 |inc ecx ;i++
00401804|.3BCA |cmp ecx, edx
00401806|.^ 72 F0 \jb short 004017F8
00401808|.5F pop edi
00401809|.8BC6 mov eax, esi ;返回校验和
0040180B|.5B pop ebx
0040180C|.5E pop esi
0040180D|.C2 0C00 retn 0C
就是简单的 XOR 运算加/解密,并计算校验和。
我们直接执行解码操作,如下图,解码完成后,数据区变红的部分就是解码后的代码:
接下来的一个 call 00401620 就是调用刚才解码的代码,如下图所示:
按 F7 进入函数,如下图:
可以看到红色的代码部分就是前面解码出来的代码,还可以看出,OD已经将“花指令”标示出来了。花指令如下:
00401626|. /EB 05 jmp short 0040162D
00401628||AD db AD
00401629||23 db 23 ;CHAR '#'
0040162A||CC int3
0040162B||55 db 55 ;CHAR 'U'
0040162C||FF db FF
一样也补个图片,看得更清晰:
我们可以去除这些指令,也可不去除,因为经 OD 处理分析后,也不影响我们阅读汇编代码,去不去就无所谓了。
“花指令”图片之前的图片中标蓝色底的代码是对字符串进行解码的函数调用(call 00401000),该函数内容如下图:
可见其是通过一个参数来控制加/解密的,当参数值为0表示解密,为1表示加密。CrackMe 会在使用字符串前先进行解密,使用完后马上重新进行加密,所以我们可以先把加密的代码去除,如下图:
修改后具体代码如下:
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
00401000/$56 push esi
00401001|.8B7424 08 mov esi, dword ptr
00401005|.56 push esi ; /String
00401006|.FF15 10784000call dword ptr ; \lstrlenA
0040100C|.03C6 add eax, esi
0040100E|.3BF0 cmp esi, eax
00401010|.73 22 jnb short 00401034
00401012|.53 push ebx
00401013|.8A5C24 10 mov bl, byte ptr ;参数:0-解密,1-加密
00401017|>8A06 /mov al, byte ptr ;取字符
00401019|.84DB |test bl, bl
0040101B|.75 04 |jnz short 00401021
0040101D|.04 40 |add al, 40 ;解密
0040101F|.EB 02 |jmp short 00401023
00401021 90 nop ;加密(addal, 0C0)
00401022 90 nop ;先 NOP 掉,不允许重新加密字符串
00401023|>8806 |mov byte ptr , al ;存字符
00401025|.46 |inc esi
00401026|.56 |push esi ; /String
00401027|.FF15 10784000|call dword ptr ; \lstrlenA
0040102D|.03C6 |add eax, esi
0040102F|.3BF0 |cmp esi, eax
00401031|.^ 72 E4 \jb short 00401017
00401033|.5B pop ebx
00401034|>5E pop esi
00401035\.C2 0800 retn 8
这样在跟踪过程中可以看到明文的字符串。
我们返回前面的 CrackMe 代码,可以看到,其通过调用 EnumWindows() API函数枚举窗口,如下所示:
枚举窗口时,会通过回调函数(0x004015C0)检查一个“Eip"的窗口,如果存在这个窗口,就会将下一步解密需要的密钥参数修改成 3,具体见回调函数,修改参数的是这一行代码:
00401604|.C705 4C794000 030>mov dword ptr , 3 ;设置错误密钥
代码位置如下图:
本来在 处保存的是 0,如果有"Eip"窗口,就改成3了。
从这个函数接下来的部分就是解密另一部分代码了,从前面的图片可见,待解码的代码是从0x004014F0位置开始的,并且密钥通过计算得到:
0040165C|.6A 13 push 13
0040165E|.A1 20784000 mov eax, dword ptr ;nShowCmd
00401663|.0305 4C794000 add eax, dword ptr ;如果回调检查出Eip窗口,这里值为3
00401669|.50 push eax
0040166A|.68 F0144000 push 004014F0 ;待解码代码位置
0040166F|.E8 6C010000 call 004017E0 ;解码, eax 返回 checkSum
可见,用到了的值,如果该值为3,则后面解码就会出错,CrackMe 也就无法继续了。
我们返回到主函数(WinMain),下面接下几个调用,与前面类似,都是解码,运行,再解码,再运行,都是反调试用的,如检查 frogsICE 等,就不详説了,反正对 OD 也没有影响。这一部分代码在 WinMain() 中如下所示:
004016AD .8B45 14 mov eax, dword ptr ;取得解密密钥(nShowCmd)
004016B0 .A3 20784000 mov dword ptr , eax ;eax == nShowCmd == 1,保存,在后面解码时要用到
004016B5 .6A 14 push 14 ;长度
004016B7 .50 push eax ;密钥
004016B8 .68 20164000 push 00401620 ;解密位置(下面的函数)
004016BD .E8 1E010000 call 004017E0 ;解密下一个过程
004016C2 .8945 E4 mov dword ptr , eax
004016C5 .3D AC020000 cmp eax, 2AC ;检查较验和
004016CA .75 54 jnz short 00401720
004016CC .E8 4FFFFFFF call 00401620 ;执行解密后的函数,并解密下一个过程
004016D1 .3D 2E010000 cmp eax, 12E ;检查较验和
004016D6 .75 1D jnz short 004016F5
004016D8 .E8 13FEFFFF call 004014F0 ;执行解密后的函数,并解密下一个过程
004016DD .3D 7F040000 cmp eax, 47F ;检查较验和
004016E2 .75 11 jnz short 004016F5
004016E4 .E8 B7FDFFFF call 004014A0 ;执行解密后的函数,并解密下一个过程
004016E9 .3D 12000000 cmp eax, 12 ;检查较验和
004016EE .75 05 jnz short 004016F5
004016F0 .E8 1BFEFFFF call 00401510 ;int3检查,如果正常则修正DlgProc
我们主要説説最后一个调用(call 00401510),其也包含了一部分解码后的代码,如下所示:
00401510 $55 push ebp
00401511 .8BEC mov ebp, esp
00401513 .51 push ecx
00401514 .53 push ebx
00401515 .56 push esi
00401516 .57 push edi
00401517 /EB 02 jmp short 0040151B ;花指令
00401519 |12FF adc bh, bh ;花指令
0040151B \66:BE 4746 mov si, 4647 ; "FG", int 3 检查SoftICE用的参数,调用SoftICE执行命令
0040151F 66:BF 4D4A mov di, 4A4D ; "JM", 作用同上,ax=功能号,如0x0911是执行命令。dx==>命令字符串
00401523 .90 nop
00401524 .EB 05 jmp short 0040152B ;花指令
00401526 .AD db AD
00401527 23 db 23 ;CHAR '#'
00401528 CC int3
00401529 55 db 55 ;CHAR 'U'
0040152A FE db FE
0040152B >C705 C4704000 2D5A0000mov dword ptr , 5A2D ;"-Z",这个值由下面 int3 异常处理程序修正,静态初始化值为 0x000025AD,这里改成 mov dword ptr , 0x16
00401535 .EB 05 jmp short 0040153C ;这里要改成 jmp short 0040153D,跳过 int3 指令
00401537 AD db AD
00401538 23 db 23 ;CHAR '#'
00401539 CC int3
0040153A 55 db 55 ;CHAR 'U'
0040153B FF db FF
0040153C >CC int3 ;引发 int3 中断,去执行中断处理程序
0040153D .EB 05 jmp short 00401544 ;花指令
0040153F AD db AD
00401540 23 db 23 ;CHAR '#'
00401541 CC int3
00401542 55 db 55 ;CHAR 'U'
00401543 FE db FE
00401544 >68 14784000 push 00407814
00401549 .E8 D2020000 call 00401820
0040154E .83C4 04 add esp, 4
00401551 .EB 05 jmp short 00401558 ;花指令
00401553 AD db AD
00401554 23 db 23 ;CHAR '#'
00401555 CC int3
00401556 55 db 55 ;CHAR 'U'
00401557 FF db FF
00401558 >A1 44794000 mov eax, dword ptr
0040155D .33C0 xor eax, eax
0040155F 90 nop
00401560 90 nop
00401561 90 nop
00401562 90 nop
00401563 .A3 44794000 mov dword ptr , eax
00401568 .EB 05 jmp short 0040156F ;花指令
0040156A AD db AD
0040156B 23 db 23 ;CHAR '#'
0040156C CC int3
0040156D 55 db 55 ;CHAR 'U'
0040156E FF db FF
0040156F >6A 32 push 32
00401571 .8B0D 44794000 mov ecx, dword ptr
00401577 .030D C4704000 add ecx, dword ptr
0040157D .51 push ecx
0040157E .68 10134000 push 00401310
00401583 .E8 58020000 call 004017E0
00401588 .8945 FC mov dword ptr , eax
0040158B .6A 14 push 14
0040158D .6A 0C push 0C ;密钥
0040158F .BA 10154000 mov edx, 00401510 ;入口地址
00401594 .83C2 14 add edx, 14 ;edx == 0x00401524
00401597 .52 push edx
00401598 .E8 43020000 call 004017E0 ;解码
0040159D .817D FC 8CFDFFFF cmp dword ptr , -274
004015A4 .74 02 je short 004015A8
004015A6 .EB 0A jmp short 004015B2
004015A8 >C705 84704000 10134000mov dword ptr , 00401310
004015B2 >5F pop edi
004015B3 .5E pop esi
004015B4 .5B pop ebx
004015B5 .8BE5 mov esp, ebp
004015B7 .5D pop ebp
004015B8 .C3 retn
上面代码也含有花指令没有去除。
其中关键代码如下图所示:
具体代码如下:
0040152B C705 C4704000 2D5A0000 mov dword ptr , 5A2D ; "-Z", 0x5A2D ===> 0x0016,全局变量静态初值为 0x0025AD
00401535 EB 06 jmp short 0040153C ; 修改这里,跳过 0x0040153C 处的 int 3,改成 jmp 0x0040153D 即可。
00401537 90 nop
00401538 90 nop
00401539 CC int3
0040153A 90 nop
0040153B 90 nop ;下面引发 int3 中断,去执行中断处理程序
0040153C CC int3 ; eax=0x0012, edx=00401544, 利用 int 3 将 0x5A2D 改成 0x16
0040153D EB 05 jmp short 00401544
这里给全局量赋值为 0x00005A2D,这个是传递给 SoftICE 的,如果 SICE 在运行,当下面 int 3 执行时,全进入 SICE,但不会有CrackMe需要的正常操作,如果没有加载 SoftICE ,则会通过 CrackMe 本身异常处理 Handler 来处理这个 int3 异常,这个异常处理过程如下:
; int 3 异常处理,已去花指令(nop填充):
00401740 55 push ebp
00401741 8BEC mov ebp, esp
00401743 83EC 0C sub esp, 0C
00401746 53 push ebx
00401747 56 push esi
00401748 57 push edi
00401749 8B45 08 mov eax, dword ptr
0040174C 8B08 mov ecx, dword ptr
0040174E 8B11 mov edx, dword ptr
00401750 8955 FC mov dword ptr , edx
00401753 68 44794000 push 00407944 ; time_t * t
00401758 E8 C3000000 call 00401820 ; _time(t)
0040175D 83C4 04 add esp, 4
00401760 EB 05 jmp short 00401767
00401762 90 nop
00401763 90 nop
00401764 CC int3
00401765 90 nop
00401766 90 nop
00401767 A1 C4704000 mov eax, dword ptr ; 取全局参数
0040176C 8945 F8 mov dword ptr , eax
0040176F 817D F8 2D5A0000 cmp dword ptr , 5A2D ; 检查参数是否"-Z"
00401776 75 20 jnz short 00401798
00401778 C745 F4 FFFFFFFF mov dword ptr , -1
0040177F EB 05 jmp short 00401786
00401781 90 nop
00401782 90 nop
00401783 CC int3
00401784 90 nop
00401785 90 nop
00401786 C745 F8 16000000 mov dword ptr , 16 ; 将5A2D改成0x16
0040178D 8B4D F8 mov ecx, dword ptr ; ecx == 0x16
00401790 890D C4704000 mov dword ptr , ecx ; 保存 ecx 到全局变量, 即保存 0x16
00401796 EB 07 jmp short 0040179F
00401798 C745 F4 01000000 mov dword ptr , 1
0040179F 68 14784000 push 00407814 ; time_t *
004017A4 E8 77000000 call 00401820 ; _time()
004017A9 83C4 04 add esp, 4
004017AC 8B15 44794000 mov edx, dword ptr ; 前面保存的时间 (0x00401758 处的调用)
004017B2 2B15 14784000 sub edx, dword ptr ; 计算指令执行时间差 (0x004017A4 处的调用)
004017B8 8915 44794000 mov dword ptr , edx ; 保存计算后的时间差,须为0
004017BE EB 0F jmp short 004017CF
004017C0 EB 01 jmp short 004017C3 ; 花指令(执行不到)
004017C2 90 nop ; 已nop花指令
004017C3 C745 F8 16000000 mov dword ptr , 16 ; 将5A2D改成0x16(执行不到)
004017CA- E9 B7FF3F00 jmp 00801786 ; 错误地址,和上一条指令一起,应该也是花指令(执行不到)
004017CF 8B45 F4 mov eax, dword ptr
004017D2 5F pop edi
004017D3 5E pop esi
004017D4 5B pop ebx
004017D5 8BE5 mov esp, ebp
004017D7 5D pop ebp
004017D8 C2 0400 retn 4
其中以下一段代码:
00401767 A1 C4704000 mov eax, dword ptr ; 取全局参数
0040176C 8945 F8 mov dword ptr , eax
0040176F 817D F8 2D5A0000 cmp dword ptr , 5A2D ; 检查参数是否"-Z"
00401776 75 20 jnz short 00401798
00401778 C745 F4 FFFFFFFF mov dword ptr , -1
0040177F EB 05 jmp short 00401786
00401781 90 nop
00401782 90 nop
00401783 CC int3
00401784 90 nop
00401785 90 nop
00401786 C745 F8 16000000 mov dword ptr , 16 ; 将5A2D改成0x16
0040178D 8B4D F8 mov ecx, dword ptr ; ecx == 0x16
00401790 890D C4704000 mov dword ptr , ecx ; 保存 ecx 到全局变量, 即保存 0x16
00401796 EB 07 jmp short 0040179F
00401798 C745 F4 01000000 mov dword ptr , 1
会将前面已赋值 0x5A2D 的全局变量的值改成 0x00000016,而这个 0x16 就是最后一次代码解码需要的密钥值之一。
返回到函数 00401510 中,可以看到如下解码过程用到了这个值:
00401571 8B0D 44794000 mov ecx, dword ptr ; 保存的时间差(参见 0x004017B8的指令)
00401577 030D C4704000 add ecx, dword ptr ; int3 异常处理过程修正此地址值为 0x16
0040157D 51 push ecx ; ecx == 0x16
0040157E 68 10134000 push 00401310 ; 解码地址
00401583 E8 58020000 call 004017E0 ; 解码代码
如果在 OD 中简单跳过 int3 指令,根本就没法解码后面的代码了,这也是最大的坑了,包括加载了 SICE 也会掉入此坑,SICE也不知如何处理。
另外还有一个坑,就是主对话框的回调过程(DlgProc)有两个,一个真的,一个假的,默认情况下是用的假的,真的就是上面代码解码出来的函数,入口为 0x00401310。而假的入口则为 0x004013F0,而且还象模象样含有大量代码,有 strcmp()之类的操作。
而如果上面解码不对,也就是 checkSum也不对,就不会修正 DialogBoxParamA 的 DlgProc 参数,修正DlgProc参数代码如下:
0040159D 817D FC 8CFDFFFF cmp dword ptr , -274 ; 检查 0x00401583 处的调用结果(校验和)
004015A4 74 02 je short 004015A8
004015A6 EB 0A jmp short 004015B2 ; 解码校验和不正确,则跳过修正 DlgProc 地址。
004015A8 C705 84704000 10134000 mov dword ptr , 00401310 ; 修正主对话框的 DlgProc 地址
给个图形象一点:
上面中保存 DlgProc 参数。
前面解码完成后,CrackMe 会将自己这个函数再次加密,如图所示位置的调用:
其实就是解码函数,再次XOR操作,就是加密了,具体代码如下:
0040158B 6A 14 push 14 ; 长度
0040158D 6A 0C push 0C ; 密钥
0040158F BA 10154000 mov edx, 00401510
00401594 83C2 14 add edx, 14 ; edx = 0x00401524
00401597 52 push edx ; 解码地址
00401598 E8 43020000 call 004017E0 ; 加密代码,实际则是破坏本函数内0x00401524开始的20个字节的代码,checkSum 存于 eax
因为代码解码函数的功能已全部完成,并且为了阻止上面的调用重新加密代码,我们可以修改加密函数了,不让其再有效,而且,我们等下用 OD 保存明码 patch 后,以后运行也不需解密了,先将函数修改,如下图所示:
将xor指令nop掉,就去除其加/解密功能,只剩下计算 checkSum 的功能,所以也不影响CrackMe对 checkSum 的检查和使用,修改后的代码如下:
004017FA 90 nop ;去掉这里,即可取消解码操作
004017FB 90 nop
还有一个问题,在 WinNT 系列内核中,这个函数中的 int3 也不会执行异常处理过程,而会导至致操作系统直接关闭 CrackMe ,因此,我们需要处理掉这个 int3 才能在 win10 下运行。
改两条指令即可,1、给变量赋值改成 0x16,2、JMP 跳过int3指令。如下图,蓝底位置的两行代码(mov/jmp)是改好后的情况:
同时,还要平衡 checkSum 值,因此对”花指令“代码进行调整,用了两个 Xor 指令来调整的,调整的代码部分如下:
0040152B C705 C4704000 16000000 mov dword ptr , 16 ; 直接保存密钥常量 0x16,不需要进行异常处理。
00401535 EB 06 jmp short 0040153D ; 跳过 0040153C 处的 int3 指令
00401537 35 00636290 xor eax, 90626300 ; 校验和平衡处理
0040153C CC int3
0040153D EB 05 jmp short 00401544
0040153F 35 00000090 xor eax, 90000000 ; 校验和平衡处理
00401544 68 14784000 push 00407814
00401549 E8 D2020000 call 00401820
0040154E 83C4 04 add esp, 4
00401551 EB 05 jmp short 00401558
如下图所示:
完成这些操作后,第一部分脱密工作已经完成,CrackMe 可以在 Win9x内核/NT系列内核的系统上直接运行了。我们可以通过 OD 的 patch 保存功能,将所有解码的修改保存了,如下图所示:
这样操作以后,我们可以重新加载 CrackMe,进行下一步进行处理。
我们再次回到 WinMain(),剩下的代码就是显示主对话框了:
就是执行上面蓝底部分的代码。具体代码如下:
004016F5 >6A 00 push 0 ; /lParam = NULL
004016F7 .A1 84704000 mov eax, dword ptr ; |004013F0 为错误的 DlgProc, 00401310 为正确的 DlgProc
004016FC .50 push eax ; |DlgProc => Elraizer.004013F0
004016FD .6A 00 push 0 ; |hOwner = NULL
004016FF .68 E9030000 push 3E9 ; |pTemplate = 3E9
00401704 .8B0D 34784000 mov ecx, dword ptr ; |
0040170A .51 push ecx ; |hInst => NULL
0040170B .FF15 08784000 call dword ptr ; \DialogBoxParamA
00401711 .EB 0D jmp short 00401720
直接F9 显示主对话界面:
可以看到,需要输入一个注册码,再点”IsThisOk“按钮来检查输入的注册码是否正确。
我们前面已经説了,主对话框的 DlgProc 入口为 0x00401310。如下图所示:
按钮事件处理代码如下:
具体DlgProc代码如下:
00401310 55 push ebp
00401311 8BEC mov ebp, esp
00401313 83EC 14 sub esp, 14
00401316 53 push ebx
00401317 56 push esi
00401318 57 push edi
00401319 8B45 0C mov eax, dword ptr
0040131C 8945 F8 mov dword ptr , eax
0040131F 8B4D F8 mov ecx, dword ptr
00401322 894D F4 mov dword ptr , ecx
00401325 836D F4 10 sub dword ptr , 10 ; WM_CLOSE
00401329 837D F4 00 cmp dword ptr , 0
0040132D 0F84 8E000000 je 004013C1 ; goto handle WM_CLOSE(0x10)
00401333 816D F4 00010000 sub dword ptr , 100 ; WM_INITDIALOG(0x110)
0040133A 837D F4 00 cmp dword ptr , 0
0040133E 0F84 8E000000 je 004013D2 ; goto handle WM_INITDIALOG
00401344 836D F4 01 sub dword ptr , 1 ; WM_COMMAND(0x111)
00401348 837D F4 00 cmp dword ptr , 0
0040134C 74 05 je short 00401353 ; goto handle WM_COMMAND
0040134E E9 8A000000 jmp 004013DD
00401353 8B55 10 mov edx, dword ptr ; 处理 WM_COMMAND 消息
00401356 8955 F0 mov dword ptr , edx
00401359 8B45 F0 mov eax, dword ptr
0040135C 8945 EC mov dword ptr , eax
0040135F 836D EC 01 sub dword ptr , 1 ; ControlID = 0x01
00401363 837D EC 00 cmp dword ptr , 0
00401367 74 02 je short 0040136B
00401369 EB 54 jmp short 004013BF
0040136B 68 04010000 push 104 ; 处理ControlID=1的控件事件
00401370 68 3C784000 push 0040783C ; 保存假码的 buffer 地址
00401375 6A 64 push 64
00401377 8B0D 24784000 mov ecx, dword ptr ; = 0x00A50FE0
0040137D 51 push ecx
0040137E FF15 30784000 call dword ptr ; USER32.GetDlgItemTextA
00401384 68 3C784000 push 0040783C ; 注册码,测试通过的SN: 6#"!!!!!!!!!!!!! 或 BZZ
00401389 E8 62FEFFFF call 004011F0 ; 处理注册码,计算后面解用的密钥(call 00401180 解码代码的密钥)
0040138E 8945 FC mov dword ptr , eax ; xorKey, eax 为sn计算后的结果, 正确结果为 0x0000F84B,
00401391 6A 06 push 6 ; 解码长度
00401393 8B55 FC mov edx, dword ptr
00401396 52 push edx ; edx == 0xFFFFF84B 才是正确结果
00401397 B8 C0124000 mov eax, 004012C0 ; 需要解码的函数入口地址
0040139C 83C0 26 add eax, 26 ; 加上偏移量0x26,eax = 004012E6,即解码开始位置
0040139F 50 push eax
004013A0 E8 DBFDFFFF call 00401180 ; 使用计算后注册码运行值(xorKey)解码6字节代码
004013A5 8945 FC mov dword ptr , eax ; 解码后的校验和
004013A8 817D FC 5B8E0000 cmp dword ptr , 8E5B ; checkSum = 0x8E5B, 才是正确值,可以通过校验和反算出 xorKey = 0xF84B,所以SN的计算处理结果为0xF84B,则表示输入SN是正确的
004013AF 75 0C jnz short 004013BD
004013B1 EB 05 jmp short 004013B8
004013B3 90 nop
004013B4 90 nop
004013B5 CC int3
004013B6 90 nop
004013B7 90 nop
004013B8 E8 03FFFFFF call 004012C0 ; 解码字符串并显示完成CrackMe的破解消息框
004013BD EB 26 jmp short 004013E5 ; 去下面代码关闭CrackMe对话框,退出CrackMe
004013BF EB 20 jmp short 004013E1
004013C1 6A 01 push 1
004013C3 8B0D 24784000 mov ecx, dword ptr ; hWnd
004013C9 51 push ecx
004013CA FF15 48794000 call dword ptr ; USER32.EndDialog
004013D0 EB 0F jmp short 004013E1
004013D2 8B55 08 mov edx, dword ptr
004013D5 8915 24784000 mov dword ptr , edx
004013DB EB 04 jmp short 004013E1
004013DD 33C0 xor eax, eax
004013DF EB 06 jmp short 004013E7
004013E1 33C0 xor eax, eax
004013E3 EB 02 jmp short 004013E7
004013E5 ^ EB DA jmp short 004013C1
004013E7 5F pop edi
004013E8 5E pop esi
004013E9 5B pop ebx
004013EA 8BE5 mov esp, ebp
004013EC 5D pop ebp
004013ED C2 1000 retn 10
其注册码处理函数是 call 004011F0,具体代码和分析如下:
004011F0 55 push ebp
004011F1 8BEC mov ebp, esp
004011F3 83EC 0C sub esp, 0C
004011F6 53 push ebx
004011F7 56 push esi
004011F8 57 push edi
004011F9 C745 F8 00000000 mov dword ptr , 0 ; int snResult = 0
00401200 8B45 08 mov eax, dword ptr ; eax ==> sn, eax ===> 假码 787878787878787878
00401203 0FBE08 movsx ecx, byte ptr ; ecx = sn
00401206 894D F4 mov dword ptr , ecx ; == sn
00401209 8B55 F4 mov edx, dword ptr
0040120C 8B45 08 mov eax, dword ptr ; eax ===> 假码 787878787878787878
0040120F 83C0 01 add eax, 1 ; i ++
00401212 8945 08 mov dword ptr , eax
00401215 85D2 test edx, edx ; 字符串是否结束
00401217 0F84 8A000000 je 004012A7
0040121D 8B4D F4 mov ecx, dword ptr ; sn
00401220 83E1 7F and ecx, 7F ; ecx = sn & 0x7F
00401223 894D F4 mov dword ptr , ecx ; = sn = sn & 0x7F
00401226 EB 05 jmp short 0040122D
00401228 90 nop
00401229 90 nop
0040122A CC int3
0040122B 90 nop
0040122C 90 nop
0040122D 8B55 F8 mov edx, dword ptr ; edx = snResult
00401230 3355 F4 xor edx, dword ptr ; snResult ^= sn
00401233 83E2 0F and edx, 0F
00401236 8955 FC mov dword ptr , edx ; snResult_tmp = (snResult ^ (sn)) & 0x0F
00401239 EB 05 jmp short 00401240
0040123B 90 nop
0040123C 90 nop
0040123D CC int3
0040123E 90 nop
0040123F 90 nop
00401240 8B45 F8 mov eax, dword ptr ; snResult
00401243 C1F8 04 sar eax, 4
00401246 8B4D FC mov ecx, dword ptr
00401249 69C9 81100000 imul ecx, ecx, 1081
0040124F 33C1 xor eax, ecx
00401251 8945 F8 mov dword ptr , eax ; snResult = (snResult>>4)^(snResult_tmp*0x1081)
00401254 EB 05 jmp short 0040125B
00401256 90 nop
00401257 90 nop
00401258 CC int3
00401259 90 nop
0040125A 90 nop
0040125B 8B55 F4 mov edx, dword ptr ; sn
0040125E C1EA 04 shr edx, 4
00401261 8B45 F8 mov eax, dword ptr ; snResult
00401264 33C2 xor eax, edx
00401266 83E0 0F and eax, 0F
00401269 8945 FC mov dword ptr , eax ; snResult_tmp = ((sn>>4)^snResult) & 0x0F
0040126C EB 05 jmp short 00401273
0040126E 90 nop
0040126F 90 nop
00401270 CC int3
00401271 90 nop
00401272 90 nop
00401273 8B4D F8 mov ecx, dword ptr ; snResult
00401276 C1F9 04 sar ecx, 4
00401279 8B55 FC mov edx, dword ptr ; snResult_tmp
0040127C 69D2 81100000 imul edx, edx, 1081
00401282 33CA xor ecx, edx
00401284 894D F8 mov dword ptr , ecx ; snResult = (snResult>>4)^(snResult_tmp * 0x1081)
00401287 EB 05 jmp short 0040128E
00401289 90 nop
0040128A 90 nop
0040128B CC int3
0040128C 90 nop
0040128D 90 nop
0040128E 8B45 F8 mov eax, dword ptr ; snResult
00401291 C1F8 04 sar eax, 4
00401294 8B4D FC mov ecx, dword ptr ; snResult_tmp
00401297 69C9 81100000 imul ecx, ecx, 1081
0040129D 33C1 xor eax, ecx
0040129F 8945 F8 mov dword ptr , eax ; snResult = (snResult>>4)^(snResult_tmp * 0x1081)
004012A2^ E9 59FFFFFF jmp 00401200
004012A7 8B45 F8 mov eax, dword ptr ; retVal = snResult
004012AA 5F pop edi
004012AB 5E pop esi
004012AC 5B pop ebx
004012AD 8BE5 mov esp, ebp
004012AF 5D pop ebp
004012B0 C2 0400 retn 4
这个函数会将输入的注册码通过运算,计算出一个 WORD 类型的密钥,然后再使用这个值去解码另外一段代码,如果注册码不对,就会解码失败,校验和通不过,也就不会提示你成功!而是直接退出 CrackMe。
我们反算注册码分成两步,首先要计算出这个”密钥“,然后再根据”密钥“反推注册码。
因为有解密后的校验和,我们只要用一个循环,从 0~65535 全部试一遍,就肯定可以得到至少一个密钥了(也只有一个)。这个密钥是用来解密下图中6个字节代码的:
具体代码如下:
004012C0 $6A 00 push 0
004012C2 .68 65704000 push 00407065 ;待解码字符串
004012C7 .E8 34FDFFFF call 00401000
004012CC .6A 00 push 0
004012CE .68 5D704000 push 0040705D ;待解码字符串
004012D3 .E8 28FDFFFF call 00401000
004012D8 .6A 00 push 0
004012DA .68 5D704000 push 0040705D
004012DF .68 65704000 push 00407065
004012E4 .6A 00 push 0
004012E6 .B4 ED mov ah, 0ED ;待解码数据,共6字节
004012E8 .57800BF8 dd F80B8057 ;待解码数据
004012EC .6A 01 push 1
004012EE .68 65704000 push 00407065
004012F3 .E8 08FDFFFF call 00401000
004012F8 .6A 01 push 1
004012FA .68 5D704000 push 0040705D
004012FF .E8 FCFCFFFF call 00401000
00401304 .C3 retn
根据这6字节数据和校验和,反推密钥,得到密钥后,再就是反推注册码了,注册机代码放在后面。我们可以算到一个简单而正确的三字符注册码:BZZ。如下图,我们输入注册码:
再次点击 "IsThisOk",来测试注册码,如下图:
得到密钥:
解码,得到校验和:
解码后的代码如下图所示:
就是一个 MessageBoxA 的调用,用来显示破解成功的提示。具体代码如下:
004012C0 6A 00 push 0
004012C2 68 65704000 push 00407065 ; ASCII "YES, YOU'RE RIGHT :)"
004012C7 E8 34FDFFFF call 00401000 ; 解码下面 MessageBox 字符串
004012CC 6A 00 push 0
004012CE 68 5D704000 push 0040705D ; ASCII "Finish"
004012D3 E8 28FDFFFF call 00401000 ; 解码下面 MessageBox 字符串
004012D8 6A 00 push 0
004012DA 68 5D704000 push 0040705D ; ASCII "Finish"
004012DF 68 65704000 push 00407065 ; ASCII "YES, YOU'RE RIGHT :)"
004012E4 6A 00 push 0
004012E6 FF15 1C784000 call dword ptr ; USER32.MessageBoxA
004012EC 6A 01 push 1
004012EE 68 65704000 push 00407065 ; ASCII "YES, YOU'RE RIGHT :)"
004012F3 E8 08FDFFFF call 00401000 ; 加密字符串
004012F8 6A 01 push 1
004012FA 68 5D704000 push 0040705D ; ASCII "Finish"
004012FF E8 FCFCFFFF call 00401000 ; 加密字符串
00401304 C3 retn
最后结果显示如下:
表示破解成功了!!!
注册机代码如下,使用 Dev-C++ 调试通过:
#include <iostream>
#include <string.h>
int count = 0;
char sn;
int checkSN(char * code);
int getPrevCheckValue(int LastCheckValue, int index);
int main(int argc, char** argv) {
unsigned short codeBase[] = {0xEDB4, 0x8057, 0xF80B};
unsigned short checkSum = 0x8E5B;
//// 通过checkSum反推解密密钥
unsigned short xorKey=0;
do {
unsigned short snResult2_tmp = 0;
for(int j=0; j<3; j++) {
snResult2_tmp += (xorKey ^ codeBase);
}
if(snResult2_tmp == checkSum) {
printf("xorKey = 0x%04X\n", xorKey);
break;
}
xorKey++; /// checkValue
} while(xorKey<0xFFFF); //// 搜索范围 0~65535
//int LastCheckValue = 0x0000F84B;
int LastCheckValue = xorKey & 0x0000FFFF;
int index = 0;
//// 计算注册码
int checkStatus = getPrevCheckValue(LastCheckValue, index);
if(checkStatus == -1) {
printf("NO key, count = %d\n", count);
}
if(checkStatus == 0) {
printf("Found.\n");
sn = '\0';
strrev((char *)sn); // 反转SN
printf("SN = %s\n", sn);
//char sn_test[] = "787878787878787878";
//printf("sn Test = 0x%08X\n", checkSN(sn_test));
printf("test sn, get xorKey = 0x%08X\n", checkSN(sn));
////
}
return 0;
}
int checkSN(char * code) {
int snResult, snResult_tmp;
unsigned int s;
int j = 0;
snResult = 0;
while((s=code) != 0) {
s &= 0x7F;
snResult_tmp = (snResult ^ (s)) & 0x0F; //// snResult_tmp 的范围是0x00~0x0F,因此后面 snResult_tmp*0x1081 < 0xFFFF
snResult = (snResult>>4)^(snResult_tmp*0x1081);//// 结果的最高位4bit(0xF000)是后面乘积的最高位4bit,而snResult初始值和计算值均小于0xFFFF,因此计算结果 snResult < 0xFFFF
////
snResult_tmp = ((s>>4)^snResult) & 0x0F; //// snResult_tmp 的范围是0x00~0x0F,因此后面 snResult_tmp*0x1081 < 0xFFFF
snResult = (snResult>>4)^(snResult_tmp * 0x1081); //// 结果的最高位4bit(0xF000)是后面乘积的最高位4bit,因此 snResult < 0xFFFF
snResult = (snResult>>4)^(snResult_tmp * 0x1081); //// 结果的最高位4bit(0xF000)是后面乘积的最高位4bit,因此 snResult < 0xFFFF
}
return snResult;// & 0x0FFFF;
}
//// short checkSum = 0x8E5B;
//// int LastCheckValue = xorKey = 0x0000F84B;
int getPrevCheckValue(int LastCheckValue, int index) {
if(LastCheckValue == 0) {
//// found sn
count = index;
sn = '\0';
return 0;
}
if(index > 3) {// 最长4位 SN
//if(index > 7) {// 最长8位 SN
//if(index > 15) { // 最长16位 SN
//// sn is too long
return -1;
}
//for(int i=0x21; i<0x7F; i++) { // 键盘可输入的非空格字符的Ascii码值(0x21~0x7E)
//for(int i=0x7E; i>=0x21; i--) { // 键盘可输入的非空格字符的Ascii码值(0x21~0x7E) ,逆序
//for(int i=0x30; i<=0x39; i++) { // 仅测试数字注册码 (找不到合适的纯数字SN)
//for(int i=0x39; i>=0x30; i--) { // 仅测试数字注册码 (找不到合适的纯数字SN),逆序
//for(int i=0x41; i<=0x5A; i++) { // 仅测试大写字母注册码(11秒内找到4位SN: "XNCA")
for(int i=0x5A; i>=0x41; i--) { // 仅测试大写字母注册码,逆序 (5秒内找到3位SN: "BZZ")
//for(int i=0x61; i<=0x7A; i++) { // 仅测试小写字母注册码 (7秒内找到4位SN: "tnba")
//for(int i=0x7A; i>=0x61; i--) { // 仅测试小写字母注册码 ,逆序
unsigned int aKey = i & 0x7F;
for(int j=0x0000; j<0x10000; j++) { //上一轮计算值 prevResult(0x0000~0xFFFF), 65536次尝试
int prevResult = j;
////---------------------------------------------------------
int snResult_tmp = (aKey ^ prevResult) & 0x0F; /// 0x00~0x0F
int snResult = (prevResult>>4)^(snResult_tmp * 0x1081);
snResult_tmp = ((aKey>>4) ^ snResult) & 0x0F;/// 0x00~0x0F
snResult = (snResult>>4)^(snResult_tmp*0x1081);
int currResult = (snResult>>4)^(snResult_tmp*0x1081);
//printf("Key: 0x%02X, prevResult = %d: snResult = 0x%08X\n", i, j, currResult);
if(currResult == LastCheckValue) {
sn = aKey;
//printf("Key: 0x%02X, prevResult = 0x%08X: snResult = 0x%08X\n", i, j, currResult);
//return prevResult;
int b = getPrevCheckValue(prevResult, index+1);
if(b==0) {
return 0;
}
}
}
}
return -1;
}
分析完毕!!!
厉害,学习了
一脸懵,虽然看不懂,但还是有股热情想学习。 拜服!学习了很久才明白 哈哈 大致明白 学习啦。谢谢大神
页:
[1]