【文章标题】: 利用特洛依动态库注入的学习总结
【文章作者】: rockhard
【作者邮箱】: wnh1[at]sohu.com
【软件名称】: 象棋奇兵3.0
【下载地址】: http://cchess.aa.topzj.com/viewthread.php?tid=177115&fpage=1
【加壳方式】: ASPR
【保护方式】: 注册码保护
【编写语言】: VC
【操作平台】: XP+SP2
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!


【详细过程】
  首先说明的是这个动态库是看雪论坛中一位大牛作品,不是我的.在此表示致敬!让我们也品尝下野猪的味道 :)
  
  
  可以从http://cchess.aa.topzj.com/viewthread.php?tid=177115&fpage=1下载安装程序和破解用的动态库(可能要注册论坛后才能下载).
  软件用ASPR加壳了.破解手法让人赞叹.将ws2_32.dll拷贝到程序的安装目录就完成了破解工作.不用脱壳,不用打补丁.下面是个人的分析与总结.
  
  一.软件注册算法分析.
  
  这个版本的注册算法不复杂,与先前的2.3版本一样.仅仅用到2048的RSA,曾修改过RSA的N,D,E写过其注册机.
  关键的验证算法在40AA10处.前面有点小检测,就是判断有没有key.dat文件,文件大小是不是介于0xc8-0x12c之间(0x40b0fe处代码)符合条件再进行注册算法的验证,此处略去.
  
  
  下面这段代码就是取硬盘物理系统号并生成32位ASICC值.也就是后面提到的程序生成的硬件信息.由于硬盘物理系列号的唯一性,这硬件信息每个电脑也就唯一.
  0040AA1F    E8 BC040100     call    0041AEE0
  0040AA24    33FF            xor     edi, edi
  0040AA26    8D4424 40       lea     eax, [esp+40]
  0040AA2A    57              push    edi
  0040AA2B    50              push    eax
  0040AA2C    E8 6F360200     call    0042E0A0
  0040AA31    68 00040000     push    400
  0040AA36    8D4C24 4C       lea     ecx, [esp+4C]
  0040AA3A    68 F0605A00     push    005A60F0                         ; ASCII "           Z5FM1251TTOSHIBA MK4026GAX"
  0040AA3F    51              push    ecx
  0040AA40    E8 AB360200     call    0042E0F0
  0040AA45    8D5424 54       lea     edx, [esp+54]
  0040AA49    52              push    edx
  0040AA4A    E8 A1370200     call    0042E1F0
  0040AA4F    33C0            xor     eax, eax
  0040AA51    894424 34       mov     [esp+34], eax
  0040AA55    894424 38       mov     [esp+38], eax
  0040AA59    894424 3C       mov     [esp+3C], eax
  0040AA5D    894424 40       mov     [esp+40], eax
  0040AA61    894424 44       mov     [esp+44], eax
  0040AA65    894424 48       mov     [esp+48], eax
  0040AA69    894424 4C       mov     [esp+4C], eax
  0040AA6D    894424 50       mov     [esp+50], eax
  0040AA71    83C4 18         add     esp, 18
  0040AA74    884424 3C       mov     [esp+3C], al
  0040AA78    33F6            xor     esi, esi
  0040AA7A    8D5C24 1C       lea     ebx, [esp+1C]
  0040AA7E    8BFF            mov     edi, edi
  0040AA80    0FB68C34 980000>movzx   ecx, byte ptr [esp+esi+98]
  0040AA88    51              push    ecx
  0040AA89    68 D4A65500     push    0055A6D4                         ; ASCII "%02X"
  0040AA8E    53              push    ebx
  0040AA8F    E8 479A0400     call    004544DB
  0040AA94    83C4 0C         add     esp, 0C
  0040AA97    46              inc     esi
  0040AA98    83C3 02         add     ebx, 2
  0040AA9B    83FE 10         cmp     esi, 10
  0040AA9E  ^ 7C E0           jl      short 0040AA80
  
  往下继续:
  调用mirsys函数.初始化大整数运算库miracl
  0040AAA0    57              push    edi
  0040AAA1    6A 64           push    64
  0040AAA3    893D 80D15900   mov     [59D180], edi
  0040AAA9    893D 84D15900   mov     [59D184], edi
  0040AAAF    E8 3C880300     call    004432F0                         ;mirsys函数
  0040AAB4    57              push    edi
  
  
  下面的call    00443120  ,是调用mirvar初始化变量 多处调用此函数,初始化变量
  0040AAB4    57               push    edi
  0040AAB5    C780 34020000 10>mov     dword ptr [eax+234], 10
  0040AABF    E8 5C860300      call    00443120                       ;mirvar
  0040AAC4    57               push    edi
  0040AAC5    8BD8             mov     ebx, eax
  0040AAC7    E8 54860300      call    00443120                       ;mirvar
  0040AACC    57               push    edi
  0040AACD    894424 2C        mov     [esp+2C], eax
  0040AAD1    E8 4A860300      call    00443120                       ;mirvar
  0040AAD6    57               push    edi
  0040AAD7    894424 28        mov     [esp+28], eax
  0040AADB    E8 40860300      call    00443120                       ;mirvar
  
  RSA的N出现在:
  0040AB22    68 D0A55500      push    0055A5D0       ; ASCII "B80BCBA9EFD...."...
  0040AB27    56               push    esi
  0040AB28    E8 B3A60300      call    004451E0       ;此处是库函数cinstr将串转为大整数 下面还有此函数调用.相同
  
  RSA的E出现在:
  0040AB31    68 C8A55500      push    0055A5C8                         ; ASCII "1C883"
  
  
  0040AB57    57               push    edi
  0040AB58    56               push    esi
  0040AB59    51               push    ecx
  0040AB5A    53               push    ebx
  0040AB5B    E8 00A40300      call    00444F60         ;RSA计算,调用 powmod:(文件key.dat中的值) ^ E % N ,
                                                        ;key.dat内容为0-9,A-F组成的256个字符串
  0040AB60    6A 00            push    0
  0040AB62    8D9424 BC000000  lea     edx, [esp+BC]
  0040AB69    52               push    edx              ;!!!!这个地址要注意,上面计算的结果就通过big_to_bytes转化成串了。该地址记为string1。
  0040AB6A    57               push    edi
  0040AB6B    68 00010000      push    100
  0040AB70    E8 EB9D0300      call    00444960         ; big_to_bytes
  
  
  这里有必要说一下正确注册时string1应该是什么样的一个值。它是由你想要注册的名称(比如无敌棋手)加上下划线'_'再加上上面所提到的硬件信息
  如果此处的硬件信息与上面生成的不一样,说明你把别人的注册文件放到自己的电脑上来了,也不能正正确注册。
  只所以要提到这地方,跟后面的特洛依DLL有关。如果此时path一个串到该地址,符合上面的条件(任意的用户名+'_' + 当前电脑的硬件信息),就可以正确注册了。
  0040AB96    8D4C24 40       lea     ecx, [esp+40]
  0040AB9A    51              push    ecx
  0040AB9B    8D9424 DC000000 lea     edx, [esp+DC]
  0040ABA2    52              push    edx
  0040ABA3    66:C74424 48 5F>mov     word ptr [esp+48], 5F  ;5F,下划线 '_'的ascii值
  
  0040ABAA    E8 77980400     call    00454426           ;从解密的明文中取出用户名(以下划线'_'做为标记)(这个CALL中调用了RtlGetLastWin32Error,patch工作就是用到这个函数的)
                                                         ;特洛依DLL修改了RtlGetLastWin32Error地址为其一子函数地址,当调用RtlGetLastWin32Error时执行了我们的代码,patch了string1.从而让程序认为正确注册了,后话
  0040ABAF    83C4 38         add     esp, 38
  0040ABB2    85C0            test    eax, eax
  
  下面将分离出来的用户名转存到固定地址005a69f0处
  0040ABC7    2BCE            sub     ecx, esi                                    ; ecx,用户名长度
  0040ABC9    8BF0            mov     esi, eax
  0040ABCB    8BC1            mov     eax, ecx
  0040ABCD    C1E9 02         shr     ecx, 2                                      ;4的倍数,先4个字节4个字节的移,
  0040ABD0    BF F0695A00     mov     edi, 005A69F0                               ; ASCII "wwww "
  0040ABD5    F3:A5           rep     movs dword ptr es:[edi], dword ptr [esi]
  0040ABD7    8BC8            mov     ecx, eax
  0040ABD9    83E1 03         and     ecx, 3                                      ;移4的余数
  0040ABDC    F3:A4           rep     movs byte ptr es:[edi], byte ptr [esi]
  0040ABDE    8D4C24 10       lea     ecx, [esp+10]
  
  0040ABE2    51              push    ecx
  0040ABE3    6A 00           push    0
  0040ABE5    E8 3C980400     call    00454426
  0040ABEA    83C4 08         add     esp, 8
  0040ABED    85C0            test    eax, eax                                     ;取得硬件信息
  0040ABEF    74 35           je      short 0040AC26
  
  
  下面类似保存从注册文件中生成的硬件信息并比较与程序生成的是不是一致。如果从注册文件中通过RSA运算的硬件信息与先前生成的一致,认为注册成功
  0040ABF3   |8D71 01         lea     esi, [ecx+1]
  0040ABF6   |8A11            mov     dl, [ecx]
  0040ABF8   |41              inc     ecx
  0040ABF9   |84D2            test    dl, dl
  0040ABFB  ^|75 F9           jnz     short 0040ABF6
  0040ABFD   |2BCE            sub     ecx, esi
  0040ABFF   |8BD1            mov     edx, ecx
  0040AC01   |C1E9 02         shr     ecx, 2
  0040AC04   |8BF0            mov     esi, eax
  0040AC06   |BF 846A5A00     mov     edi, 005A6A84                               ; ASCII "E507C88FADD96090C6283299DD8E64C5"
  0040AC0B   |F3:A5           rep     movs dword ptr es:[edi], dword ptr [esi]
  0040AC0D   |8BCA            mov     ecx, edx
  0040AC0F   |83E1 03         and     ecx, 3
  0040AC12   |F3:A4           rep     movs byte ptr es:[edi], byte ptr [esi]
  0040AC14   |B9 08000000     mov     ecx, 8
  0040AC19   |BE 846A5A00     mov     esi, 005A6A84                               ; ASCII "E507C88FADD96090C6283299DD8E64C5"
  0040AC1E   |BF 546A5A00     mov     edi, 005A6A54                               ; ASCII "E507C88FADD96090C6283299DD8E64C5"
  0040AC23   |F3:A5           rep     movs dword ptr es:[edi], dword ptr [esi]
  0040AC25   |A4              movs    byte ptr es:[edi], byte ptr [esi]
  0040AC26   \B9 08000000     mov     ecx, 8
  0040AC2B    8D7C24 1C       lea     edi, [esp+1C]
  0040AC2F    BE 846A5A00     mov     esi, 005A6A84                               ; ASCII "E507C88FADD96090C6283299DD8E64C5"
  0040AC34    33C0            xor     eax, eax
  0040AC36    F3:A7           repe    cmps dword ptr es:[edi], dword ptr [esi]    ;比较由注册文件运算出的硬件信息与先前取硬盘系列号生成的是否一致。
  0040AC38    75 0B           jnz     short 0040AC45
  
  如果相等,al=1(成功的标志),否则跳到0040AC45,由040AC47  32C0   xor  al, al可知不成功返回0
  0040AC3A    B0 01           mov     al, 1       ;置al为1
  0040AC3C    5F              pop     edi
  0040AC3D    5E              pop     esi
  0040AC3E    5B              pop     ebx
  0040AC3F    8BE5            mov     esp, ebp
  0040AC41    5D              pop     ebp
  0040AC42    C2 0400         retn    4
  
  比较不相等的处理:
  0040AC45    5F              pop     edi
  0040AC46    5E              pop     esi
  0040AC47    32C0            xor     al, al       ;置al 为 0
  0040AC49    5B              pop     ebx
  0040AC4A    8BE5            mov     esp, ebp
  0040AC4C    5D              pop     ebp
  0040AC4D    C2 0400         retn    4
  
  
  
  大致就是这样。上面说得比较乱。做个总结。
  注册机的算法如下:
  比如说你要注册的名称为 ABC,你的硬件信息为1234(实际为32个字节,此处假设为4个字节) 则生成注册码的算法为:
  ABC-->414243
  1234-->3031323334
  '_'--->ASCII值为5F
   
  进行如下运算 X =( 4142435F3031323334) ^ D  % N  。D是作者手中的私钥。N就是上面给出的。
  X转换为串就是key.dat中值,也就是注册码了。
  
  验证部分就是上面的算法了。Y= ( X ^1C883) %N   理论上用正确的注册码和对应的硬件信息生成的 Y值就是4142435F3031323334转换为串就是
  ABC_1234 作者以_为标志,分出注册名称与硬件信息部分。然后验证1234与当前硬盘物理系列号生成的值是不是一致。一致认为注册成功(一机一码)。
  
  
  二.特洛依Dll的破解原理.
  
  由于程序在载入DLL时,首先会尝试从当前的程序文件路径装载,如果没找到,则在系统目录下查找. 利用这个特点,我们可以让程序在调用系统DLL中某个函数时先调用自己提供的DLL中的同名函数(提供与系统DLL相同的名字,函数名也相同,放在程序的目录下面),在完成我们某个目的后,再通过自己的函数取出系统DLL该同名函数的地址,跳到该地址执行原函数功能.
  下面是特洛依DLL的分析.
  
  该DLL是通过挂钩WSAStartup实现PATCH功能的.看看该特洛依DLL提供的WSAStartup代码:
  
  int __stdcall WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData)
  .text:10001A10                 public WSAStartup
  .text:10001A10 WSAStartup      proc near
  .text:10001A10                 call    sub_10001100                   ;一开始就进入PATCH代码函数.
  .text:10001A15                 push    offset aWsastartup ; "WSAStartup" 
  .text:10001A1A                 call    sub_10001140       ;这个call主要工作就是通过传入的串,也就是函数名,取到系统提供的
                                                            ;ws2_32.dll该函数的地址,放到EAX中去.
  .text:10001A1F                 jmp     eax                ;跳到系统中真正的WSAStartup函数.(不能是CALL,只能是JMP,跟栈中参数有关)
  .text:10001A1F WSAStartup      endp
  
  
  再看下10001100处这个函数的代码:
  .text:10001100 sub_10001100    proc near               ; CODE XREF: WSAStartupp
  .text:10001100                 mov     eax, dword_10008B38            ;一个全局变量,记为x8B38吧
  .text:10001105                 push    esi
  .text:10001106                 inc     eax                            ;将x8B38加一
  .text:10001107                 mov     esi, 5592D0h                   ;5592d0是IAT中的一个地址,存放RtlGetLastWin32Error的地址
  .text:1000110C                 cmp     eax, 2                         ;x8B38变量是不是为2
  .text:1000110F                 mov     dword_10008B38, eax
  .text:10001114                 jnz     short loc_1000113E             ;不为2不PATCH.
  
  这地方为什么要用一个全局变量,并判断该变量是不是为2 ?
  主要原因是第一次调用这个函数时,壳还没有将代码段解压出来,不是PATCH时机.第二次才是时机.
  
  ; 下面这个cmp检测是不是要修改的目标程序,只有地址在040AC2B处值为1C247C8D才是目标程序
  ;否则你不小心把这个特洛依DLL放在某个文件下下面,而这个文件夹下面有程序正好调用系统的ws2_32.dll就麻烦了.
  .text:10001116                 cmp     dword ptr ds:40AC2Bh, 1C247C8Dh 
  .text:10001120                 jnz     short loc_1000113E
  
  
  .text:10001122                 call    sub_10001040
  ;上面的call是创建一个key.dat文件,并写入256个0,让程序执行到注册算法验证部分.没这文件或大小不对,直接跳到其它地方了.
  
  
  .text:10001127                 mov     eax, [esi]              ; 取RtlGetLastWin32Error(见.text:10001107 处说明)
  .text:10001129                 mov      dword_10008B34, eax    ;将RtlGetLastWin32Error地址放到一个全局变量处,因为做完我们想做的事后,还得真正调用它
  
  .text:1000112E                 mov     dword ptr [esi], offset loc_10001000 ;
  上面一句代码就是将原iat中RtlGetLastWin32Error地址换为地址10001000 即当程序调用RtlGetLastWin32Error时不是调用RtlGetLastWin32Error,而是跑到10001000 处.
  
  
  .text:10001134                 mov     dword ptr ds:5594CCh, offset loc_10001030 ; 
  这句代码同上,处理GetTickCount地址,既当调用GetTickCount时,跑到了10001030处
  (题外话,用这种方法破解,并不需要处理GetTickCount函数的.我当初写LAODER是因为有SLEEP,会被原程序检测到的,不知大牛对此有何看法?)
  
  .text:1000113E loc_1000113E:                           ; CODE XREF: sub_10001100+14j
  .text:1000113E                                         ; sub_10001100+20j
  .text:1000113E                 pop     esi
  .text:1000113F                 retn
  .text:1000113F sub_10001100    endp
  
  
  下面看看10001000处的代码,即调用RtlGetLastWin32Error时所要执行的代码:
  .text:10001000                 mov     eax, 40ABAFh
  .text:10001005                 cmp     [ebp+4], eax
  .text:10001008                 jnz     short loc_10001026 ; 
  这个判断很重要,不是对每次调用RtlGetLastWin32Error都要patch的.只有eax符合上面的条件时才patch的.
  联想一下前面注册算法分析部分.我们提到string1,如果提供一个正确的sting1,就可以让程序认为注册成功.
  前面的注册算法分析部分有这么一行:
  0040ABAA    E8 77980400     call    00454426           ;从解密的明文中取出用户名(以下划线'_'做为标记)(这个CALL中调用了RtlGetLastWin32Error,patch工作就是用到这个函数的)
                                                         ;特洛依DLL修改了RtlGetLastWin32Error地址,当调用RtlGetLastWin32Error时执行了我们的代码,patch了string1.从而让程序认为正确注册了,后话
  
  
  这个后话就是指现在了.因为在454426这个call中,调用了RtlGetLastWin32Error.刚才的那个判断就是判断RtlGetLastWin32Error是不是来自于这个地方.
  如果来自于这个对方,就构造该sting1串.只在来自于该处的RtlGetLastWin32Error才能修改内存
  下面的代码就是构造string1串了.
  .text:1000100A                 pusha
  .text:1000100B                 mov     edi, [ebp+8]
  .text:1000100E                 mov     ecx, 5
  .text:10001013                 lea     esi, aZ_        ; "学习_"   (也就是用户名+ "_")
  .text:10001019                 rep movsb
  .text:1000101B                 lea     esi, [ebp+5Ch]  ;ebp+5ch处是硬件信息
  .text:1000101E                 mov     ecx, 20h        ;此处0x20是硬件信息的长度,32个字节
  .text:10001023                 rep movsb               ; 移硬件信息。
  .text:10001025                 popa
  .text:10001026
  .text:10001026 loc_10001026:                           ; CODE XREF: .text:10001008j
  
  
  跳到真正的RtlGetLastWin32Error执行其代码.
  .text:10001026                 jmp     dword_10008B34  ; 上面提到的保存RtlGetLastWin32Error的变量
  
  
  
  至于为什么要处理GetTickCount,跟本主题没关系.不多说了.
  
  系统提供的ws2_32.dll还有许多其它导出函数.所有特洛依DLL也要提供相应的导出函数.其做法类似上面WSAStartup函数.
  .text:10001A15                 push    offset aWsastartup ; "WSAStartup" 
  .text:10001A1A                 call    sub_10001140       ;这个call主要工作就是通过传入的串,也就是函数名,取到系统提供的
                                                            ;ws2_32.dll该函数的地址,放到EAX中去.
  .text:10001A1F                 jmp     eax     
  
  只是上面的串WSAStartup要改为其它导出函数名就行了.
  
  至此这个特洛依DLL分析工作就差不多结束了.
  
  
  
  
  
  


【经验总结】
  写文章真不容易,有些想法大脑清楚,说出来难.还怕有错误误导别人.概括下这个DLL大致的流程。我们将用于破解的ws2_32.dll放到程序当前目录下,这样当原程序调用WSASTartup函数时就调用了我们提供DLL中的WSASTartup函数,第一次是壳调用该函数。我们直接调用系统的WSASTartup。第二次是被加壳程序调用。此时我们将程序IAT表项中RtlGetLastWin32Error改为自己的函数入口。再调用原来的WSASTartup.
  今后,只要程序中调用到RtlGetLastWin32Error时,都必将进入到我们提供的函数入口处,在这个函数中,判断RtlGetLastWin32Error是不是来自于注册算法验证的那个地方,是的话修改程序内存地址,达到注册目的,不是仅仅调用系统原来的RtlGetLastWin32Error
  
  这个方法不只用于系统的DLL,也可以处理程序自己的DLL,如果程序调用A.dll中的abc函数.我们写自己的A.dll ,将原来的
  A.dll改名为B.dll,做完想做的事后再jmp GetProcAddress( LoadLibrary(b.dll),"abc")就是了.
  
  注意的就是要处理好patch时机,还有对patch目标程序的检验.
  
  对于加壳的程序,选择挂接的函数最好是在壳中没有被调用,而程序中是在patch代码执行之前就调用了.这样的函数处理起来
  最方便.只要第一次执行到该函数,说明壳已运行完毕,而要PATCH的代码还没执行.就你随便XXXXX吧...
  
  


【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!