作死的逆向分析

打开源程序所在文件夹,发现有一个crackme


双机运行程序发现有这个提示:


应该是文件的PE结构被修改了,winhex载入分析发现:

果然是PE结构的问题,在DOS头后面的PE头的16进制应该为50 45,将上述52修改为45,保存文件之后发现仍然运行不了,证明PE结构仍然存在问题。

分析PE头后面的IMAGE_FILE_HEADER(映像文件头,NT头),对比结构:

typedef struct _IMAGE_FILE_HEADER {

    WORD    Machine;                            //运行平台

    WORD    NumberOfSections;            //文件的区块数目

    DWORD   TimeDateStamp;               //文件创建的日期和时间

    DWORD   PointerToSymbolTable;      //指向符号表

    DWORD   NumberOfSymbols;           //符号表中符号个数

    WORD    SizeOfOptionalHeader;             //IMAGE_OPTIONAL_HEADER32结构大小

    WORD    Characteristics;                  //文件属性

} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

 

上述地址分别相对于50450000之后偏移04h(4C01),06h(0300),08h,0Ch,10h,14h,16h对比之后发现在运行平台上为1F0h。

而我们熟知运行平台如下图:

故将其修改为14Ch,保存文件。修复PE结构之后可以正常运行了

双机运行之后如下图:


输入用户名和注册码之后提示不正确,而且在过了一段时间之后程序自动退出,应该加了时间控制(此时主窗口已经退出,只有错误提示框)

用PEID分析载入分析之后发现:

程序加了WinUpack的壳好在只是一个普通的压缩弱壳,用PEID自带的插件Krypto ANAlzer扫了一遍程序


了解到该程序并没有使用什么知名的加密算法。

因为这里加了壳,不便于静态分析,故笔者在这里并未使用IDA。用OD载入程序,OEP被壳修改,要先脱壳。因为WinUpack为弱壳,所以根据OEP定律,单步运行至OEP改变时右键数据窗口中跟随,然后下硬件访问断点:

运行之后程序停在OEP,因为加了壳使OD并没有完全正常解析指令,:

开始脱壳,用ollydump记录下程序OEP

脱壳之后因为IAT被破坏所以无法正常运行软件

使用ImportREC修复脱壳后程序,将 OEP改为上述14EC,获取输入表,发现全部有效,然后修复上面脱壳的转存文件:

能正确运行。OD再次载入,停在正确的OEP,正式开始破解:(这里因为重建了输入表,所以程序的大小会比之前的源程序要大一些,属于正常情况)

看到程序的入口点应该想到程序使用了较为高级的花指令,伪装了一些API调用,然后通过call eax致使无法查到这些API的调用。

根据最开始的提示Error(标题栏)和“注册码错误”,使用字符串查找如下:

Ctrl+G(转到上述地址),到这些地址处发现:

然后发现有一个call eax,但是并没有显示出来哪个函数,继续Ctrl+G到403884处发现如下:

程序在运行的时候会进行自修改,将这些地方补充上代码,进而显示正确的函数。因为这些反静态分析技术,所以使用IDA进行分析并不方便,故这里并未使用IDA进行静态分析。

在上面调用处下断点,F9(运行)发现程序直接退出,又证明程序中加了反动态调试技术。

退回到OEP一步步分析:运行到这一步时程序会自动终止:

因为前面有一个捕获异常函数SetUnhandledExceptionFilter,在程序被调试时,ptr ds[eax]此处地址为0是不可读写的,而这里向一块不可写的内存中写入0x1,自然触发异常,终止程序。

Nop掉这个函数和异常触发的mov。

重新载入程序至:

这个是窗口主函数了,这个API的第4个参数为00401340,就是窗口主程序所在地址了,转到在00401340下int 3断点,运行至后得:

先不着急单步,浏览一遍代码之后发现程序在此段中多次调用了0040101E处的函数,enter进去之后发现第二层反调试:

00401027 |.  68 28010000   push 0x128                               ; /Length = 128(296.)

0040102C |.  8D85 D8FEFFFF leaeax,[local.74]                       ; |

00401032 |.  50            push eax                                 ; |Destination

00401033  |.  E8FA050000   call <jmp.&kernel32.RtlZeroMemory>       ; \RtlZeroMemory

00401038 |.  C785 D8FEFFFF>mov[local.74],0x128

00401042 |.  6A 00         push 0x0                                 ; /ProcessID =0

00401044 |.  6A 02         push 0x2                                 ; |Flags = TH32CS_SNAPPROCESS

00401046  |.  E8AB050000   call<jmp.&kernel32.CreateToolhelp32Snap>; \CreateToolhelp32Snapshot

0040104B |.  8985 D4FEFFFF mov[local.75],eax

00401051 |.  8D85 D8FEFFFF leaeax,[local.74]

00401057 |.  50            push eax                                 ; /lppe

00401058 |.  FFB5 D4FEFFFF push[local.75]                          ;|hSnapshot

0040105E  |.  E8C3050000   call<jmp.&kernel32.Process32First>     ; \Process32First

00401063 |.  EB 1F         jmp Xdump1.00401084

00401065  |> E8 98050000   /call<jmp.&kernel32.GetCurrentProcessId>; [GetCurrentProcessId

0040106A |.  3B85 E0FEFFFF |cmpeax,[local.72]

00401070 |.  74 26         |je Xdump1.00401098

00401072 |.  8D85 D8FEFFFF |leaeax,[local.74]

00401078 |.  50            |push eax                                ; /lppe

00401079 |.  FFB5 D4FEFFFF |push[local.75]                         ;|hSnapshot

0040107F  |.  E8A8050000   |call<jmp.&kernel32.Process32Next>     ; \Process32Next

00401084 |>  0BC0           or eax,eax

00401086 |.^ 75 DD         \jnzXdump1.00401065

上面主要是通过调用系统快照函数(红色字体标注部分),然后遍历这个系统当前的进程ID,直到找到当前dump的进程ID后跳走,数据窗口中跟踪[local.72]地址,发现确实在遍历进程名和ID。证实了上面我的想法。

最终得到当前进程名dump1.exe和ID:12C0,然后跳出循环。

而在以下代码中发现了第二层反调试的真面目:

将当前进程的父进程与系统下Explorer.exe进行对比。

继续单步运行:第二次判断父进程是否为CMD.exe


这里因为一般运行在windows系统下进程调度时,大部分进程都是有父进程Explorer.exe或者cmd.exe创建的,而当程序处于调试状态时父进程肯定是调试进程,所以这一层反调试能针对很多调试软件起到很好的反调试作用。

在程序中多次调用了这一层反调试,所以单纯的nop需要靠IDC脚本实现多次,这里我们让

00401114  |. /7468         je Xdump1.0040117E

改为jmp 00401117E,让它恒跳走,让程序误以为父进程校验正确。

保存文件之后想到刚开始注册时会有成功或者失败提示,那么是调用了MessageBox这个函数。根据这个信息,我们想到了查找函数。于是查找api调用(方法是Ctrl+N或者右键,点击“查找”中的“当前模块中的名称”)如下

发现这里并没有messagebox,这里应该是到了花指令,调用api之前将api名字做了隐藏,之后直接call eax,程序刚开始便说明了这一点:

但是在调用的API中发现了GetDlgItem,这是一个破绽。直接下int 3断点,运行之后发现自己还没来得及输入用户名和注册码程序自动退出,这让我想到刚刚主窗口中的两个可疑的函数SetTimer

0040138E |.  6A 00         push 0x0                                 ; /Timerproc =NULL

00401390 |.  68 E8030000   push 0x3E8                               ; |Timeout =1000. ms

00401395 |.  6A 06         push 0x6                                 ; |TimerID = 6

00401397 |.  FF75 08       push [arg.1]                             ; |hWnd

0040139A  |.  E845020000   call<jmp.&user32.SetTimer>             ; \SetTimer

004013C4 |.  6A 00         push 0x0                                 ; /Timerproc =NULL

004013C6 |.  68 10270000   push 0x2710                              ; |Timeout =10000. ms

004013CB |.  6A 05         push 0x5                                 ; |TimerID = 5

004013CD |.  FF75 08       push [arg.1]                             ; |hWnd

004013D0  |.  E80F020000   call<jmp.&user32.SetTimer>             ; \SetTimer

两个都是SetTimer,这个就能解释之前程序会自动退出的原因了,SetTmier函数即为每隔固定的一个时间向所在窗口发送消息。上面这段应该发送的是WM_CLOSE而销毁了窗口。,分析代码知道一个是1000ms一个是10000ms,而我们在反调试分析代码过程中所需要的时间远远大于这些时间,所以自然会退出。这也是利用调试时间差起到反调试的思路。

将timeout参数值改成FFFF,时间应该我们足够逆向分析用了

然后保存文件之后载入文件,在getdlgitem上下int 3断点,成功断下:

0040143B |.  6A 03         push 0x3                                 ; /ControlID =3

0040143D |.  FF75 08       push [arg.1]                             ; |hWnd

00401440 |.  E8 8D010000   call <jmp.&user32.GetDlgItem>            ; \GetDlgItem

00401445 |.  A3 60304000   mov dword ptr ds:[0x403060],eax

0040144A |.  6A 04         push 0x4                                 ; /ControlID =4

0040144C |.  FF75 08       push [arg.1]                             ; |hWnd

0040144F |.  E8 7E010000   call <jmp.&user32.GetDlgItem>            ; \GetDlgItem

00401454 |.  A3 64304000   mov dword ptr ds:[0x403064],eax

00401459 |.  68 74304000   pushdump3.00403074                      ;/lParam = 403074

0040145E |.  6A 32         push 0x32                                ; |wParam = 32

00401460 |.  6A 0D         push 0xD                                 ; |Message =WM_GETTEXT

00401462 |.  FF35 60304000 push dword ptrds:[0x403060]             ; |hWnd = C0B2C

00401468 |.  E8 71010000   call<jmp.&user32.SendMessageA>         ; \SendMessageA

0040146D |.  68 F4304000   push dump3.004030F4                      ; /lParam = 4030F4

00401472 |.  6A 32         push 0x32                                ; |wParam = 32

00401474 |.  6A 0D         push 0xD                                 ; |Message =WM_GETTEXT

00401476 |.  FF35 64304000 push dword ptrds:[0x403064]             ; |hWnd = A07EC

0040147C |.  E8 5D010000   call<jmp.&user32.SendMessageA>         ; \SendMessageA

程序使用SendMessageA,将字符串的内容送至00403074和004030F4两处,避免使用GetDlgItemTextA函数直接能获取明文:

单步跟踪

发现算法在call eax之后来到如下代码。跟进之后算法分析见代码中注释:

 

004011D9  > \A1 56304000   mov eax,dwordptr ds:[0x403056]          ;  核心算法,此地址处存放用户名的长度

004011DE  .  83F8 06       cmp eax,0x6                              ;  用户名长度>=6

004011E1  .  0F8C 97000000 jl dump3.0040127E

004011E7  .  50            push eax

004011E8  .  59            pop ecx

004011E9  .  8D35 00304000 lea esi,dword ptrds:[0x403000]          ;  预定字符串S1

004011EF  .  8D3D 74304000 lea edi,dword ptrds:[0x403074]          ;  用户名

004011F5  >  33C0          xor eax,eax

004011F7  .  33DB          xor ebx,ebx

004011F9  .  8B07          mov eax,dword ptr ds:[edi]               ;  将4位用户名给eax

004011FB  .  8B1E          mov ebx,dword ptr ds:[esi]               ;  将4位s1给ebx

004011FD  .  25 FF000000   and eax,0xFF                             ;  去掉高位保留第一位,即取一位用户名

00401202  .  81E3 FF000000 and ebx,0xFF                             ;  去掉高位保留第一位,取S一位

00401208  .  33C3          xor eax,ebx                              ;  二者异或

0040120A  .  0305 4E304000 add eax,dword ptrds:[0x40304E]          ;  然后累加

00401210  .  A3 4E304000   mov dword ptr ds:[0x40304E],eax

00401215  .  46            inc esi

00401216  .  47            inc edi                                  ;  循环向后读取

00401217  .^ E2 DC         loopdXdump3.004011F5

00401219  .  33C9          xor ecx,ecx

0040121B  .  8B0D 5A304000 mov ecx,dword ptrds:[0x40305A]          ;  注册码长度

00401221  .  8D35 25304000 lea esi,dword ptrds:[0x403025]          ;  预定字符串S2

00401227  .  8D3D F4304000 lea edi,dword ptrds:[0x4030F4]          ;  注册码

0040122D  >  33C0          xor eax,eax

0040122F  .  33DB          xor ebx,ebx

00401231  .  8B07          mov eax,dword ptr ds:[edi]               ;  算法同上

00401233  .  8B1E          mov ebx,dword ptr ds:[esi]

00401235  .  25 FF000000   and eax,0xFF

0040123A  .  81E3 FF000000 and ebx,0xFF

00401240  .  33C3          xor eax,ebx

00401242  .  0305 52304000 add eax,dword ptrds:[0x403052]

00401248  .  A3 52304000   mov dword ptr ds:[0x403052],eax

0040124D  .  46            inc esi

0040124E  .  47            inc edi

0040124F  .^ E2 DC         loopdXdump3.0040122D                    ;  循环读取

00401251  .  A1 52304000   mov eax,dword ptr ds:[0x403052]

00401256  .  8B1D 4A304000 mov ebx,dword ptrds:[0x40304A]

0040125C  .  85DB          test ebx,ebx

0040125E  .  75 3A         jnz Xdump3.0040129A                      ;  不等跳至失败

00401260  .  8505 4E304000 test dword ptrds:[0x40304E],eax         ;  比较用户名与S1异或和是否等于注册码与S2的异或和

00401266  .  75 32         jnz Xdump3.0040129A                      ;  不等则跳向失败

00401268  .  6A 00         push 0x0

0040126A  .  68 98114000   push dump3.00401198                      ;  ASCII "Success"

在jnz时将标志位Z改为1之后看到程序注册成功了,

然后直接将jnz修改为nop,

保存之后按照理论上来讲,应该爆破成功,此时无论输入任何用户名和注册码都应该会提示成功,但是当我们再次运行程序的时候发现

我们在到反汇编窗口发现代码刚刚爆破的jnz被还原了

看来是有代码SMC防爆自校验,防止爆破技术,回溯之前代码,发现在程序载入时又一个这样的call

跟进之后发现:

004012F3 /$  B8 66124000   mov eax,dump4.00401266

004012F8 |.  A3 90384000   mov dword ptr ds:[0x403890],eax

004012FD |.  8B18          mov ebx,dword ptr ds:[eax]

004012FF |.  66:81FB 753A  cmp bx,0x3A75

00401304 |.  74 41         je Xdump4.00401347

00401306 |.  68 94384000   push dump4.00403894                      ; /pOldProtect =dump4.00403894

0040130B |.  6A 40         push 0x40                                ; |NewProtect =PAGE_EXECUTE_READWRITE

0040130D |.  6A 10         push 0x10                                ; |Size = 10(16.)

0040130F |.  FF35 90384000 push dword ptrds:[0x403890]             ; |Address =NULL

00401315 |.  E8 2C030000   call<jmp.&kernel32.VirtualProtect>      ; \VirtualProtect

0040131A |.  A1 90384000   mov eax,dword ptr ds:[0x403890]

0040131F |.  BB 753A0000   mov ebx,0x3A75

00401324 |.  66:8918       mov word ptr ds:[eax],bx

00401327 |.  B8 6E124000   mov eax,dump4.0040126E

0040132C |.  A3 90384000   mov dword ptr ds:[0x403890],eax

00401331 |.  8B18          mov ebx,dword ptr ds:[eax]

00401333 |.  66:81FB 7532  cmp bx,0x3275

00401338 |.  74 0D         je Xdump4.00401347

0040133A |.  A1 90384000   mov eax,dword ptr ds:[0x403890]

0040133F |.  BB 75320000   mov ebx,0x3275

00401344 |.  66:8918       mov word ptr ds:[eax],bx

00401347 \>  C3            retn

原来在程序载入的时候会对上面两个关键的jnz进行检验,因为jnz机器码为7532如果发现被修改了就再次修改回去,这样就无法简单的nop成功了。

于是我们将这个函数也nop掉保存文件,再次注册时成功了


一路下来破解过程:

(注:在IAT修复之后之所以程序会变大,是因为脱壳的时候重建了输入表)



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值