solly 发表于 2019-6-27 20:04

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;
}



分析完毕!!!

倾尽晨光慕流年 发表于 2019-6-27 20:13

厉害,学习了

anixix 发表于 2019-6-28 14:48

一脸懵,虽然看不懂,但还是有股热情想学习。

jgq13900 发表于 2019-6-28 17:40

拜服!学习了很久才明白 哈哈 大致明白

cai_zm 发表于 2019-7-1 21:02

学习啦。谢谢大神
页: [1]
查看完整版本: 160 个 CrackMe 之 056 Elraizer.2 解码代码破解、注册分析及注册机实现