【文章标题】: Radmin_Commu_Client 3.0 主程序去自校验
【文章作者】: CCDebuger
【软件名称】: Radmin Communication Client 3.0
【下载地址】: http://www.radmin.com/
【使用工具】: OD,PEiD,LordPE,ResScope
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
【详细过程】
没事在汉化新世纪逛的时候看到一个求解自校验的,说想汉化 Radmin Communication Client 3.0 这个软件,可主程序只要修改就会出错,无法运行。这个软件是开发远程控制软件 Remote Administrator 的公司出的产品,是这个公司最新出的产品 radmin communication server v3.0 中的客户端。这东西没用过,看介绍好像是用来在企业网内部聊天的。不管它,我这只看它是怎么自校验的。先用 PEiD 检测一下程序,显示为 Microsoft Visual C++ 6.0,呵,原来是无壳的。随便修改一下资源,保存后运行一下,跳出一个对话框:“Executable file is corrupted”,呵呵,当然出错了,要不然我干嘛?既然有对话框,我就先拦对话框。用 OD 载入,命令行中输入 bp MessageBoxA,F9运行,被拦下。ALT+F9 返回,听得一声出错时的响声,又被OD拦下。不管,F9运行,弹出出错对话框,点确定,又被OD拦下。现在再按 ALT+F9 返回,来到这里:
009AF6CD 68 44A8A400 PUSH 0A4A844 ; ASCII "Error"
009AF6D2 68 24A8A400 PUSH 0A4A824 ; ASCII "Executable file is corrupted"
009AF6D7 53 PUSH EBX
009AF6D8 FF15 FC23A300 CALL DWORD PTR DS:[A323FC] ; USER32.MessageBoxA
009AF6DE 8B4424 10 MOV EAX,DWORD PTR SS:[ESP+10] ; 我们返回的位置
009AF6E2 3BC3 CMP EAX,EBX
009AF6E4 895C24 0C MOV DWORD PTR SS:[ESP+C],EBX
009AF6DE 地址处就是我们返回的位置。看看这个代码的地址,感觉不对,地址是 009XXXXX,这是哪个的领空?一看OD的标题,竟然还是我们调试程序的领空!这怎么可能?这个程序的基址是01400000,怎么也不可能跑到这样的地址来啊。有问题,重新载入程序,打开 RUN 跟踪,添加所有函数入口,F9运行程序,等弹出出错对话框后按 F12 暂停程序,现在我们来看一下RUN跟踪的记录,在RUN跟踪记录中看一下主程序模块的最后一条指令:
RUN 跟踪, 选定行
返回=7.
线程=主
模块=Rcomclt
地址=01401189
=POP ESI
修改后的寄存器=ESI=00A56000
上面是复制出来的结果,在 OD 中我们见到的就是一条记录。现在在记录表格中双击这条,来到这里:
01401150 /$ 56 PUSH ESI
01401151 |. 8BF1 MOV ESI,ECX
01401153 |. 8B06 MOV EAX,DWORD PTR DS:[ESI]
01401155 |. 85C0 TEST EAX,EAX
01401157 |. 57 PUSH EDI
01401158 |. 8B3D 08B04001 MOV EDI,DWORD PTR DS:[<&KERNEL32.VirtualFree>] ; kernel32.VirtualFree
0140115E |. 74 0A JE SHORT Rcomclt.0140116A
01401160 |. 68 00800000 PUSH 8000 ; /FreeType = MEM_RELEASE
01401165 |. 6A 00 PUSH 0 ; |Size = 0
01401167 |. 50 PUSH EAX ; |Address
01401168 |. FFD7 CALL EDI ; \VirtualFree
0140116A |> 8B46 08 MOV EAX,DWORD PTR DS:[ESI+8]
0140116D |. 85C0 TEST EAX,EAX
0140116F |. C706 00000000 MOV DWORD PTR DS:[ESI],0
01401175 |. 74 0A JE SHORT Rcomclt.01401181
01401177 |. 68 00800000 PUSH 8000
0140117C |. 6A 00 PUSH 0
0140117E |. 50 PUSH EAX
0140117F |. FFD7 CALL EDI
01401181 |> 5F POP EDI
01401182 |. C746 08 00000000 MOV DWORD PTR DS:[ESI+8],0
01401189 |. 5E POP ESI ; 我们来到的地方
呵呵,这里竟然调用了 VirtualFree?我来查查函数参考,应该还有 VirtualAlloc。一查果然有,先在 VirtualAlloc 函数上全部设上断点,再在上面的01401150地址处设个断点,CTR+F2重新载入程序,F9运行,断下:
01401348 . FF15 0CB04001 CALL DWORD PTR DS:[<&KERNEL32.VirtualAlloc>] ; \断在这里
0140134E . 8945 E8 MOV DWORD PTR SS:[EBP-18],EAX ; 保存已分配内存的开始地址,我这是8D0000
01401351 . 837D E8 00 CMP DWORD PTR SS:[EBP-18],0
01401355 . 75 1E JNZ SHORT Rcomclt.01401375
01401357 . 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
01401359 . 68 18284101 PUSH Rcomclt.01412818 ; |Title = ""
0140135E . 68 D8004101 PUSH Rcomclt.014100D8 ; |err0
01401363 . 6A 00 PUSH 0 ; |hOwner = NULL
01401365 . FF15 A0B04001 CALL DWORD PTR DS:[<&USER32.MessageBoxA>] ; \MessageBoxA
0140136B . B8 01000000 MOV EAX,1
01401370 . E9 C3060000 JMP Rcomclt.01401A38
01401375 > 8B4D E8 MOV ECX,DWORD PTR SS:[EBP-18] ; 分配内存开始地址送ECX
01401378 . 894D EC MOV DWORD PTR SS:[EBP-14],ECX
0140137B . 8D55 BC LEA EDX,DWORD PTR SS:[EBP-44]
0140137E . 52 PUSH EDX
0140137F . 8B45 EC MOV EAX,DWORD PTR SS:[EBP-14] ; 分配内存开始地址送EAX
01401382 . 50 PUSH EAX
01401383 . 8B4D C0 MOV ECX,DWORD PTR SS:[EBP-40] ; 我这里是14353B4,原来是资源段2032项的VA
01401386 . 8B51 04 MOV EDX,DWORD PTR DS:[ECX+4] ; 资源段2032项的大小
01401389 . 8B4D DC MOV ECX,DWORD PTR SS:[EBP-24] ; 资源段2032项的VA送ECX
0140138C . 83C1 08 ADD ECX,8
0140138F . E8 2CFDFFFF CALL Rcomclt.014010C0
现在看到程序是读资源段中153下ID为2032资源中的内容。我感兴趣的就是程序分配的起始地址为 8D0000 的那块内存。我想看看它要干什么。现在在数据窗口中转到 8D0000 处,现在还都是空的,在这里设个内存写入断点,看看什么时候有东西写进来。设好后F9运行,断下:
01403520 |. 8806 |MOV BYTE PTR DS:[ESI],AL ; 断在这里
01403522 |. E9 AB020000 |JMP Rcomclt.014037D2 ; 来了个大跳
...
014037D2 |> \3B5424 38 |CMP EDX,DWORD PTR SS:[ESP+38] ; 看看信息窗口,原来还是读2032那个资源中的部分内容
014037D6 |. 73 50 |JNB SHORT Rcomclt.01403828
014037D8 |. 3B7424 3C |CMP ESI,DWORD PTR SS:[ESP+3C]
014037DC |. 73 4A |JNB SHORT Rcomclt.01403828
014037DE |.^ E9 9DFCFFFF \JMP Rcomclt.01403480 ; 又来个大跳,难道是跳大神?
...
01403480 |> /83FB 0F /CMP EBX,0F ; 与F比较,我哪知道它什么时候等于F?设个条件断点吧,省得我老按F8
01403483 |. |73 24 |JNB SHORT Rcomclt.014034A9
01403485 |. |33C0 |XOR EAX,EAX
01403487 |. |8A42 01 |MOV AL,BYTE PTR DS:[EDX+1] ; 2032资源段中的内容
0140348A |. |42 |INC EDX ; 指针加1
在01403480地址处设个条件断点,按SHIFT+F2,输入 ebx==0F,现在F9运行,断下,EBX值为F。F8再看看,还在这段代码里跳?晕。不看了,这么多东西看完要得颈椎病。管它,就是解密2032的资源写到分配的内存地址中嘛,CTR+F9返回。这个返回时间可够长的,半天才来到这:
0140387A \. C3 RETN F8单步一下,来到这:
014046A2 |. 8B4424 14 |MOV EAX,DWORD PTR SS:[ESP+14] ; 返回到这
014046A6 |. 8B48 0C |MOV ECX,DWORD PTR DS:[EAX+C]
014046A9 |. 8B50 10 |MOV EDX,DWORD PTR DS:[EAX+10]
014046AC |. 8B38 |MOV EDI,DWORD PTR DS:[EAX]
014046AE |. 8B40 04 |MOV EAX,DWORD PTR DS:[EAX+4]
014046B1 |. 8B5E 30 |MOV EBX,DWORD PTR DS:[ESI+30]
014046B4 |. 8B6E 34 |MOV EBP,DWORD PTR DS:[ESI+34]
014046B7 |. 894C24 2C |MOV DWORD PTR SS:[ESP+2C],ECX
014046BB |. 895424 24 |MOV DWORD PTR SS:[ESP+24],EDX
014046BF |. 897C24 1C |MOV DWORD PTR SS:[ESP+1C],EDI
014046C3 |. 894424 10 |MOV DWORD PTR SS:[ESP+10],EAX
014046C7 |. 895C24 18 |MOV DWORD PTR SS:[ESP+18],EBX
014046CB |. E9 13060000 |JMP Rcomclt.01404CE3 ; 又是一个大跳
乖乖,后面还有一长串代码,坚决不看了,珍爱生命,远离破解!F9运行,断在 VirtualAlloc 函数上。
014014C2 . FF15 0CB04001 CALL DWORD PTR DS:[<&KERNEL32.VirtualAlloc>] ; \VirtualAlloc
014014C8 . A3 10284101 MOV DWORD PTR DS:[1412810],EAX ; 这次分配了个起始地址9A0000的
向下看看:
0140154D . 66:C702 4D5A MOV WORD PTR DS:[EDX],5A4D ; 呵,MZ,看样子它不在内存中整出一个PE文件出来是不罢休啊
01401552 . 8B45 E4 MOV EAX,DWORD PTR SS:[EBP-1C]
不管,F9,又断在 VirtualAlloc 上:
014015BE . FF15 0CB04001 CALL DWORD PTR DS:[<&KERNEL32.VirtualAlloc>] ; \VirtualAlloc
014015C4 . 85C0 TEST EAX,EAX ; 内存地址9A1000,.text段
F9,又断在上面的位置,这次分配的内存地址是00A32000了。再F9,还是断在上面的位置,分配的内存地址变为00A4A000。F9,第三次断,分配地址00A56000。继续F9,断:
014010E2 |. 8B3D 0CB04001 MOV EDI,DWORD PTR DS:[<&KERNEL32.VirtualAlloc>] ; 断在这
014010E8 |. 6A 40 PUSH 40 ; /Protect = PAGE_EXECUTE_READWRITE
014010EA |. 68 00100000 PUSH 1000 ; |AllocationType = MEM_COMMIT
014010EF |. 8BF1 MOV ESI,ECX ; |
014010F1 |. 68 00100000 PUSH 1000 ; |Size = 1000 (4096.)
014010F6 |. 6A 00 PUSH 0 ; |Address = NULL
014010F8 |. C746 04 0000000>MOV DWORD PTR DS:[ESI+4],0 ; |
014010FF |. C746 0C 0000000>MOV DWORD PTR DS:[ESI+C],0 ; |
01401106 |. FFD7 CALL EDI ; \VirtualAlloc
01401108 |. 85C0 TEST EAX,EAX ; 地址00A60000
管它,让它去解密代码去,继续F9,来到这:
01401150 /$ 56 PUSH ESI
01401151 |. 8BF1 MOV ESI,ECX
01401153 |. 8B06 MOV EAX,DWORD PTR DS:[ESI]
01401155 |. 85C0 TEST EAX,EAX
01401157 |. 57 PUSH EDI
01401158 |. 8B3D 08B04001 MOV EDI,DWORD PTR DS:[<&KERNEL32.VirtualFree>] ; kernel32.VirtualFree
0140115E |. 74 0A JE SHORT Rcomclt.0140116A
01401160 |. 68 00800000 PUSH 8000 ; /FreeType = MEM_RELEASE
01401165 |. 6A 00 PUSH 0 ; |Size = 0
01401167 |. 50 PUSH EAX ; |起始地址A60000处
01401168 |. FFD7 CALL EDI ; \VirtualFree
原来释放了一块内存,后面我们没断点了,不要瞎来,F8慢慢跟:
01401177 |. 68 00800000 PUSH 8000
0140117C |. 6A 00 PUSH 0
0140117E |. 50 PUSH EAX ; 内存地址00A70000
0140117F |. FFD7 CALL EDI ; 调用VirtualFree函数
01401181 |> 5F POP EDI
01401182 |. C746 08 00000000 MOV DWORD PTR DS:[ESI+8],0
01401189 |. 5E POP ESI ; 我们来到的地方
0140118A \. C3 RETN
看了00A70000处的数据,原来是 KERNEL32.DLL 什么的,呵呵,毁尸灭迹啊!经过上面那个 RETN,我们来到这:
01401909 . C745 E0 00000000 MOV DWORD PTR SS:[EBP-20],0 ; 来到这里
01401910 . EB 09 JMP SHORT Rcomclt.0140191B
01401912 > 8B4D E0 MOV ECX,DWORD PTR SS:[EBP-20]
01401915 . 83C1 01 ADD ECX,1
01401918 . 894D E0 MOV DWORD PTR SS:[EBP-20],ECX
0140191B > 8B55 E0 MOV EDX,DWORD PTR SS:[EBP-20]
0140191E . 3B55 AC CMP EDX,DWORD PTR SS:[EBP-54] ; .text段
01401921 . 0F83 C4000000 JNB Rcomclt.014019EB
01401927 . BA 38004101 MOV EDX,Rcomclt.01410038 ; mycol
0140192C . 8B45 E0 MOV EAX,DWORD PTR SS:[EBP-20]
0140192F . 6BC0 18 IMUL EAX,EAX,18
01401932 . 8B4D A8 MOV ECX,DWORD PTR SS:[EBP-58] ; 段名称
01401935 . 03C8 ADD ECX,EAX
01401937 . E8 14010000 CALL Rcomclt.01401A50 ; 这里把段名称与上面的mycol比较
再来到下面:
0140198B > /8B55 F4 MOV EDX,DWORD PTR SS:[EBP-C]
0140198E . |83C2 01 ADD EDX,1
01401991 . |8955 F4 MOV DWORD PTR SS:[EBP-C],EDX
01401994 > |8B45 F4 MOV EAX,DWORD PTR SS:[EBP-C]
01401997 > . |3B85 A8FAFFFF CMP EAX,DWORD PTR SS:[EBP-558] ; 代码段大小,24CC
0140199D . |73 45 JNB SHORT Rcomclt.014019E4
0140199F . |8B4D F4 MOV ECX,DWORD PTR SS:[EBP-C]
014019A2 . |8B15 14284101 MOV EDX,DWORD PTR DS:[1412814] ; 内存地址00A56000
014019A8 . |8B048A MOV EAX,DWORD PTR DS:[EDX+ECX*4]
014019AB . |0305 10284101 ADD EAX,DWORD PTR DS:[1412810] ; 内存地址009A0000
014019B1 . |8B4D F4 MOV ECX,DWORD PTR SS:[EBP-C]
014019B4 . |8B15 14284101 MOV EDX,DWORD PTR DS:[1412814]
014019BA . |89048A MOV DWORD PTR DS:[EDX+ECX*4],EAX
014019BD . |8B45 F4 MOV EAX,DWORD PTR SS:[EBP-C]
014019C0 . |8B0D 14284101 MOV ECX,DWORD PTR DS:[1412814]
014019C6 . |8B1481 MOV EDX,DWORD PTR DS:[ECX+EAX*4]
014019C9 . |8995 A4FAFFFF MOV DWORD PTR SS:[EBP-55C],EDX
014019CF . |8B85 A4FAFFFF MOV EAX,DWORD PTR SS:[EBP-55C]
014019D5 . |8B08 MOV ECX,DWORD PTR DS:[EAX]
014019D7 . |034D D8 ADD ECX,DWORD PTR SS:[EBP-28]
014019DA . |8B95 A4FAFFFF MOV EDX,DWORD PTR SS:[EBP-55C]
014019E0 . |890A MOV DWORD PTR DS:[EDX],ECX
014019E2 .^\EB A7 JMP SHORT Rcomclt.0140198B
不管它,我知道它是往内存地址00A56000开始处的空间写东西就行了,要用到这里我再研究。在01401997地址那条指令上设个条件断点,EAX==24CC,F9继续,条件断点断下后,我们F8,到这:
01401A0E > \68 00800000 PUSH 8000 ; /FreeType = MEM_RELEASE
01401A13 . 6A 00 PUSH 0 ; |Size = 0
01401A15 . 8B4D E8 MOV ECX,DWORD PTR SS:[EBP-18] ; |
01401A18 . 51 PUSH ECX ; |Address = 008D0000
01401A19 . FF15 08B04001 CALL DWORD PTR DS:[<&KERNEL32.VirtualFree>] ; \又要毁尸灭迹了
01401A1F . 8B15 10284101 MOV EDX,DWORD PTR DS:[1412810] ; 9A0000,解密后代码基址,以后要用
01401A25 . 0395 D8FEFFFF ADD EDX,DWORD PTR SS:[EBP-128]
01401A2B . 8955 B8 MOV DWORD PTR SS:[EBP-48],EDX
01401A2E . 8B45 B8 MOV EAX,DWORD PTR SS:[EBP-48]
01401A31 .- FFE0 JMP EAX ; 我这EAX是00A1D855,要跳到解完密的代码那执行了
继续F8,一看全是PE文件初始化的一些代码。向下翻翻代码,F4到这:
00A1D92F 50 PUSH EAX ; F4运行到这,看看还要干什么
00A1D930 E8 3B18F9FF CALL 009AF170 ; 跟进去
F4到 00A1D92F 的时候发现 EAX 就是原程序的镜像基址01400000,跟进00A1D930地址处的那个CALL,看看干了些什么,来到下面:
009AF1EB 68 F4A5A400 PUSH 0A4A5F4 ; UNICODE "*.lng_rcc"
009AF1F0 68 8EA00000 PUSH 0A08E ; 要开始判断是否有语言文件了
继续,到这:
009AF263 E8 A8F60000 CALL 009BE910
009AF268 85C0 TEST EAX,EAX
009AF26A 0F84 58040000 JE 009AF6C8 ; 这里一跳就完蛋了,EAX不能为0
在009AF268地址处这条指令检测EAX时,我先修改一下寄存器,让EAX等于1,下面那条指令它肯定不会跳的。继续,来到这:
009AF27F E8 BCFA0000 CALL 009BED40
009AF284 85C0 TEST EAX,EAX
009AF286 0F84 3C040000 JE 009AF6C8 ; 这里一跳也完蛋,EAX不能为0
同样在执行到009AF284地址处的指令时把EAX改为1,顺便看一下地址 009AF6C8 处都有什么东西:
009AF6C8 68 30200000 PUSH 2030
009AF6CD 68 44A8A400 PUSH 0A4A844 ; ASCII "Error"
009AF6D2 68 24A8A400 PUSH 0A4A824 ; ASCII "Executable file is corrupted"
009AF6D7 53 PUSH EBX
009AF6D8 FF15 FC23A300 CALL DWORD PTR DS:[A323FC] ; USER32.MessageBoxA
呵呵,到这就歇菜了。当场给你个出错对话框。我们上面都改了标志位,现在肯定不会跳到这了。分析过原程序,这里的EAX都是1。F9运行一下,呵呵,现在正常了。到此就要考虑了,不可能我每次都挂个调试器来修改标志位让程序运行啊。做个 Loader?人家运行你的汉化版之前还要先运行 Loader 来修改内存,多郁闷啊!还有种办法就是把解密算法全部搞清楚,写个程序,把解密后的代码修改了再加密回去,替换原来的资源。这。。。这也太让人抓狂了吧?这活我不干。还是找个简单的方法吧,我在程序跳到内存中已解密后的代码执行之前,让程序先把解密后的代码按我的要求修改一下,然后再让程序去执行,这样行吧?我只要把原来那两条 TEST EAX,EAX 指令改为 INC EAX 就行了,当然后面还要补个 NOP 来补足代码长度。INC EAX 再加上 NOP 机器码是 4090。现在剩下的问题就是在哪里接管程序,在什么地方写我们的补丁代码了。我现在先考虑这一段代码:
01401A1F . 8B15 10284101 MOV EDX,DWORD PTR DS:[1412810] ; 9A0000,解密后代码镜像基址,以后要用
01401A25 . 0395 D8FEFFFF ADD EDX,DWORD PTR SS:[EBP-128]
01401A2B . 8955 B8 MOV DWORD PTR SS:[EBP-48],EDX
01401A2E . 8B45 B8 MOV EAX,DWORD PTR SS:[EBP-48]
01401A31 .- FFE0 JMP EAX ; 我这EAX是00A1D855,要跳到解完密的代码那执行了
这里是在跳到在内存中解密后的代码执行前我最后一面见到的主程序。舍不得啊!就在这我们交流一下吧。现在先到主程序中找块空地,我用个小工具 Topo 1.2,在地址 0140A4B2 处找了块空地,现在开始 patch:
01401A1F . 8B15 10284101 MOV EDX,DWORD PTR DS:[1412810] ; 9A0000,解密后代码镜像基址
01401A25 . 0395 D8FEFFFF ADD EDX,DWORD PTR SS:[EBP-128]
01401A2B E9 828A0000 JMP Rcomclt.0140A4B2 ; 跳到我们要打补丁的地方
01401A30 90 NOP
01401A31 . FFE0 JMP EAX
补丁代码:
0140A4B2 60 PUSHAD ; 保护现场
0140A4B3 2B95 D8FEFFFF SUB EDX,DWORD PTR SS:[EBP-128] ; 获取解密后代码镜像基址
0140A4B9 C682 68F20000 40 MOV BYTE PTR DS:[EDX+F268],40 ; 下面四条指令是修改内存中的代码
0140A4C0 C682 69F20000 90 MOV BYTE PTR DS:[EDX+F269],90
0140A4C7 C682 84F20000 40 MOV BYTE PTR DS:[EDX+F284],40
0140A4CE C682 85F20000 90 MOV BYTE PTR DS:[EDX+F285],90
0140A4D5 61 POPAD ; 恢复现场
0140A4D6 8955 B8 MOV DWORD PTR SS:[EBP-48],EDX ; 恢复前面我们修改的指令
0140A4D9 8B45 B8 MOV EAX,DWORD PTR SS:[EBP-48]
0140A4DC ^ E9 5075FFFF JMP Rcomclt.01401A31 ; 返回原位置继续执行
patch 完后在OD中右键选择复制到可执行文件->所有修改,保存为 Rcomclt1.exe 文件。运行一下,呵呵,一切OK!
【经验总结】
这个软件的自校验真是别具一格,把可执行代码加密后放在资源中,运行的时候再动态在内存中解密出代码执行,整个实现的就是个加密壳的功能,而且你又不好脱出来。解密的代码不光含有自校验部分,还有程序初始化及其它的一大堆代码,你还没法禁用它。这种方法还是值得学习的。
【版权声明】: 本文纯属技术交流, 转载请注明作者并保持文章的完整, 谢谢!
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)