SkinCrafter破解笔记

开篇废话

最近的一个Win下的小项目,图形界面需求简单,就直接上MFC。完成后某人觉得界面不够和谐,于是得折腾下界面。貌似目前VC++界的UI主流是MS自家DirectUI思想下的一些UI库,可惜本人的哲学“懒”字当头,再学一套UI的用法实在是要了亲命,直接放弃。记得本科期间用过一个Skin++的皮肤库,加载后可以直接换肤,可惜那玩意皮肤太少、没有皮肤编辑器而且貌似已经停止维护,因此作罢。后google到同为皮肤库的SkinCrafter,并下了3.4.4的破解版,可惜在VS2010下整合后MessageBox的按钮底色在Win7下无法匹配皮肤设置(官网查阅后发现是bug,新版已解决),遂换到3.8.0。该版本的VS2012有相关破解版,但VS2010没有,于是自己上手破解(正所谓学会破解,用D版软件都有底气了),花了差不多快2小时最终成功,并记录成本文。

Dll版工作流程

对于VC/MFC程序,SkinCrafter有Dll/ActiveX两个版本,我个人喜欢使用Dll版(两个版本的破解方法其实是一样的)。下面介绍Dll版本官方的使用方法:

  1. 首先需要引入CSCSkin.h和CSCSkin.cpp文件,并将dll及皮肤文件.skf放在目录下。
  2. 实例化CSCSkin对象m_skin,并在App的InitInstance()中执行Init()、LoadSkinFromFile()及ApplySkin()三个函数。

对比ActiveX版本,可以发现少执行好几个函数,其实这些函数在Init()方法中都完成了,所以真正的工作流程是:

  1. 定义了Dll导出表中所有的函数指针。
  2. 通过LoadLibrary()加载Dll,并执行InitLicenKeys()和InitDecoration()。
  3. LoadSkinFromFile()加载皮肤。
  4. ApplySkin()应用皮肤。

InitLicenKeys函数破解

这个函数用于写入注册信息,对于DEMO版,该函数的4个参数是定死的。其实不破解这个函数也无所谓,反正写在程序里面,但是真要破解也很简单。

由于是简单的固定字符串匹配,因此可以直接替换Dll中的字符串。当然还可以采用爆破的方法,这样无论输入什么都行。

查找四个参数中任意一个字符串,我这儿用DEMOSKINCRAFTERLICENCE(比较有代表性),定位到程序。发现有两个处strcmp()比较字符串并作出比较和跳转,因此直接nop掉跳转即可。

...
10005A49  |> \85C0          test eax,eax
10005A4B  |.  75 48         jnz XSkinCraf.10005A95                   ;  第一次跳转判断
10005A4D  |.  8B45 14       mov eax,[arg.4]
10005A50  |.  B9 8C420610   mov ecx,SkinCraf.1006428C                ;  DEMOSKINCRAFTERLICENCE
...
10005A7A  |>  85C0          test eax,eax
10005A7C  |.  75 17         jnz XSkinCraf.10005A95                   ;  第二次跳转判断
10005A7E  |.  5E            pop esi
10005A7F  |.  B8 01000000   mov eax,0x1
10005A84  |.  5B            pop ebx
10005A85  |.  8B4D FC       mov ecx,[local.1]
10005A88  |.  33CD          xor ecx,ebp
10005A8A  |.  E8 E91E0500   call SkinCraf.10057978
10005A8F  |.  8BE5          mov esp,ebp
10005A91  |.  5D            pop ebp
10005A92  |.  C2 1000       retn 0x10
10005A95  |>  6A 00         push 0x0                                 ; /Style = MB_OK|MB_APPLMODAL
10005A97  |.  68 80420610   push SkinCraf.10064280                   ; |SkinCrafter
10005A9C  |.  68 D8410610   push SkinCraf.100641D8                   ; |You are using DEMO version of SkinCrafter. InitLicenKeys function parameters should be: SKINCRAFTER, SKINCRAFTER.COM, support@skincrafter.com, DEMOSKINCRAFTERLICENCE
10005AA1  |.  6A 00         push 0x0                                 ; |hOwner = NULL
10005AA3  |.  FF15 4C350610 call dword ptr ds:[<&USER32.MessageBoxA>>; \MessageBoxA
...

去除弹出框

最明显的莫过于这个弹出框,程序启动后就会出现。该弹出框无边框可拖动,而且强制置顶(TOP_MOST),因此想到SetWindowPos()及SetWindowLong()两个函数,仔细查找后均无果。后来又查找了CreateWindow()、CreateRectRgn()及CreateRectRgnIndirect()也没有很好的下手位置。就在找函数这条思路没有突破口的时候,看到弹出窗口上有一条可以点击的网址,因此直接查字符串找这个网址,然后跳转到程序处。

找到程序处之后翻看上下文,无非就在load一些资源,突然看到ShowWindow()函数。

10002F40  /$  55            push ebp
10002F41  |.  8BEC          mov ebp,esp
...
10002F87  |.  68 F3000000   push 0xF3
10002F8C  |.  6A 02         push 0x2
10002F8E  |.  68 F3000000   push 0xF3
10002F93  |.  E8 D6410500   call <jmp.&mfc100.#AfxFindResourceHandle_1900>
10002F98  |.  50            push eax                                             ; |hInst
10002F99  |.  FF15 70350610 call dword ptr ds:[<&USER32.LoadBitmapW>]            ; \LoadBitmapW
10002F9F  |.  50            push eax
10002FA0  |.  B9 CC450710   mov ecx,SkinCraf.100745CC
10002FA5  |.  E8 BE410500   call <jmp.&mfc100.#CGdiObject::Attach_2184>
10002FAA  |.  A1 D0450710   mov eax,dword ptr ds:[0x100745D0]
10002FAF  |.  A3 D85F0710   mov dword ptr ds:[0x10075FD8],eax
10002FB4  |.  391D DC5F0710 cmp dword ptr ds:[0x10075FDC],ebx
10002FBA  |.  0F85 7C020000 jnz SkinCraf.1000323C
...
10003143  |.  A1 E05F0710   mov eax,dword ptr ds:[0x10075FE0]
10003148  |.  6A 01         push 0x1
1000314A  |.  51            push ecx
1000314B  |.  8BCC          mov ecx,esp
1000314D  |.  8965 DC       mov [local.9],esp
10003150  |.  68 843D0610   push SkinCraf.10063D84                               ;  http://www.skincrafter.com/order.html
10003155  |.  C780 84000000>mov dword ptr ds:[eax+0x84],0x497D
1000315F  |.  FF15 A4390610 call dword ptr ds:[<&mfc100.#ATL::CStringT<char,StrT>;  mfc100.#ATL::CStringT<char,StrTraitMFC_DLL<char,ATL::ChTraitsCRT<char> > >::CStringT<char,StrTraitMFC_DLL<char,ATL::ChTraitsCRT<char> > >_310
10003165  |.  8B0D E05F0710 mov ecx,dword ptr ds:[0x10075FE0]
...
1000323C  |>  8B0D DC5F0710 mov ecx,dword ptr ds:[0x10075FDC]
10003242  |.  6A 05         push 0x5
10003244  |.  E8 35410500   call <jmp.&mfc100.#CWnd::ShowWindow_12962>
...

可以看到,函数执行完成之后会ShowWindow(),而当参数为5的时候,就是SW_SHOW,因此可以将push的参数设置为0(SW_HIDE)或者直接把整合函数nop掉。

2014-12-03更新:

今天在项目上调试的时候发现又出现弹窗,使用相同的dll在皮肤测试程序上则没有出现。于是直接调试项目程序,发现还有几处ShowWindow函数。

10012B40  /$  55            push ebp
10012B41  |.  8BEC          mov ebp,esp
10012B43  |.  833D D45F0710>cmp dword ptr ds:[0x10075FD4],0x0
10012B4A  |.  56            push esi
10012B4B  |.  8BF1          mov esi,ecx
10012B4D  |.  75 42         jnz XSkinCraf.10012B91
10012B4F  |.  8B0D DC5F0710 mov ecx,dword ptr ds:[0x10075FDC]
10012B55  |.  6A 05         push 0x5
10012B57  |.  E8 22480400   call <jmp.&mfc100.#CWnd::ShowWindow_12962>
10012B5C  |.  8B0D DC5F0710 mov ecx,dword ptr ds:[0x10075FDC]
10012B62  |.  6A 00         push 0x0
10012B64  |.  6A 08         push 0x8
10012B66  |.  6A 00         push 0x0
10012B68  |.  E8 8B490400   call <jmp.&mfc100.#CWnd::ModifyStyle_7889>
10012B6D  |.  8B0D E05F0710 mov ecx,dword ptr ds:[0x10075FE0]
10012B73  |.  6A 05         push 0x5
10012B75  |.  E8 04480400   call <jmp.&mfc100.#CWnd::ShowWindow_12962>
10012B7A  |.  8B0D E45F0710 mov ecx,dword ptr ds:[0x10075FE4]
10012B80  |.  6A 05         push 0x5
10012B82  |.  E8 F7470400   call <jmp.&mfc100.#CWnd::ShowWindow_12962>
10012B87  |.  C705 D45F0710>mov dword ptr ds:[0x10075FD4],0x1
10012B91  |>  8B46 18       mov eax,dword ptr ds:[esi+0x18]
10012B94  |.  85C0          test eax,eax
10012B96  |.  74 74         je XSkinCraf.10012C0C
...

可直接在OD的Name表中找出MFC的CWnd::ShowWindow函数调用处全部下断,此处连续出现3处ShowWindow(),将第一次出现处的push 0x5改成push 0x0即可。

去除标题栏DEMO字样

本以为这步会很简单,很可惜,低估了作者的智商。直接找字符串没看到SkinCrafter Demo字样,因此怀疑很可能字符串是被加密的了。OD调试Dll会加载LoadDll.exe这个程序,并在Dll加载后允许通过接口执行Dll的导出函数,对于一般作为函数库的Dll这样调试当然没问题,每个函数都是独立的,但是对于这种函数之间有数据依赖的Dll来说,我必须先调用几个依赖的函数,这样就过于麻烦了(主要懒得输那么长的参数,还要小端码,而且每次加载都要输入……)。这里就用到一个技巧,自己写一个调用dll的exe,直接调试exe,并进入Dll的领空找需要的东西。

在OD的调试选项中,找到事件(Event)选项卡,把“中断于新模块”这项勾上,之后加载exe程序。每当exe加载一个dll的时候,就会被断下,F9继续执行,直到目标出现。当然SkinCrafterDll这个Dll是在程序中通过LoadLibrary()加载的,因此先要把程序跑起来。

断下后进入该Dll领空,并查看所有函数符号。考虑到和绘图有关,并经过几次实验,最后定位在GdiPlus库中的GdipDrawString()函数上,直接下断函数。但是查找函数引用的时候却没有结果,这是因为Dll刚开始并没有填充IAT,所以需要再继续F9,直到IAT初始化完毕。再查找引用就可以找到函数调用处了。

...
02E01511  |.  53            push ebx                                             ; /hIcon
02E01512  |.  FF15 0034E102 call dword ptr ds:[<&USER32.DestroyIcon>]            ; \DestroyIcon
02E01518  |>  8B9D 54FFFFFF mov ebx,[local.43]
02E0151E  |>  A1 406CE102   mov eax,dword ptr ds:[0x2E16C40]
02E01523  |.  8B15 3C6CE102 mov edx,dword ptr ds:[0x2E16C3C]
02E01529  |.  8B0D 446CE102 mov ecx,dword ptr ds:[0x2E16C44]
02E0152F  |.  8945 E0       mov [local.8],eax
02E01532  |.  66:A1 4C6CE10>mov ax,word ptr ds:[0x2E16C4C]
02E01538  |.  8955 DC       mov [local.9],edx
02E0153B  |.  8B15 486CE102 mov edx,dword ptr ds:[0x2E16C48]
02E01541  |.  66:8945 EC    mov word ptr ss:[ebp-0x14],ax
02E01545  |.  8D45 DC       lea eax,[local.9]
02E01548  |.  894D E4       mov [local.7],ecx
02E0154B  |.  8955 E8       mov [local.6],edx
02E0154E  |.  33C9          xor ecx,ecx
02E01550  |.  8D70 01       lea esi,dword ptr ds:[eax+0x1]
...
02E017E5  |.  8B8D 64FFFFFF mov ecx,[local.39]
02E017EB  |.  56            push esi
02E017EC  |.  51            push ecx
02E017ED  |.  8B8D 48FFFFFF mov ecx,[local.46]
02E017F3  |.  8D95 F8FEFFFF lea edx,[local.66]
02E017F9  |.  52            push edx
02E017FA  |.  8B95 6CFFFFFF mov edx,[local.37]
02E01800  |.  51            push ecx
02E01801  |.  6A FF         push -0x1
02E01803  |.  50            push eax
02E01804  |.  52            push edx
02E01805  |.  E8 D26D0000   call <jmp.&gdiplus.GdipDrawString>
02E0180A  |.  8B85 64FFFFFF mov eax,[local.39]
02E01810  |.  50            push eax
02E01811  |.  E8 846D0000   call <jmp.&gdiplus.GdipDeleteStringFormat>
02E01816  |.  56            push esi
02E01817  |.  E8 A66C0000   call <jmp.&gdiplus.GdipDeleteBrush>
02E0181C  |.  8B8D 48FFFFFF mov ecx,[local.46]
02E01822  |.  51            push ecx
02E01823  |.  E8 906D0000   call <jmp.&gdiplus.GdipDeleteFont>
02E01828  |.  8B95 6CFFFFFF mov edx,[local.37]
02E0182E  |.  52            push edx
02E0182F  |.  C645 FC 01    mov byte ptr ss:[ebp-0x4],0x1
02E01833  |.  E8 906C0000   call <jmp.&gdiplus.GdipDeleteGraphics>
...

经过跟踪,可以发现字符串在DestroyIcon()执行之后被装入,在内存中显示为:

02E16C38  80 0F E0 02 54 6C 6E 69 44 75 66 61 73 62 75 27  €?TlniDufasbu'
02E16C48  43 62 6A 68 27 00 00 00 E4 D9 E1 02 A0 2B E0 02  Cbjh'...???

因此字符串为TlniDufasbu'Cbjh',这个字符串经过后面的算法转换成了SkinCrafter Demo,因此只要直接干掉字符串就可以,或者nop掉GdipDrawString()。我采用删掉字符串的方法,用16进制编辑器打开dll找到这个字符串并填0。

后记

从我使用的情况来看暂时没有出现其他问题,如果有破解不完全或造成错误的地方,我会在后续做出完善。另外,此次调试过程中全程在IDA中同步参考,IDA的图形视图能很好的看出程序的执行流程,当然IDA还有万恶的F5大杀器 :P