• 标 题:Visual Protect3.0脱壳 (16千字)
  • 作 者:hying
  • 时 间:2001-6-10 11:29:53
  • 链 接:http://bbs.pediy.com

标题:Visual Protect3.0脱壳讨论
所用工具:trw2000,peditor1.7
研究对象:easypdf1.5.2
前言:
    Visual Protect3.0是一款不错的加密软件,它可以给你的共享软件提供如使用总时间限制、使用总日期限制,使用次数限制、过期限制等,还可以定制试用版提示框。如果用户注册的话只需要利用网络传送一个小于1K的keyfile即可去除所有限制。在它的帮助里说:Once a file or program has been protected with Visual Protect and distributed to your customers, its protection CANNOT BE REMOVED。在这点上它好象有些太自信了。下面我们就来看看如何去除它的保护。
保护原理:
    利用Visual Protect保护一个软件时,它会在被保护软件同目录下放一个vp.dll,再对被保护软件加一个外壳,利用这个外壳调用vp.dll,利用vp.dll对授权文件*.vpl进行验证,验证通过后对程序解密运行。可见破解的方法可以有两种,一种是对被保护程序进行脱壳,一般脱壳后就不再会有使用日期等限制,而且可以脱离vp.dll运行。第二种是对vp.dll的破解。利用同一版本Visual Protect加密的软件的vp.dll是相同的,一旦破解了某个版本的vp.dll,那所有利用这个版本的Visual Protect保护的软件的加密都会失效。
破解对象简介:
easypdf据说是一款不错的所见即所得的pdf文件制作工具(很遗憾它似乎不支持中文,用它制作的中文文档会显示乱码,我还没找到解决办法,修改过W32DASM的同志可以看看)。它与Visual Protect是同一家软件公司编写的,所以它加密用的Visual Protect应该是最典型的。安装后观察有3个文件是用Visual Protect加密的,分别是easypdf.exe、vspdf.dll和vp.dll(他们好象真的很自信,这么重要的文件都用它来加密),这三个文件的脱壳都不完全相同,下面我们就分别讨论它们的脱壳方法。
脱壳实战:
一、easypdf.exe的脱壳
运行trw2000,运行命令faults off,用trw2000载入easypdf.exe,先出现一个试用提示框,按下'try'后会中断在easypdf.exe的入口6B5BC0,往下走有4个CALL,在第四个CALL按F8进入,进入后按F10一直往下走,直到:
0167:006B6C4A 8D4C2424        LEA      ECX,[ESP+24]
0167:006B6C4E 50              PUSH    EAX
0167:006B6C4F 8D54242C        LEA      EDX,[ESP+2C]
0167:006B6C53 51              PUSH    ECX
0167:006B6C54 52              PUSH    EDX
0167:006B6C55 FF1574AB6B00    CALL    `VP![NONAME]`    <-注意
0167:006B6C5B 8B442414        MOV      EAX,[ESP+14]
0167:006B6C5F 85C0            TEST    EAX,EAX
0167:006B6C61 740F            JZ      006B6C72
0167:006B6C63 8B0DD8AB6B00    MOV      ECX,[006BABD8]
0167:006B6C69 03C1            ADD      EAX,ECX
0167:006B6C6B 81C464030000    ADD      ESP,0364
0167:006B6C71 C3              RET   
在6B6C55处按F8进入,运行命令D 401000,你将会看到很多的"?",不管它,一直用F10往下走,直到:
0167:008AD84D  MOV      EAX,[008B35F8]
0167:008AD852  CALL    0088CDDC
0167:008AD857  MOV      EAX,[EBP+08]
0167:008AD85A  CALL    NEAR [EAX]        <-注意
0167:008AD85C  MOV      EAX,[008B35AC]
0167:008AD861  MOV      EAX,[EAX]
经过8AD85A处的CALL后,你会发现原来显示"?"的地方可以看到内容了,所以这个CALL应该是对程序进行了解码,再往下走,直到:
0167:008AD909  MOV      EAX,008ACEC8
0167:008AD90E  CALL    008AE198
0167:008AD913  CMP      EAX,[008B66B0]
0167:008AD919  JZ      008AD92D
0167:008AD91B  MOV      EAX,[008B357C]
0167:008AD920  MOV      BYTE [EAX],00
0167:008AD923  MOV      EAX,[008B35F8]
0167:008AD928  CALL    0088CDDC
0167:008AD92D  JMP      NEAR [EBP-04]        <-注意
0167:008AD930  PUSH    BYTE +00
0167:008AD932  CALL    00836788
0167:008AD937  JMP      008ADD31
0167:008AD93C  MOV      EAX,[008B3824]
0167:008AD941  MOV      EAX,[EAX+41]
注意,程序将由上面的JMP跳到原始的入口67B9E0,现在你可以把它DUMP下来,DUMP完后用F8慢慢走,可到如下地方:
0167:00407DF4  JMP      NEAR [0068C304]
0167:00407DFA  MOV      EAX,EAX
0167:00407DFC  JMP      NEAR [0068C300]
0167:00407E02  MOV      EAX,EAX
0167:00407E04  JMP      NEAR [0068C2FC]
0167:00407E0A  MOV      EAX,EAX
0167:00407E0C  JMP      NEAR [0068C2F8]
根据经验,可以判断68C300前后应该是输入表的所在,再上下看看,看到如下:
0030:0068C1E0 00 00 00 00 00 00 00 00-00 00 00 00 EA F4 28 00 ............牯(.
0030:0068C1F0 84 CB 28 00 00 00 00 00-00 00 00 00 00 00 00 00 勊(.............
0030:0068C200 00 00 00 00 00 00 00 00-88 F7 6E 01 A0 AE 6E 01 ........堶n.牣n.
0030:0068C210 DC B4 6E 01 FC 7E 73 01-08 7F 73 01 5C 7F 73 01 艽n.鼅s..s.\s.
……………………
0030:0068CBD0 6C 76 7E 00 54 76 7E 00-38 76 7E 00 44 74 7E 00 lv~.Tv~.8v~.Dt~.
0030:0068CBE0 50 70 7E 00 74 70 7E 00-28 72 7E 00 F4 8D 7E 00 Pp~.tp~.(r~.魨~.
0030:0068CBF0 00 00 00 00 6B 65 72 6E-65 6C 33 32 2E 64 6C 6C ....kernel32.dll
0030:0068CC00 00 00 00 00 12 EA A3 E1-C7 13 22 14 43 51 31 25 .....辏崆.".CQ1%
根据经验判断输入地址表是从68C200到68CBF0,其中的016EF788等都是被加密的函数入口地址,例如:程序通过JMP  NEAR [0068C208]调用某个函数,[0068C208]中应存放有这个函数的入口地址,现在被加密放入了016EF788,程序跳到16EF788处的代码为:
0167:016EF788  MOV      EAX,009311E8
0167:016EF78D  JMP      EAX
程序将跳到9311E8,9311E8处的代码为:
0167:009311E8  MOV      EAX,BFF816F3
0167:009311ED  JMP      EAX
程序将跳到BFF816F3,BFF816F3处的代码为:
KERNEL32!GetCurrentThreadId
0167:BFF816F3  MOV      EAX,[BFFCADE0]
0167:BFF816F8  PUSH    DWORD [EAX]
0167:BFF816FA  CALL    BFF80455
所以JMP  NEAR [0068C208]调用的应该是KERNEL32!GetCurrentThreadId函数,[0068C208]中放的本应是BFF816F3。原理很简单,下面就是如何修复了。可是我很遗憾的发现常用的修复输入表的工具对它都失效了。看样子我们得另想办法。一种是我们先象刚才一样通过手动将正确的地址逐个写入,写入以后再利用工具完全修复,但是这样太费时间了,估计一天都可能干不完。看来得另想方法。另一个方法是考虑如何让它不加密。刚才我们已经讲了008AD85A处的CALL [EAX]是对程序进行解码,那输入表的加密肯定是再这个之后,所以再次运行,通过这个CALL后我们下命令:
bpx GetProcAddress
然后G,停下来后用F12走几次返回,看到如下代码:
0167:008C5BEA  PUSH    DWORD [EBP+0C]
0167:008C5BED  PUSH    ECX
0167:008C5BEE  CALL    `KERNEL32!GetProcAddress`
0167:008C5BF4  PUSH    BYTE +08        <-返回到这
0167:008C5BF6  PUSH    BYTE +00
0167:008C5BF8  MOV      [EBP+0C],EAX
0167:008C5BFB  CALL    `KERNEL32!LocalAlloc`
0167:008C5C01  MOV      ESI,EAX
0167:008C5C03  LEA      EAX,[EBP+0C]
0167:008C5C06  PUSH    BYTE +04
0167:008C5C08  PUSH    EAX
0167:008C5C09  LEA      EAX,[ESI+01]
0167:008C5C0C  MOV      BYTE [ESI],B8
0167:008C5C0F  PUSH    EAX
0167:008C5C10  CALL    008C6320
0167:008C5C15  ADD      ESP,BYTE +0C
0167:008C5C18  OR      BYTE [ESI+05],FF
0167:008C5C1C  MOV      BYTE [ESI+06],E0
0167:008C5C20  MOV      EAX,ESI
0167:008C5C22  POP      ESI
0167:008C5C23  POP      EBP
0167:008C5C24  RET   
返回时EAX中放的就是正确的入口地址。按F12两次后走到:
0167:0089EEB2  CALL    00836858
0167:0089EEB7  CALL    0089EDF0    <-到这,注意这个CALL
0167:0089EEBC  POP      ECX
0167:0089EEBD  MOV      EAX,[EBP+08]
0167:0089EEC0  MOV      EBX,[EAX-04]    <-注意此时的EBX的值
0167:0089EEC3  MOV      EAX,[EBP+08]
0167:0089EEC6  MOV      ECX,[EAX-04]
0167:0089EEC9  MOV      EDX,[EBP-04]
0167:0089EECC  MOV      EAX,EDI
0167:0089EECE  MOV      ESI,[EAX]
0167:0089EED0  CALL    NEAR [ESI+38]
0167:0089EED3  XOR      EAX,EAX
走几圈后你会发现规律,就是上面取出的EBX的值就是最后放入输入表的值,那这个值是如何产生的呢?进入上面的CALL看个究竟:
0167:0089EDF0  PUSH    EBP
0167:0089EDF1  MOV      EBP,ESP
0167:0089EDF3  PUSH    ECX        <-入栈,此时ECX的值就是正确的人口地址
0167:0089EDF4  MOV      [EBP-04],EAX    <-破坏堆栈内的正确人口地址
0167:0089EDF7  MOV      EAX,07
0167:0089EDFC  CALL    0083279C
0167:0089EE01  MOV      EDX,[EBP+08]
0167:0089EE04  MOV      [EDX-04],EAX    <-保存加密后的入口地址
0167:0089EE07  MOV      EAX,[EBP+08]
0167:0089EE0A  MOV      EAX,[EAX-04]
0167:0089EE0D  MOV      BYTE [EAX],B8
0167:0089EE10  MOV      EAX,[EBP+08]
0167:0089EE13  MOV      EAX,[EAX-04]
0167:0089EE16  LEA      EDX,[EAX+01]
0167:0089EE19  LEA      EAX,[EBP-04]
0167:0089EE1C  MOV      ECX,04
0167:0089EE21  CALL    008328B4
0167:0089EE26  MOV      EAX,[EBP+08]
0167:0089EE29  MOV      EAX,[EAX-04]
0167:0089EE2C  MOV      BYTE [EAX+05],FF
0167:0089EE30  MOV      EAX,[EBP+08]
0167:0089EE33  MOV      EAX,[EAX-04]
0167:0089EE36  MOV      BYTE [EAX+06],E0
0167:0089EE3A  POP      ECX
0167:0089EE3B  POP      EBP
0167:0089EE3C  RET   
我们可以在内存中对这些代码稍做修改,如下:
0167:0089EDF0 55              PUSH    EBP
0167:0089EDF1 8BEC            MOV      EBP,ESP
0167:0089EDF3 51              PUSH    ECX        <-入栈,此时ECX的值就是正确的人口地址
0167:0089EDF4 90              NOP                <-去掉了破坏刚入栈的正确地址的代码
0167:0089EDF5 90              NOP   
0167:0089EDF6 90              NOP   
0167:0089EDF7 B807000000      MOV      EAX,07
0167:0089EDFC E89B39F9FF      CALL    0083279C
0167:0089EE01 8B5508          MOV      EDX,[EBP+08]
0167:0089EE04 8942FC          MOV      [EDX-04],EAX
0167:0089EE07 8B4508          MOV      EAX,[EBP+08]
0167:0089EE0A 8B40FC          MOV      EAX,[EAX-04]
0167:0089EE0D C600B8          MOV      BYTE [EAX],B8
0167:0089EE10 8B4508          MOV      EAX,[EBP+08]
0167:0089EE13 8B40FC          MOV      EAX,[EAX-04]
0167:0089EE16 8D5001          LEA      EDX,[EAX+01]
0167:0089EE19 8D45FC          LEA      EAX,[EBP-04]
0167:0089EE1C B904000000      MOV      ECX,04
0167:0089EE21 E88E3AF9FF      CALL    008328B4
0167:0089EE26 8B4508          MOV      EAX,[EBP+08]
0167:0089EE29 59              POP      ECX        <-取出正确的地址
0167:0089EE2A 8948FC          MOV      [EAX-04],ECX    <-替换掉被加密的入口地址
0167:0089EE2D 5D              POP      EBP
0167:0089EE2E C3              RET   
这样修改后放入输入表的就将是未被加密的函数入口地址。
运行起来后我们就可以用Import REConstructor来进行输入表的完全修复,具体操作可看相关文章。须注意的是修复时发现还有两个函数的入口地址不对。仔细研究发现它们对应的都是函数KERNEL32!GetProcAddress,需手工选择一下,再将程序的入口修正为正确入口就可以了。至此,easypdf.exe脱壳完成。
二、vspdf.dll的脱壳
dll文件的脱壳一般较少被提到,开始我也没有注意,只是因为发现在easypdf.exe运行前已经出现一个试用框,然后寻找原因才发现是vspdf.dll被加壳了。下面我们就来看如何脱它。
要脱壳先得找到它的入口点,但是dll文件被载入的基址并不是我们静态观察文件头看到的基址,那如何找到他的入口呢?我们可以用peditor1.7来找,先按"browse"按钮打开vspdf.dll,取得它的入口偏移为6CBC0,再运行easypdf,运行好后按"tasks"按钮,在上半窗口中点easypdf.exe,然后可在下半窗口中找到vspdf.dll的基址为7B0000,所以正确入口应该是81CBC0。
运行trw2000,下命令faults off和bp 81CBC0。运行easypdf,中断后我们可看到如下代码:
0167:0081CBC0  MOV      EAX,[ESP+08]
0167:0081CBC4  CMP      EAX,BYTE +01
0167:0081CBC7  JNZ      0081CC04
0167:0081CBC9  MOV      [00821B9C],EAX
0167:0081CBCE  MOV      EAX,[ESP+04]
0167:0081CBD2  PUSH    EAX
0167:0081CBD3  MOV      [00821BD8],EAX
0167:0081CBD8  CALL    0081D540        <-进入
0167:0081CBDD  ADD      ESP,BYTE +04
0167:0081CBE0  MOV      [00820704],EAX
0167:0081CBE5  TEST    EAX,EAX
0167:0081CBE7  JNZ      0081CBEC
0167:0081CBE9  RET      0C
0167:0081CBEC  MOV      ECX,[ESP+0C]
0167:0081CBF0  MOV      EDX,[00821BD8]
0167:0081CBF6  PUSH    ECX
0167:0081CBF7  PUSH    BYTE +01
0167:0081CBF9  PUSH    EDX
0167:0081CBFA  MOV      [00820700],EAX
0167:0081CBFF  CALL    EAX            <-注意此时EAX的值
0167:0081CC01  RET      0C
0167:0081CC04  MOV      ECX,[ESP+0C]
0167:0081CC08  MOV      EDX,[00821BD8]
0167:0081CC0E  PUSH    ECX
0167:0081CC0F  PUSH    EAX
0167:0081CC10  PUSH    EDX
0167:0081CC11  CALL    NEAR [00820700]
0167:0081CC17  RET      0C
第一个JNZ是不跳的,我们进入81CBD8的CALL,然后就跟easypdf.exe查不多了。不同的是对输入表加密完后继续走会回到81CBDD,那哪是正确的入口呢?我们继续往下走,走到81CBFF的CALL  EAX,然后发现此时EAX的值为7B1000,这不是正确入口吗?好了在入口处我们可以DUMP它,不过发现用Procdump无法DUMP,但是peditor可以,DUMP下来后再按上面的方法修复输入表,修复时发现一个奇怪的现象,就是vspdf.dll的输入地址表有两份,交错地放在一起,不知道为什么?其中一份已经初始化好了,我们只要把这一份修复就可以了。还有一个问题就是Import REConstructor修复的vspdf.dll的输入表变得让我看不懂了。我竟然找不到函数名,但是却可以初始化,谁能帮我解释一下。
三、vp.dll的脱壳
可运用上面相同的方法找到它的现在的人口地址和原来的入口地址。不同的是第一个CALL进入后的流程与前面不同,当然了,因为这时候vp.dll还没有完全运行。可以看到如下代码:
0167:008C62CE  MOV      EAX,[ESP+04]
0167:008C62D2  MOV      [008CAB5C],EAX
0167:008C62D7  CALL    008C5E2D
0167:008C62DC  CALL    008C6085
0167:008C62E1  CALL    008C5A47
0167:008C62E6  MOV      EAX,[008CAB5C]
0167:008C62EB  CMP      EAX,[008CA7C5]
0167:008C62F1  JZ      008C6301
0167:008C62F3  CMP      DWORD [008CA7BD],BYTE +00
0167:008C62FA  JZ      008C6301
0167:008C62FC  CALL    008C5AA5
0167:008C6301  CALL    008C5CAF
0167:008C6306  MOV      EAX,[008CAB5C]
0167:008C630B  MOV      ECX,[008CA7B9]
0167:008C6311  ADD      EAX,ECX
0167:008C6313  RET   
我们可以走到8C6301后下bpx GetProcAddress找到关键的输入表加密处。走几圈你会找到规律。关键代码如下:
0167:008C5E03  PUSH    EAX
0167:008C5E04  PUSH    EBP
0167:008C5E05  CALL    008C5C25        <-关键的CALL
0167:008C5E0A  POP      ECX
0167:008C5E0B  POP      ECX
0167:008C5E0C  CMP      EAX,EDI
0167:008C5E0E  MOV      [ESI],EAX        <-将被加密的入口地址放入
0167:008C5E10  JNZ      008C5E18
0167:008C5E12  MOV      DWORD [ESI],DEADBEEF    <-将被加密的入口地址放入
0167:008C5E18  ADD      ESI,BYTE +04
0167:008C5E1B  JMP      SHORT 008C5DDA
我们可以参照前面的改法修改,但是这儿我们可以尝试另一种方法。你可能注意到了,在现在的输入表里,函数名部分还是被加密的,我们能否完全修复它呢?
进入上面的CALL看一下:

我把它改成了这样:
0167:008C5C25 55              PUSH    EBP
0167:008C5C26 8BEC            MOV      EBP,ESP
0167:008C5C28 81EC00010000    SUB      ESP,0100
0167:008C5C2E 8B450C          MOV      EAX,[EBP+0C]
0167:008C5C31 56              PUSH    ESI
0167:008C5C32 0FB630          MOVZX    ESI,BYTE [EAX]
0167:008C5C35 40              INC      EAX
0167:008C5C36 50              PUSH    EAX
0167:008C5C37 56              PUSH    ESI
0167:008C5C38 50              PUSH    EAX
0167:008C5C39 48              DEC      EAX
0167:008C5C3A 90              NOP   
0167:008C5C3B 90              NOP   
0167:008C5C3C 90              NOP   
0167:008C5C3D 90              NOP   
0167:008C5C3E 50              PUSH    EAX
0167:008C5C3F E8DC060000      CALL    008C6320
0167:008C5C44 80243000        AND      BYTE [EAX+ESI],00
0167:008C5C48 90              NOP   
0167:008C5C49 90              NOP   
0167:008C5C4A 90              NOP   
0167:008C5C4B 90              NOP   
0167:008C5C4C 6A00            PUSH    BYTE +00
0167:008C5C4E 90              NOP   
0167:008C5C4F 90              NOP   
0167:008C5C50 90              NOP   
0167:008C5C51 90              NOP   
0167:008C5C52 90              NOP   
0167:008C5C53 90              NOP   
0167:008C5C54 56              PUSH    ESI
0167:008C5C55 50              PUSH    EAX
0167:008C5C56 E815000000      CALL    008C5C70
0167:008C5C5B 83C418          ADD      ESP,BYTE +18
0167:008C5C5E 58              POP      EAX
0167:008C5C5F 48              DEC      EAX
0167:008C5C60 90              NOP   
0167:008C5C61 90              NOP   
0167:008C5C62 90              NOP   
0167:008C5C63 90              NOP   
0167:008C5C64 50              PUSH    EAX
0167:008C5C65 FF7508          PUSH    DWORD [EBP+08]
0167:008C5C68 E8DCFEFFFF      CALL    008C5B49
0167:008C5C6D 5E              POP      ESI
0167:008C5C6E C9              LEAVE 
0167:008C5C6F C3              RET   
你对照着走一偏就能明白它的意思。最后还要把8C5E0E和8C5E12处的两句NOP掉,让它不初始化。然后返回到程序的真正入口。这时候的输入表就是完全被解密的未初始化的输入表,接下来就可以DUMP了,DUMP完后就把这个进程杀掉,不然由于输入表未初始化,再运行就是非法操作。将DUMP下来的程序的入口和输入表地址修改一下就可以使用了。
问题:上面的方法在本机上经过试验都成功了。对于vp.dll的输入表的修复方法是否可以运用在前两个文件上面呢?应该也是可以的,但修改会比较复杂一些,各位可以自己试一下。还有所有脱壳都是在我的98上进行的,脱壳后的vp.dll在NT4.0上运行会产生初始化错误,估计应该不是输入表的问题,哪位可以找出原因。前面两个脱壳文件未试。
由于本人水平有限,如有错误之处请指正。
hying[CCG]
2001年6月10日