首页
社区
课程
招聘
[分享]Winlicense过硬件锁定和时间限制
发表于: 2013-2-6 13:07 54580

[分享]Winlicense过硬件锁定和时间限制

2013-2-6 13:07
54580
工具:原版OllyDbg v1.10 + StrongOD + ODBGScript
声明:JFF(JUST for FUN);转载请注明出处;不得用于商业目的;对滥用本文所造成的一切损害均不承担任何责任!

本文为帖子《WinLicense SDK 应用实例分析》的“前传”。
那篇文章中,在没有许可证的情况下,使了三十六计中的“无中生有”,直接从"NEAR OEP"处开始,对相当部分的人来说有一定难度,难免“曲高和寡”。
这次通过演示在另一台虚拟机中对Repack的版本进行Bypass,帮助你解开那个结。目标为那篇文章的附件。内容将限制在本帖标题范围内,不探讨其他限制或脱壳之类的问题。

写这类文章着实是很纠结的,既不能畅所欲言、知无不尽,又不能胡说八道、言之无物。好比莎翁《威尼斯商人》中“一磅肉”的故事,不可能做到兼顾各方利益、绝对公平;不由得心中暗唱到“真的好难...”,有的地方只好“犹抱琵琶半遮面”了,还请各位看官谅解!

一.版本问题
有人问目标的Winlicense版本。想法是对的,大概是不同的版本,应对的方法有所不同。
LCF-AT很早有个脚本"TheMida - WinLicense Info Script",后来这个功能放在了"Themida - Winlicense 1.x - 2.x Multi PRO Edition 1.2"中,不幸地是它最高只能检测到2.0.6.0。
原因是,脚本在包含Logo文本和代码的数据片段(该片段在代码运行后会被再次加密)解密后,搜索ASCII的版本信息,后来Oreans把这里的明文信息去掉了。该目标中只剩下"(c)2009 Oreans Technologies"字样。
事实上,版本的明文信息还是存在的,不过在非常靠后的地方,而且始终保持明文状态。因为有一个SDK函数"void WLGetVersion(char* pBuffer);"要用到它,这个函数非常简单,等同于"REP MOVS BYTE PTR ES:[EDI], BYTE PTR [ESI]"一条指令。
因为是给SDK用的,WinLicense自身并不需要它,所以在接近"NEAR OEP"的时候,版本的ASCII字符串才会被解密为明文。
这在Themida时,不会有问题,而在Winlicense时就产生了一个矛盾:我们想通过版本信息来决定如何搞定它,但在搞定它之前又没法知道它的具体版本。
在"NEAR OEP"处,可以查到目标的版本为"2.100",对比一下ZeNiX的"WinLicense 2.1.0.10",同为"2.100",显然它忽略了最后的小版本号。可见目标的版本确实不够“新”,我们把它拿来说事儿,是看中了其SDK应用的特色。
如果你不是Oreans的用户,基本上也就没有多少选择的余地,最近也就Kissy发了个"WinLicense 2.2.3.0 Demo"的Crack算整出点儿水响;VMP那边厢,1ST刚发了个"VMProtect v2.12.3 ULTIMATE"。

随着经验的积累,还有个方法可以帮助我们确定目标版本的范围:通过比较VM_CONTEXT的大小和Handlers的个数。依据为:保护程序和被保护应用里这两个数值是相同的,当然也只是一个参考,不是每一次升级这两个数值都会变化。目标中这两个值为50和A8,与"WinLicense 2.1.0.10"完全一致;查一下"WinLicense 2.2.1.0 Demo",此两值则分别为94和AA,可找到"2.210"字样。
因此,我们有理由推断,当Oreans发布新版本的WinLicense时,它也需要保护,一定是采用自己的技术,即用新版本的WinLicense来保护发行版本:对Demo版,无“锁定”,其效果等同于用新版本的Themida保护,功能上肯定有限制;对"Custom Build"版本,不同的用户限制的项目也是不一样的,没有人愿意放出来(我在tuts4you曾看到有人请求某人放出某个版本遭到拒绝。),有被追查到的风险,就算换KEY、Repack,那个常数(softworm称其为DeltaOffset,Deathway叫做Align)基本上是没法改的,恐怕很难“坐等”了。

所以版本不是重点,了解大致情况即可。甚至方法都不是重点,解决问题的思路才是我们需要的。

二.“FirstJmpAddress/CmpEcxEaxAddress方法”
有人和我交流时,说用到了streamload发表于2011-3-29的文章“ByPass-WinLicense 硬件锁定[2.1.010-2.1.332]”中的方法。我发现他的FirstJmpAddress和CmpEcxEaxAddress都找对了,但是脚本Log.txt跑出来的结果不对。非常好,这是一个不错的开始,我正愁怎么开始这篇文章。
帖子的附件"要用到的脚本.rar"中Log.txt和bypassHWLock.txt来源于cektop发表于2011-1-6的文章“Wl.Cring”中"Wl.Cring\5.WinLicense V2.1.0.10 ByPass\Example.rar"里的"Script\Log.txt"和"Script\WL ByPassII.txt"。cektop文章中的链接已经失效,可从boycott发表于2011-10-10的帖子“Winlicense 2.1.0.10 脱壳”中找到。

本来我打算把它称为“cektop方法”。后来考虑到,万一是其他人首先公布了这个方法,就乌龙了。而且咱也不能搞个人崇拜,有木有!如果一定要再往前追溯,可能来自于文章“The Winlicense tutorials v1.2.1”(by quosego/snd, 17-04-2009),Is_Registered dwords这个术语大概源于此文。cektop是SND的退休“探员”,意味着cektop和quosego曾是一条战壕的战友!
我老在想,包括LCF-AT这些snd的人,怎么看都像来自咱东北那旮旯的,肿么榜样都移民了?!难怪老太太倒了都没有人扶一把。我们的幸福生活水平总在不断地提高,有人欢天喜地地搬进了新房,有人心满意足地开上了新车;但是生存环境却日趋恶化,这个城市变得越来越拥挤,公共资源因此而短缺,人们整天呼吸充斥着尾气的空气、生活在钢筋水泥森林的鸽子笼中,你能回答你是幸福的吗?移民自然成为成功人士们的选择。屌丝们大概只剩一条路,与焕生们因为失去土地而不得不进城背道而驰,主动去那些大有作为的地方接受再教育!

事实上这个方法就“过硬件锁定”来说,至今在很多情形下仍然工作得很好,看到很多“大牛”还在向人传授这一方法,有人还在这个基础上制作出了所谓的“通用破解工具”。
配合这个方法,cektop在文章“WinLicense ByPass Source”中还给出了制作“通用工具”的源码。以至于人们轻松地不仅摹仿了脸,连面都给模仿了。这也没什么好丢人的,文章写出来就是让人模仿和超越的!
void __declspec(naked) New_CmpEcxEaxAddress()
{
    _asm
        {
        popfd
                cmp eax,CheckWord_0
                jne l_1
                mov ecx,eax
l_1:
                cmp ecx,eax
                pushfd
                jmp hCheckNextJmp
        }
};


1. FirstJmpAddress和CmpEcxEaxAddress
以前知道这个方法,那时还是“初哥”,搞不大清楚,逐渐就淡忘了。到后来和WinLicense斯混熟了,就用了自己的方法,位置比这里要早很多,更安全有效。这次为了写文章,重新研究了一下。
该方法是存在一些问题的,cektop也没有透露更多的技术细节,时隔两年后再来讨论它显得不够公道。冒犯之处还请cektop大人海涵,勿降罪于臣。
这个方法应该说是非常好的,特别是对不熟悉WinLicense技术的人,能将你方便迅速地带到解决问题的关键之处。一个不错的切入点,就算是对逆向破解一无所知的人也能按图索骥予以搞定。
从WinLicense的工作原理上讲,正如quosego所指出的,存在两个Is_Registered DWORDs,在被保护程序开始运行后,在较早的位置这两个DWORDs被VM4中的一个"VM CALL"初始化成正确的值。随着“许可系统”的逐渐进行,如果HWID或Expiration等等“锁定”对象有问题,两个DWORDs会被修改。
到FirstJmpAddress的时候,实际上是准备调用VM1中的一个"VM CALL",结合本例FirstJmpAddress=0059AF5E:
0059AF5E  /E9 B76F0000     JMP     005A1F1A     ; This is the "First JUMP", to VM1_13793FE5
0059AF63  |68 8B427913     PUSH    1379428B
0059AF68  |E9 8F48E8FF     JMP     0041F7FC     ; Search for CmpEcxEaxAddress under this jmp address
0059AF6D  |68 A8447913     PUSH    137944A8
0059AF72  |E9 8548E8FF     JMP     0041F7FC
0059AF77  |68 02467913     PUSH    13794602
0059AF7C  |E9 7B48E8FF     JMP     0041F7FC
0059AF81  |68 FE937913     PUSH    137993FE
0059AF86  |E9 7148E8FF     JMP     0041F7FC
0059AF8B  |68 A7947913     PUSH    137994A7
0059AF90  |E9 6748E8FF     JMP     0041F7FC
0059AF95  |68 08967913     PUSH    13799608     ; Call stack return to here after MessageBox
0059AF9A  |E9 5D48E8FF     JMP     0041F7FC
...

跟随FirstJmpAddress会来到VM1_ENTRY(0041F7FC),这时[ESP]=13793FE5,标记为VM1_13793FE5。也就是说FirstJmpAddress将去调用VM1_13793FE5这个"VM CALL"。
教程让你去第二个JUMP地址0041F7FC下面找CmpEcxEaxAddress,实际上是找VM1的一个Handler(我助记为"CMP_dw[ESP+4]_[ESP]/EFL")的地址,注:《WinLicense SDK 应用实例分析》里误为VM4,这里更正。本例中CmpEcxEaxAddress=0043111C,教程讲要多找几个并下F2是因为不知道准确的位置。
有了FirstJmpAddress和CmpEcxEaxAddress,就可以跑脚本Log.txt了。

2. 脚本Log.txt
原版的脚本有点小毛病,稍微修改了一下,脚本文件名为"Log.MistHill.osc",已打包在附件中。
a) 原脚本运行要求:在找到两个地址后,重新载入目标,清除所有断点,HE断FirstJmpAddress,断下来后再F2断CmpEcxEaxAddress,然后运行脚本。
考虑到有的人没有正确掌握这个严格的过程,我把它“傻瓜”化了,只需填入两个地址,“从头再来”(人的一生中有多少时刻允许你从头再来?),直接运行脚本即可。
b) 增加了一个VM1_EXIT_Address,用来结束记录,保证记录的数据仅限于VM1_13793FE5内,排除多余的干扰数据。
c) 将记录到文件改为记录到窗口,因为记录的数据并不多,脚本运行结束后,直接查看"Script Log"窗内的信息更方便一些。“阅后即焚”(Burn After Reading),符合环保理念。

不知你留意到没有,《WinLicense SDK 应用实例分析》中创建许可证时图片中"Days Expiration"的设置为30天、"Date Expiration"的设置为"2013-1-12",意味着那张证书现在已经过期了,或许是无意中为写此续集埋下了伏笔。
电影《寒战》(Cold War)是我喜欢的类型,到结尾明示我们会有续集。影片节奏感很强,留给你思考的时间并不多,基本没有废话对白,一些貌视打酱油的桥段、镜头也成为我们判断谁是“内鬼”的依据。其中的一些细节也是非常重要的,特别是在玩Oreans这类的数字游戏时更是如此,好比一根链条,一环扣一环,任何一处断掉都会给你后面的分析带来困难。

如果你赶在“过期之前”跑过脚本"Log.txt",将有幸同时目睹Is_Registered_DWORD1和Is_Registered_DWORD2的风采,下面是脚本"Log.MistHill.osc"运行的结果:
43111C     eax:0  ecx:0
43111C     eax:0  ecx:0
43111C     eax:0  ecx:0
43111C     eax:0  ecx:0
43111C     eax:557135FA  ecx:32C724F2     ; 557135FA is the Is_Registered_DWORD1, 32C724F2=[004194E9]
43111C     eax:2E77EE79  ecx:560BC21      ; 0560BC21 is the Is_Registered_DWORD2, 0560BC21=[00419E21]
43111C     eax:2DA04743  ecx:560BC21
43111C     eax:1B84290B  ecx:560BC21
43111C     eax:721F6913  ecx:560BC21
43111C     eax:4BDB9C0E  ecx:560BC21
43111C     eax:4DCD6E  ecx:560BC21
43111C     eax:390A42C7  ecx:560BC21
43111C     eax:390A42C7  ecx:390A42C7     ; 390A42C7=[004178D5]
43111C     eax:0  ecx:0

脚本"Log.MistHill.osc"在“过期之后”的可能结果:
43111C     eax:0  ecx:0
43111C     eax:0  ecx:0
43111C     eax:0  ecx:0
43111C     eax:0  ecx:0
43111C     eax:557135FA  ecx:32C724F2     ; 32C724F2=[004194E9]
43111C     eax:2E77EE79  ecx:7040FE04     ; 7040FE04=[00419E21]
43111C     eax:2DA04743  ecx:7040FE04
43111C     eax:1B84290B  ecx:7040FE04
43111C     eax:721F6913  ecx:7040FE04
43111C     eax:4BDB9C0E  ecx:7040FE04
43111C     eax:4DCD6E  ecx:7040FE04
43111C     eax:390A42C7  ecx:7040FE04
43111C     eax:390A42C7  ecx:390A42C7     ; 390A42C7=[004178D5]
43111C     eax:0  ecx:0

这就是你现在跑脚本可能看到的结果。你会发现,0560BC21(Is_Registered_DWORD2)从脚本输出结果中消失了。也就是说,我们现在对Is_Registered_DWORD2一无所知。如果只有557135FA(Is_Registered_DWORD1),我们无论如何是搞不定的。
依样画葫芦地按部就班是不行了,但咱也不能困“死”在这里,得有所突破。
我们将脚本再改进一下,升级为"Log.MistHill.V2.osc"。改进的地方很简单,你可以从附件中对比一下:只要谁与557135FA比较,我们就Patch它。
和谐社会,如果说“人人都爱雷蒙德”(Everybody Loves Raymond)属于“可以有”,那么凡事人人都得讲道理则是“必须的”,所以我们不但要"Learn How",而且还要"Know Why"——愿钟书先生宽恕我吧——老人家最痛恨中文夹洋文的腔调。算是“鱼”与“渔”的不同境界。
修改的理由:程序发现不满足证书的要求,把Is_Registered_DWORD1改为32C724F2了。脚本“拨乱反正”,把它改回来,继续走正确的道路。
这下两个“内鬼”都让我们逮到了:
43111C     eax:0  ecx:0
43111C     eax:0  ecx:0
43111C     eax:0  ecx:0
43111C     eax:0  ecx:0
43111C     eax:557135FA  ecx:32C724F2     ; 32C724F2=[004194E9]
43111C     eax:557135FA  ecx:557135FA     ; 557135FA=Patched
43111C     eax:560BC21  ecx:7040FE04      ; 7040FE04=[00419E21]
43111C     eax:2E77EE79  ecx:7040FE04
43111C     eax:2DA04743  ecx:7040FE04
43111C     eax:1B84290B  ecx:7040FE04
43111C     eax:721F6913  ecx:7040FE04
43111C     eax:4BDB9C0E  ecx:7040FE04
43111C     eax:4DCD6E  ecx:7040FE04
43111C     eax:390A42C7  ecx:7040FE04
43111C     eax:390A42C7  ecx:390A42C7     ; 390A42C7=[004178D5]
43111C     eax:0  ecx:0

第5行:557135FA与32C724F2比较,这是记录Patch之前的数据,没有真正执行。执行的是第6行Patch之后两个557135FA相比较指令。第7行eax中我们预料中的第二个“内鬼”Is_Registered_DWORD2主动送上门来。

显然第6行和第7行eax的值557135FA和0560BC21就是我们要找的正确数据,细心的人这里一定会问WHY? 这没有可什么奇怪的,VM1_13793FE5的代码保证了它就是事实。

有了它们我们就可以跑"WL ByPassII.txt"脚本了(原版只Patch了CheckWord_0,需要改进)。哦,且慢,等一等,再等一等,“不要慌,后面一定还会上鸡汤”的,不然你会遗憾只吃到了酸菜。

现在有太多人时刻处于一种很“赶”(hurry)的状态。没办法,我们的生存环境决定了,你不hurry就挤不上“最后一班地铁”,或接不到你翘首以盼的孩子,甚至你的POSITION都会被人顶掉。
电影《无间道》借杜的口说到:你周围的人如果有谁做事不认真、不专心,那他一定是警察。你会发现我们身边的“警察”不在少数。不认真,略知皮毛就跑路,终归不好。那个谁不是最讲究“认真”二字吗?扯远了,不谈...。

我们说过,细节很重要。难道我们就不想一想,这些貌视随机的数值究竟代表什么?它们的出处?
在Oreans的产品中,有太多这样的“随机数”,防止“通用破解”。它的大部分标志(Flag)不会是简单的1/0(true)或0/1(false),在保护一个应用时,它们确是“随机数”,但在已保护的程序中,它们已然变为“常数”了。
从Log中可以看到,除了两个“内鬼”,还有很多貌视随机的数值,它们都有特定的意义。

先说两个我们认为是正确数据的两个值:557135FA和0560BC21的出处,它们不会象孙猴子那样从石头缝里蹦出来,既然是Flags,一定是保存在内存中,不可能仅出现在寄存器中。
顺便说一下,Handlers中除了CMP_dw[ESP+4]_[ESP]/EFL,你可以猜到一定还有CMP_w[ESP+2]_[ESP]/EFL(CmpCxAx)和CMP_b[ESP+2]_[ESP]/EFL(CmpClAl)。相比较时,一个来自内存,另一个硬编码在PCODE中,通过Handlers:PUSH_IMM32、PUSH_IMM16或PUSH_IMM8入栈。
怎么找这两个值的地址,很简单:CTRL+B/CTRL+L,但需要注意从中排除VMs_CONTEXT寄存器的地址。或者你人品够好,也可请Deathway替你找,还捎带上CmpEcxEaxAddress,一找一个准,不用CTRL+S/CTRL+L的组合拳出手。
因为简单,我们就直接小儿科CTRL+B了。本例中:
Is_Registered_DWORD1_Address = 004194E9
Is_Registered_DWORD2_Address = 00419E21

特别提醒:当断在FirstJmpAddress处时,你是找不着557135FA和0560BC21的,它们已经被修改,应该找与之对应的ecx中的值:32C724F2和7040FE04。

声明一下,这个不是我的“发现”,事实上quosego的那篇文档早就告诉我们去找这两个地址,只是他的方法太具体,适合较低的版本。

下面说各“随机数”的含义。但得先弄清楚FirstJmpAddress处这个"VM CALL"究竟是干什么的?
它是程序流程中必经的中途岛。Oreans在这里保持了应有的绅士形象,对用户非常地友好,为即将可能呈现的MessageBox(就是引导你至FirstJmpAddress的那个)作准备。

A) 正如上面Log所记录的那样,它首先检查[Is_Registered_DWORD1_Address]([004194E9])的值,它是对证书所限制的内容逐项检查的结果,可理解为注册正常与否的标志,数据类型为Boolean:557135FA=true;32C724F2=false。既然是逻辑值,程序就没有对第三种情况进行判断处理,在FirstJmpAddress处你试试将[004194E9]改为其他值,一定会得到MessageBox:
"An internal exception occured (Address: 0x%x)",LF,CR,"Please, contact support@oreans.com. Thank you!"


B) 其次,检查[Is_Registered_DWORD2_Address]([00419E21])的内容,其数据类型类似于枚举,一系列随机常数。用于证书验证失败时,描述错误原因。
如果[004194E9]==557135FA(true),理论上[00419E21]应该保持初始值0560BC21。既然证书验证一切正常,何来失败原因?换句话说,即哥俩要么都不被改写,要么都被改写。出现誖论的唯一解释是你做过手脚,所以为了避免这一情况发生,Oreans将坚持检查[00419E21]的情况。

这就好理解了,在只有"Hardware Lock"的目标中,“cektop方法”依然有效:在CmpEcxEaxAddress处,通过“欺骗”的手段让程序相信[004194E9]为true(实际上它已经false了),幸运地是它没有识破骗术,或者是被阻止了,没有去修改[00419E21]的初始值0560BC21(可从“过期之前”的Log记录得到证实)。
怎么解读上面三个Log记录:
  i) eax中的值为PCODE硬编码的IMM32
 ii) ecx的值见各自注释
而在本例,除了"Hardware Lock",现在"Days Expiration"和"Date Expiration"都出问题了,[00419E21]被改写为7040FE04,所以只Patch了Is_Registered_DWORD1的“原版”脚本Bypass失败。

C) 在FirstJmpAddress处,如果:[004194E9]==557135FA && [00419E21]==0560BC21。表示一切正常,注册成功,目标在初次运行时MessageBox会显示MsgID23:Show registration info (only first time when registered):
"This application has been registered to",LF,CR,"%name - %company"
如果只在FirstJmpAddress处将[004194E9]和[00419E21]Patch为“正确”的值,将显示异常对话框,因为这时它早就进入了错误流程,其他标志已经设置,地球人将无法阻止外星人了,为时已晚。在这里再改,Oreans的人已经被整糊涂了,搞不清你在搞什么东东,简单地抛出一个异常了事。

D) 在[004194E9]==32C724F2的情况——也正是我们现在所面临的情况——这个"VM CALL"就开始根据[00419E21]的值加上其他标志表明的失败原因来解密准备显示在MessageBox中的信息。
错误码与错误信息的对应关系见下表:
[00419E21]      Message                                   Other Flags
----------      -------                                   -----------
2E77EE79        当前注册文件不是本机文件,请合法注册。
                您的机器码: %machineid 已拷贝到剪贴板
2DA04743        当前注册文件无效。只有有效的注册文件才能运行。
1B84290B        程序只允许特定机器的特定注册文件,请与程序供应商联系。
721F6913        抱歉,网络不允许运行更多的实例
4BDB9C0E        你需要在服务端首先运行软件。
004DCD6E        请开通服务器共享文件夹的写入权限。
390A42C7        您的注册文件已损坏,请重新注册。
                您的机器码: %machineid 已拷贝到剪贴板
!=0560BC21      当前注册文件不是本机的,请重新注册。      [004178D5]==390A42C7
                您的机器码: %machineid 已拷贝到剪贴板
!=0560BC21      程序不允许硬件更改,请重新注册。          [004178D5]==7040FE00
                您的机器码: %machineid 已拷贝到剪贴板
23C225E6        抱歉,程序的使用期限已到,请重新注册。    [004178D5]!=390A42C7 && !=7040FE00
3184ECC7        抱歉,当前注册权限已无效。                [004178D5]!=390A42C7 && !=7040FE00
7040FE01        你的注册文件已过期,请重新注册。          [004178D5]!=390A42C7 && !=7040FE00 && b[005990D2] !=00
                您的机器码: %machineid 已拷贝到剪贴板。
7040FE02        你的注册文件已过期,请重新注册。          [004178D5]!=390A42C7 && !=7040FE00 && b[00599262] !=00
                您的机器码: %machineid 已拷贝到剪贴板。
7040FE04        你的注册文件已过期,请重新注册。          [004178D5]!=390A42C7 && !=7040FE00 && b[005993F2] !=00
                您的机器码: %machineid 已拷贝到剪贴板。
7040FE08        你的注册文件已过期,请重新注册。          [004178D5]!=390A42C7 && !=7040FE00 && b[00599D52] !=00
                您的机器码: %machineid 已拷贝到剪贴板。
42369F8A        抱歉,软件无法在当前国家运行。            [004178D5]!=390A42C7 && !=7040FE00
!=0560BC21      对不起,此软件必须注册后才能使用。        [004178D5]!=390A42C7 && !=7040FE00 && [0059AE82]==00000001
                您的机器码: %machineid 已拷贝到剪贴板

说明:
a) 表很长,只节选了与Log记录相关的部分。事实上,它遍历了"Options" -> "Customized Dialogs" -> "Trial/Registration Messages"下面所有的MsgID??。
b) 检查顺序:按行从上到下;如果有"Other Flags"列,先予以确认,再检查[00419E21]列。
c) 请注意7040FE0?,虽然Message列内显示相同的“过期”信息,但它们由不同的地址解密出来,显然Oreans故意不告诉你具体的过期原因。可以证实的是:7040FE01表示"Days Expiration"有问题,而7040FE04对应"Date Expiration"验证失败。
基本上,现在你的组合为:
[004194E9]=32C724F2 & [00419E21]=7040FE04|7040FE01 & [004178D5]=390A42C7

你得到一个Dialog显示:
"当前注册文件不是本机的,请重新注册。",LF,CR,"您的机器码: %machineid 已拷贝到剪贴板"


3. 脚本"WL ByPassII.txt"
这个脚本同样存在小问题:
a) 运行时要求正确的断点存在,也将其“傻瓜”化了。
b) 它从一开始就Hook CmpEcxEaxAddress,效率不好。改为两个DWORDs被置为正确的值后开始下断。
c) 要命的是,它只Patch了Is_Registered_DWORD1,在本例中过不了。MessageBox依然显示“当前注册文件不是本机的,请重新注册。”
d) 在加上对Is_Registered_DWORD2的Patch后,可以顺利到达"NEAR OEP"。这在通常的目标中,就算成功了。“孩子们可以高兴地大声唱着歌奔向解放区了”,但是在本例中由于在用户代码中加入了SDK函数调用进行再验证,童鞋们还需要克服许多障碍才能到达最终目的地。详见附件中的"WL ByPassII.MistHill.V3.osc"和视频。

三.“血字的研究”
我得承认我是柯南·道尔爵士(Sir Arthur Conan Doyle)的粉丝。"A Study In Scarlet"是他第一本以歇洛克·福尔摩斯(Sherlock Holmes)为主角的作品。
友情提醒:本节文字含有大量少儿不宜的成人内容,未成年人请绕道。对有关本节可能的任何提问,将一律无视:内容纯系虚构,疑为南柯一梦般地云游太虚幻境所得,当真不得的,万不可对号入座;若有雷同,当属巧合。窃以为“抛砖引玉”篇尔。

1. 关于HWID
先说Oreans的HWID构成。以前曾在某个帖子中回复过,为方便大家,这里再复制、粘贴如下:
摘自"Winlicense - The Anatomy of File Key"笔记。
    typedef struct _s_WL_LF_HWID
    {
    //  Here just for "PC Hardware"; not for "External Hardware" such as "U3 USB device" or what else.
    //  "Hardware ID" Pattern: AAAA-BBBB-CCCC-DDDD-EEEE-FFFF-GGGG-HHHH
            WORD    HWID_CPUID;             //  0 :: PartA - AAAAXXXX
            DWORD   HWID_HDDID;             //  2 :: PartB - BBBBCCCC
            DWORD   HWID_BIOSID;            //  6 :: PartC - DDDDEEEE
            DWORD   HWID_MACID;             //  A :: PartD - FFFFGGGG
            WORD    HWID_CHECKSUMID;        //  E :: PartE - HHHHYYYY
    } sMachineID, sHardwareID, sHWID;

WinLicense的HWID格式:
AAAA-BBBB-CCCC-DDDD-EEEE-FFFF-GGGG-HHHH

[B]HWID_CPUID[/B]         AAAAXXXX
  计算自两次CPUID指令的结果:a) "Get vendor ID"; b) "Processor Info and Feature Bits",只用高字HIWORD。

[B]HWID_HDDID[/B]         BBBBCCCC
  计算自硬盘的"HDD Serial"字符串。
  在两台虚拟机上测试:IDE和SCSI。SCSI情况下API取"HDD Serial"失败,用"Volume Serial Number"数据。

[B]HWID_BIOSID[/B]        DDDDEEEE
  两种情况:"Ring-0 option"为"Enabled"或"Disabled"时不同,请阅Oreans's KnowledgeBase文章。
  "Disabled"时,仍用"HDD Serial"数据,但算法不同。

[B]HWID_MACID[/B]         FFFFGGGG
  计算自API Iphlpapi.GetIfTable。
  如果返回"Ethernet network interface"的MIB_IFROW.bDescr结构中,第一个双字为0x61774D56("VMwa"),不使用MAC数据,而用一组常数。
  也就是说,只有这个字段在早期的VMware虚拟机中(WMware Tools版本差异)为常量。

[B]HWID_CHECKSUMID[/B]    HHHHYYYY
  前四个HWIDs的Checksum DWORD,只用高字HIWORD。


算法恕不公开,亦无法展开讨论,牵扯很多技术细节,涉嫌泄露商业秘密。

2. Patching HWID
这里有两个思路,其一还是Bypass,其二终极(Ultimate)方案。
a) HWID Bypass
本质还是“行骗”。我们说过,除了CmpEcxEaxAddress,还有CmpCxAxAddress和CmpClAlAddress。这次我们断后两个地址,在一个"VM CALL"中解决战斗,效率非常高,位置比FirstJmpAddress要提前许多。显然还不够彻底。
这里Oreans表演了其“多态性”或“随机性”,在不同的目标中,可能是CmpClAl/CmpCxAx组合,或仅仅是两次CmpCxAx,这和保护时的选项有关。
b) Ultimate方案
“金蝉脱壳”、“瞒天过海”、“李代桃僵”、“暗渡陈仓”、“偷梁换柱”,我已经闹不清是唱的那一出了,此事你怎么看?
总之,本质是“调包”。其一,替换证书中的HWID为本机的,这条路比“二万五千里”“路云和月”还难,接下来还得上演一幕又一幕的“连环计”,累死你。
反之,替换本机的HWID为证书中指定的,“反客为主”。很快完事儿,你可以轻松地去“喝茶”、“斗地主”、KTV庆贺了。
位置是关键:在API调用完成、计算各部分HWIDs之前。按证书中指定的HWID“拆分”,用它的逆算法填回计算的源数据中,剩下的就让Winlicense自己去玩儿了:加密各部分HWIDs、计算Checksums、转Hex为ASCII、加上分隔符"-"形成最终MachineID,再加密存储之。SDK函数"bool WLHardwareGetID (char* pHardwareId);"就是由它解密得到,试想此函数不可能再将前面的那么多过程再跑一遍。
这个方法极为安全,你完全不用再去考虑因HWID问题对Is_Registered_DWORDs的修改。

3. “IAT加密”?
分析Oreans的东西,除了虚拟机带来的困难外,还使用了其他技术来增加逆向的难度。其中一个就是对三个重要DLL的处理,不知是不是脱壳时人们常说的“IAT加密”。
先对比两张表,以kernel32.dll为例。这张表是我用脚本生成的:
Ordinal    Address     PCRC        API Name
-------    --------    --------    --------
1          00D49AE4    BADCFD97    ActivateActCtx
2          00D7491D    91ED744E    AddAtomA
3          00D71AF1    FFB6EA98    AddAtomW
4          00DB11FF    AF323AC1    AddConsoleAliasA
5          00DB11C1    C169A417    AddConsoleAliasW
6          00D98812    E62B0C66    AddLocalAlternateComputerNameA
7          00D986F6    887092B0    AddLocalAlternateComputerNameW
8          00D6B311    AAD2D3FF    AddRefActCtx
9          00D48411    C4B2B630    AddVectoredExceptionHandler
A          00DB1851    FB2BBBB3    AllocConsole
...
3B8        00D4B256    DEA1C273    lstrlen
3B9        00D4B256    C6126A73    lstrlenA
3BA        00D48EA9    A849F4A5    lstrlenW

这张表是"Windows XP SP3 CHS"中kernel32.dll正常的输出表:
Ordinal    Address     API Name
-------    --------    --------
    1      7C80A6E4    ActivateActCtx
    2      7C83551D    AddAtomA
    3      7C8326F1    AddAtomW
    4      7C871DFF    AddConsoleAliasA
    5      7C871DC1    AddConsoleAliasW
    6      7C859412    AddLocalAlternateComputerNameA
    7      7C8592F6    AddLocalAlternateComputerNameW
    8      7C82BF11    AddRefActCtx
    9=     7C809011    AddVectoredExceptionHandler = NTDLL.RtlAddVectoredExceptionHandler
   10      7C872451    AllocConsole
...
  952      7C80BE56    lstrlen
  953      7C80BE56    lstrlenA
  954      7C809AA9    lstrlenW

注意两张表中API函数入口地址的不同之处,第一张表的地址 = ImageBase + PointerToRawData ( ImageBase + OffsetInFile ) of API Function
第二张表的地址 = ImageBase + RVA of API Function
区别是由“装载”(LoadLibraryA)和“映射”(MapViewOfFile)造成的。

用KERNEL32.GetProcAddress只能得到第二张表的地址(模块中函数的RVA经Thunk后的实际地址)。所以它用了一个我称为GetProcAddressByPCRC(DWORD ImageBase, ULONG PCRC)的函数来取得第一张表的地址。
这个PCRC是由API函数名的ASCII字符串(含末尾NULL)计算出来的一个DWORD值,它实际上不是CRC算法,所以我称其为伪CRC(PCRC)。

函数GetProcAddressByPCRC通用于KERNEL32.dll、USER32.dll和ADVAPI32.dll等,ImageBase是将它们作为文件映射到内存中时的MemoryBlock基址。
这个函数还使用了API函数名的第一个字母作为输入参数,以减少搜索范围。函数在ImageBase上通过以下结构:
IMAGE_DOS_HEADER.e_lfanew
IMAGE_NT_HEADERS.OptionalHeader
IMAGE_OPTIONAL_HEADER.DataDirectory[0]
IMAGE_DATA_DIRECTORY.VirtualAddress

找到输出表的数据:
IMAGE_EXPORT_DIRECTORY.NumberOfNames
IMAGE_EXPORT_DIRECTORY.AddressOfFunctions
IMAGE_EXPORT_DIRECTORY.AddressOfNames

然后开始:
1) 从AddressOfNames找到第一个首字母匹配的API函数名
2) 计算PCRC,比较是否匹配。循环直到找到与输入相符合的函数名
3) 从对应的AddressOfFunctions中得到该函数的RVA
4) 转换该RVA为OffsetInFile,加上ImageBase就得到此函数在映射内存中的入口地址。

所以"BPX CreateFileA"等等得不到任何结果,用"BPX API函数名"是断不下来的,只能断到第二张表的地址。一不小心就跟进了这三个DLLs里,很多时候是将直接面对NTDLL.DLL,而这个是大多数人所不熟悉的。
虽然MSFT不鼓励开发人员直接使用NTDLL.DLL里的API,当然你也没法用,没有提供NTDLL.H。事实上这个NTDLL.H在世界的某个角落是存在的,不管你信不信,它就在那里。
在某些关键的地方,Oreans确实直接使用了NTDLL.DLL里的API,很有趣,有些API是Undocumented。

这样,我就有三张表,从PCRC就能查到它准备调用那个API。
当然,也可以选择阻止对IAT的“加密”。但我不想过多地搅扰Winlicense的平静生活。

4. 关于Expiration
"Date Expiration"好理解,意为过了这个村就没有下一个“龙门客栈”了,"GAME OVER"。
"Days Expiration"要稍微解释一下,在目标第一次运行时,取得当前日期,加上这个值得到截止日期。
在这里停下来想一想,如果是你打算写一个保护系统,应该怎么做?
在不让你写Dog(含SmartCard等类似的东西)、Server端数据的情况下,就只剩下注册表和文件可选。也就是用常规方法,成本相对较低。强度够好的话还是可以保证一定程度的安全,这世界上根本就没有绝对保险的东西。
你可以见到太多的保护系统,这两个东西的位置和名称都是固定的,让人们可以轻易地找到。
这里Oreans再次上演了其因“随机性”而呈现的“多态性”。注册表和文件的位置和名称,在不同的应用、机器、许可证状态时,都是不一样的。
追这些东西的过程其实是很好玩的,所以人们更注重过程而不在乎结果。Winlicense就好比一个冷艳无比的女子,拒人于千里之外,在你和她熟识后,就会发现她内心的脆弱之处。

先说注册表。
注册表的键名(KeyName)可能为:
    SOFTWARE\Classes\CLSID\{%8x-%4x-%4x-%4x-%8x}
    SOFTWARE\Classes\CcFWSettg
    Category\CLSID\{%8x-%4x-%4x-%4x}
    SOFTWARE\Classes\CertificateAuthority
    SOFTWARE\Classes\CompressedFolder\CLSID
    SOFTWARE\Microsoft\Cryptography\RNG
    SOFTWARE\Microsoft\DrWatson
    SOFTWARE\Microsoft\Windows\Help
    ...

注册表的值名(ValueName)可能为:
    ProdID
    SortOrderIndex
    InfoTip
    LocalizedString
    AppID
    Merit
    %8s
    {%8x-%4x-%4x-%4x-%8x}
    ...

初一看,这些组合足以让人疯掉。键名(和/或)值名是机器(许可证状态)相关的,那些类似于USER32.wsprintf中格式字符串(format-control string)的具体数据是应用(APP)相关的。
例如,本例在一台物理机上,未Bypass HWID时为:
SOFTWARE\Classes\CLSID\{721AFD69-6D40-78EE-B9DE-5F631481}, AppID
    A0 54 81 54 80 F0 09 70 00 94 F8 23 DE 2F 60 B0
SOFTWARE\Classes\CLSID\{AA9FE269-D59D-EE66-AAAE-AA7CCB89}, AppID
    77 23 90 AE BE 48 ED 47 DE E7 FE 2B 47 3D 1D 02
SOFTWARE\Classes\CLSID\{7C1DBB35-9AAB-F701-3FC6-BF22E248}, AppID
    7A F1 20 2A BE 08 EC 87 9E E7 FE AB 52 0C 0C 04
...

物理机上Bypass HWID后为:
SOFTWARE\Classes\CompressedFolder\CLSID, {721AFD69-6D40-78EE-B9DE-5F631481}
    E0 4A 42 3E EC 00 0D 71 0C 08 1D 5D FE BD A1 0D
SOFTWARE\Classes\CompressedFolder\CLSID, {AA9FE269-D59D-EE66-AAAE-AA7CCB89}
    70 DD 6F CE D2 08 12 67 52 84 21 9F F0 58 3F F6
SOFTWARE\Classes\CompressedFolder\CLSID, {7C1DBB35-9AAB-F701-3FC6-BF22E248}
    8A 11 21 50 EA 07 F2 F6 6A 84 E2 9E F2 69 2E F0
...

本例在一台虚拟机上,未Bypass HWID时为:
SOFTWARE\Microsoft\DrWatson, foeyxiti
    20 F9 B2 C4 40 E9 D5 C7 60 B8 F6 A7 FE C7 0F 9B
SOFTWARE\Microsoft\DrWatson, rswmtjqq
    01 DD 0F B0 7E 11 29 26 7E D4 18 B6 01 17 1F DE
SOFTWARE\Microsoft\DrWatson, sklynwfq
    8A ED 20 54 7E 0E 19 24 7E D3 08 54 72 20 0E D8
...

虚拟机上Bypass HWID后同物理机列表。

为什么会这样呢?在未Bypass HWID的情况下,用当前机器的MachineID按某种算法得到一个Index查表/计算取得键名(和/或)值名;在Bypass HWID后是用证书里指定的HWID来计算,所以保持不变。格式字符串里可变的具体数据是目标在被保护时“固化”的。

这里只列出了与"Date Expiration"和"Days Expiration"相关的东西。
值名的数据类型为REG_BINARY,解密后全部为一个DWORD。按他的加密和解密算法写两个对应的脚本,就可以做试验了。算法很有趣,是“多对一”的关系,即不同的键值(Data),解出的DWORD可能保持不变,具迷惑性。
在"SOFTWARE\Classes\CLSID\{%8x-%4x-%4x-%4x-%8x}", AppID的情形下,要特别小心。如果随便改,可能会导致你重装系统,所以强烈建议在虚拟机中玩这类游戏。
最初写了个批处理来遍历"SOFTWARE\Classes\CLSID\"以找出可疑对象,因为数据太多,慢得难以忍受;后来改为VBScript,用WMI来枚举它下面的内容,很快得到结果。
本例中GUID{7C1DBB35...}或sklynwfq保存"Date Expiration",解开后的DWORD格式: YYYYMMDD。
GUID{721AFD69...}、{AA9FE269...}或foeyxiti、rswmtjqq保存由"Days Expiration"计算的截止日期,解开后为两个DWORDs,分别保存FILETIME结构的dwLowDateTime和dwHighDateTime。

接下来说文件路径和名称。
文件路径(Path)和名称(Name)可能为:
    %SystemRoot%\install%5d.log
    %SystemRoot%\setup%4d.log
    %SystemRoot%\Q%6d.log
    %SystemRoot%\SET%3d.log
    %SystemRoot%\system\MMVER%1d.DLL
    %SystemRoot%\system\C3DMOUSE%2d.DLL
    %SystemRoot%\system32\%8s.%3s
    %SystemRoot%\system32\acprot32X%1d.DLL
    %SystemRoot%\system32\SND32NTV86%1s.DLL
    %SystemRoot%\system32\kbdro%3dm.dll
    ...

比如,本例在一台物理机上,未Bypass HWID时为:
    2012-07-27  06:11             2,666 C:\WINDOWS\SET831.log

Bypass HWID后为:
    2013-01-03  10:03             2,666 C:\WINDOWS\setup8317.log

本例在一台虚拟机上,未Bypass HWID时为:
    2012-04-20  20:50             2,666 C:\WINDOWS\system32\kbdro831m.dll

Bypass HWID后为:
    2011-08-11  19:59             2,666 C:\WINDOWS\setup8317.log


道理同上分析,文件的大小和文件名里的数字是应用相关的,在被保护时“固化”。需要特别指出的是文件的LastWriteTime,体现了Oreans的细致,用一个合理的随机值生成,且不再变化,让你从时间上不易察觉它创建了那些文件。
显然这些文件并非真正的log或dll,通篇都只写着一个字节:过期与否。
如果你在系统中发现了这些注册表项或文件,说明你曾经运行过Winlicense保护的应用。

同样,在这里我们也有两个方案可选。要么让它牵着鼻子走(谁让你是“大牛”呢?),Patch这些注册表和文件,但无法做到“通杀”。这条路相当艰难(A tough way),“痛并快乐着”。
在Ultimate方案中,我无耻地利用了她的“脆弱”(fragile)。改写了一个标识,让她天真地认为每一天都是a "Brand New Day"(取自Police前主唱Sting同名专缉,我尤其喜欢他的"Shape of my heart"、"Fragile"等)。
我只需要Patch一下证书里的"Date Expiration"就好了,她自己会帮我处理注册表和文件这类琐碎的事情。
只有一点需要注意,一开始我在脚本里用KERNEL32.GetLocalTime取当前日期,加上一天量的FILETIME,结果她用浮点指令来处理FILETIME数据,又是减,又是除,还取整,搞七搞八地在上午我还有一天,到下午就圆整为零天了。

四.DEMO说明
Demo1. Finding FirstJmpAddress & CmpEcxEaxAddress
重复别人的教程,没什么新意:寻找FirstJmpAddress和CmpEcxEaxAddress。

Demo2. Run Log Script
只录了跑"Log.MistHill.V2.osc"的过程;其他目标需要先跑"Log.MistHill.osc"。
必备输入:FirstJmpAddress、CmpEcxEaxAddress和dwLicenseIsValid(Is_Registered_DWORD1)。dwLicenseIsValid由跑"Log.MistHill.osc"得到。
可选输入:VM1_EXIT_Address。如果在其他目标中不会找这个地址,请注释掉与之相关的语句。
演示中忘了录找Is_Registered_DWORDs_Address这一步,请按上面方法自己找吧。

Demo3. Bypass HWID & Expiration
演示跑脚本"WL ByPassII.MistHill.V3.osc"的过程。
必备输入:FirstJmpAddress、CmpEcxEaxAddress、Is_Registered_DWORD1、Is_Registered_DWORD2、Is_Registered_DWORD1_Address和Is_Registered_DWORD2_Address。
可选输入:NEAR_OEP_Address。其他不使用SDK函数的目标不需要这个。
a) 运行之前记录了注册表的值,确保与Demo4运行条件相同,以示“公平”。
"Days Expiration"截止日期:"2013.01.26 08:00:00 Saturday" (01CDFB9B 23A30000)
{721AFD69-6D40-78EE-B9DE-5F631481}
    80 0F 37 AC 40 08 FB 57 E0 7B 0A F4 DE 62 34 D0         23A30000
{AA9FE269-D59D-EE66-AAAE-AA7CCB89}
    F3 A3 91 CE BE F8 E6 D7 5E 7C F7 73 43 8F 1E B2         01CDFB9B

"Date Expiration"日期:2013/01/12 (07DD010C)
{7C1DBB35-9AAB-F701-3FC6-BF22E248}
    8A 11 21 50 EA 07 F2 F6 6A 84 E2 9E F2 69 2E F0         07DD010C

b) 脚本只写了到达"NEAR OEP"的部分。需要直达用户界面的,请按本Demo自行添加——在相关SDK函数调用后:
    00609DA0处      改EAX为0;
    00609DAA处      改EAX为1;
    00609DB4处      改EAX为1。

在《WinLicense SDK 应用实例分析》中还有一处错误,这里更正一下:将SDK函数WLHardwareGetID误为WLRegGetLicenseHardwareID。

Demo4. A Study In Scarlet
抱歉只有演示,没有脚本!

五.附件列表
2013-02-06  12:05         2,048,000 WinLicense.Bypass.HWID&Expiration.part1.rar
2013-02-06  12:05         2,048,000 WinLicense.Bypass.HWID&Expiration.part2.rar
2013-02-06  12:05           779,170 WinLicense.Bypass.HWID&Expiration.part3.rar

包含:
Scripts
-------
2010-12-25  22:47               193 Log.cektop.txt
2011-01-06  19:34               487 WL ByPassII.cektop.txt

2013-01-10  15:42             1,375 Log.MistHill.osc
2013-01-13  16:13             1,487 Log.MistHill.V2.osc
2013-01-14  16:32             3,440 WL ByPassII.MistHill.V3.osc

Demos
-----
2013-02-05  19:29             1,014 01.Finding FirstJmpAddress & CmpEcxEaxAddress.htm
2013-02-05  19:29           572,658 01.Finding FirstJmpAddress & CmpEcxEaxAddress.swf

2013-02-05  19:48               902 02.Run Log Script.htm
2013-02-05  19:48           455,566 02.Run Log Script.swf

2013-02-05  20:53               942 03.Bypass HWID & Expiration.htm
2013-02-05  20:53         1,473,936 03.Bypass HWID & Expiration.swf

2013-02-05  21:47               918 04.A Study In Scarlet.htm
2013-02-05  21:47         1,742,462 04.A Study In Scarlet.swf

WLGetVersion
------------
2013-02-04  18:01            40,960 WLVersion.exe
2013-02-04  18:10           630,784 WLVersion_WL.exe


六.最后的致意
这节说一下怎么使用SDK函数。
顾名思义,SDK是给developer用的,需要有Source Code才能用。即如果有人“偷”人家的APP,再用Winlicense来保护,就没办法用。
以第一节使用WLGetVersion函数为例,用VC及"WinLicense 2.1.0.10"。
1. 让VC新建一个"Hello World!"的"Win32 Application"模板程序,项目(Project)名称:WLVersion。
2. 复制"\WinLicense\WinlicenseSDK\Include\C\WinlicenseSDK.h"和"\WinLicense\WinlicenseSDK\Lib\COFF\WinlicenseSDK.lib"到项目文件夹WLVersion中。
3. 为方便及简单,我们把这个Winlicense的版本信息显示在About对话框中:先修改Dialog的IDD_ABOUTBOX,增加一个"Static Text",显示"WinLicense Version:";再添加一个"Edit Box",命名ID为IDC_EDIT_WLVERSION。
4. 在WLVersion.cpp中添加代码。
a) 文件开始部分,增加三行
#include "stdafx.h"
#include "resource.h"

#include "WinlicenseSDK.h"                   // 新增 Line1
#pragma comment (lib,"WinlicenseSDK.lib")    // 新增 Line2

#define MAX_LOADSTRING 100

// Global Variables:
HINSTANCE hInst;                             // current instance
TCHAR szTitle[MAX_LOADSTRING];               // The title bar text
TCHAR szWindowClass[MAX_LOADSTRING];         // The title bar text
TCHAR szWLVersion[MAX_LOADSTRING];           // 新增 Line3
...

b) 在About回调函数处增加两行:
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        case WM_INITDIALOG:
            WLGetVersion(szWLVersion);                              // 新增 Line1 - 取版本信息
            SetDlgItemText(hDlg, IDC_EDIT_WLVERSION, szWLVersion);  // 新增 Line2 - 显示之
            return TRUE;
...

这样整个项目就完工了。
5. Build项目为Release版本,文件夹Release下得到WLVersion.exe。
6. 复制"\WinLicense\WinlicenseSDK\"下的WinlicenseSDK.dll和WinlicenseSDK.ini(此两文件仅用于未保护时测试用)到WLVersion.exe所在的Release文件夹中。
7. 运行WLVersion.exe,"Help"->"About...",会显示:"WinLicense Version: 1.0.0.0"
当然这是未被保护的WLVersion.exe的测试结果,并不是Winlicense真实的版本。WinlicenseSDK.dll的作用仅限于按你使用的SDK函数从WinlicenseSDK.ini中取结果,你可在WinlicenseSDK.ini定义它们。
8. 用Winlicense保护WLVersion.exe并命名为WLVersion_WL.exe。因为我们要调试它,不要给自己添堵,全部选项中尽可能使用最小保护。不要使用任何“锁定”,相当于用Themida保护。
9. 运行WLVersion_WL.exe,"Help"->"About...",这次会显示:"WinLicense Version: 2.100"
10. 如果想继续玩,OD载入WLVersion_WL.exe,很容易找到WLGetVersion函数入口,就可以开始分析了。
0040134C   68 C0844000     PUSH    004084C0                 ; char* pBuffer
00401351   E8 D2350000     CALL    00404928                 ; WLGetVersion
00401356   68 C0844000     PUSH    004084C0
0040135B   68 E9030000     PUSH    3E9
00401360   8B4C24 0C       MOV     ECX, [ESP+C]
00401364   51              PUSH    ECX
00401365   90              NOP
00401366   E8 07B69377     CALL    user32.SetDlgItemTextA
0040136B   B8 01000000     MOV     EAX, 1
00401370   C2 1000         RETN    10
...
00404928   E9 ABB40900     JMP     0049FDD8
...
0049FDD8   55              PUSH    EBP
...
0049FE44   E9 E03AF7FF     JMP     00413929                 ; to VM_ENTRY, Key=09334AF3
...

附件中WLVersion.exe所需要的WinlicenseSDK.dll和WinlicenseSDK.ini随便找个Winlicense版本(含Demo版)的复制即可。

最后,感谢上面提及或未提到的Name、ID和论坛,感谢OllyDbg、StrongOD、ODBGScript的作者及本文目标的作者!
特别致谢Oreans!

抱歉胡言乱语写了这么多,感谢你花时间阅读此“破”文!
祝各位新春快乐!
WinLicense.Bypass.HWID&Expiration.part1.rar

WinLicense.Bypass.HWID&Expiration.part2.rar

WinLicense.Bypass.HWID&Expiration.part3.rar

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
免费 0
支持
分享
最新回复 (23)
雪    币: 219
活跃值: (853)
能力值: (RANK:290 )
在线值:
发帖
回帖
粉丝
2
~
   ~~
        ~~~
               ~~~~
                          ~~~~~
~~~~mark~~
2013-2-6 13:11
0
雪    币: 6
活跃值: (1206)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
哈哈,lz终于发出来了,等了好久啊,感谢楼主
2013-2-6 13:20
0
雪    币: 2882
活跃值: (1315)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
yjd
4
好文章。顶
cektop好久没出现了。
2013-2-6 15:03
0
雪    币: 1844
活跃值: (35)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
5
不错,顶一下
2013-2-7 22:59
0
雪    币: 627
活跃值: (663)
能力值: ( LV9,RANK:270 )
在线值:
发帖
回帖
粉丝
6
实在不好意思,拖了这么久。
同时要感谢你,我不得不去整理那些杂乱无章的笔记。
2013-2-8 12:00
0
雪    币: 627
活跃值: (663)
能力值: ( LV9,RANK:270 )
在线值:
发帖
回帖
粉丝
7
放个本帖的PDF。
WinLicense Bypass Hardware Lock and Expiration Restrictions.pdf.7z

《WinLicense SDK 应用实例分析》里有这么一句:
顺便说一下,OllyDBG的SFX功能或手工查找栈上的"SEH Record"也可以找到0040DE8A处,它是现实世界中执行的第一条用户程序指令。

让细心的人逮到了,问怎么个情况。觉得在这回复比较合适,“戏说”一下。

1. 用OllyDBG自带的SFX功能查找"NEAR OEP"
可能有人还不知道OD的这个功能。使用这个功能有个条件:目标能正常运行进入用户界面。换句话说,一般的加密压缩壳,如TMD或带有效KEY的WL等,如果WL无KEY或无效就不行。
正好本帖附件里有个WLVersion_WL.exe,我们用它来做例子。
OD里SFX标签的缺省设置大概是这个样子:

OD载入WLVersion_WL.exe后,停在目标EP:

退出OD,最好将OD的UDD文件夹里WLVersion_WL.udd删除掉,防止以前调试时其他断点的干扰。
运行OD,先将SFX按下图设置,然后再载入WLVersion_WL.exe:

载入后目标会自动运行,在停下来的时候,就是"NEAR OEP"的位置,是不是很方便?不得不叹服OllyDBG的强大!

这个时候,Breakpoint菜单下会多一个选项“Set real SFX entry here”:

这个地址究竟对不对,我们验证一下,用OD载入附件里未保护的WLVersion.exe,比较如下图:

从OEP数下来,有15条指令被虚拟(“偷”)了,这是WL/TMD的缺省设置。
记住将SFX标签的设置还原。
重新载入WLVersion_WL.exe,跑LCF-AT的脚本,找到"VM OEP":

UV后的结果:
    ...
    004B3D5F    POP ESP
    004B3D60    PUSH EBP                                ; OEP
    004B3D67    MOV EBP,ESP
    004B3D7D    PUSH 0xff
    004B3DA9    PUSH 0x405100
    004B3DD0    PUSH 0x40204c
    004B3DF8    MOV EAX,DWORD PTR FS:[0x0]
    004B3E2D    PUSH EAX
    004B3E4A    MOV DWORD PTR FS:[0x0],ESP
    004B3E74    SUB ESP,0x58
    004B3E8C    PUSH EBX
    004B3E90    PUSH ESI
    004B3EAA    PUSH EDI
    004B3EAE    MOV DWORD PTR [EBP+0xffffffe8],ESP
    004B3EE2    CALL DWORD PTR [0x40506c]               ; [0x40506c]=7C8111DA kernel32.GetVersion

@Label_004B3EF4
    004B3F24    XOR EDX,EDX
    004B3F63    JMP 0x4013ae                            ; to "NEAR OEP"


2. 手工查找"SEH Record"的方法
记不清是从谁的教程学来的,没办法提及他的大名致谢。这个方法适用的目标情形更广泛一些(VC & MFC),以本帖目标为例。
1) OD载入目标,记录一下第一个SEH的位置和值。

2) 运行脚本Bypass,并过SDK函数再验证直到出现用户界面。这里我们假定还不知道"NEAR OEP",其他未用SDK函数的目标中运行脚本正常情况下会直接到用户界面。
3) 不用暂停,切换到OD。在CPU窗口的"Stack pane"中,定位到最下面,从记录的第一个SEH位置向上找第二个SEH。找到第二个SEH为0040DFEC,跟随它:

4) 向上找,应该不远。

在找的过程中,因为我们不知道"NEAR OEP"准确的位置,这时需要用到“类比法”——即参考目标的相同编程语言、编译连接版本等入口特征信息。同时,要善用OD的"CTRL+↑/CTRL+↓"热键。
再引用一下那帖中被虚拟的OEP指令:
...
    005C85AC    POP ESP                                 ; <-***
    005C85B4    PUSH EBP                                ; emulating OEP of MFC started here
    005C85BD    MOV EBP,ESP
    005C85D3    PUSH 0xff
    005C85D8    PUSH 0x411670
    005C8603    PUSH 0x40dfec                           ; SE handler installation
    005C863A    MOV EAX,DWORD PTR FS:[0x0]
    005C86AA    PUSH EAX
    005C86BD    MOV DWORD PTR FS:[0x0],ESP
    005C86CC    SUB ESP,0x68
    005C8732    PUSH EBX
    005C8747    PUSH ESI
    005C8755    PUSH EDI
    005C8768    MOV DWORD PTR [EBP+0xffffffe8],ESP      ; [EBP-18]
    005C8798    XOR EBX,EBX
    005C87F8    MOV DWORD PTR [EBP+0xfffffffc],EBX      ; [EBP-04]
    005C8867    JMP 0x40de8a                            ; back to the real world
...

同样有15条指令被虚拟了(WL的缺省设置),请注意将这些指令与上面那张图"Stack pane"中的内容对比一下。
显然,此方法也适用于SFX例子的WLVersion_WL.exe。
上传的附件:
2013-2-16 17:33
0
雪    币: 136
活跃值: (429)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
8
再次细读一次,GOOD JOB
2013-2-17 21:35
0
雪    币: 162
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
大牛呀,看起来Winlicense生成注册码也不安全呀,
2013-3-5 20:46
0
雪    币: 107
活跃值: (419)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
我艹这都没有加精还有天理没???

发哥赶紧来操作下..
2013-3-6 07:15
0
雪    币: 80
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
楼主厉害,此贴入门不错
2013-3-17 09:09
0
雪    币: 15
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
技术贴,必须顶。
2013-3-17 11:41
0
雪    币: 51
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
细读了文章,太好了,可是第二内鬼我无法抓出
2013-4-11 19:52
0
雪    币: 197
活跃值: (19)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
都是厉害的大牛们啊,偶只能看了
2013-4-11 21:59
0
雪    币: 510
活跃值: (483)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
15
难得的屌丝文,很好很好,,今天你撸了吗?
2013-4-12 09:42
0
雪    币: 1
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
学习学习学习!!!
2013-4-21 06:31
0
雪    币: 627
活跃值: (663)
能力值: ( LV9,RANK:270 )
在线值:
发帖
回帖
粉丝
17
有人email问问题,虽说是炒冷饭,还是将回复帖出来,希望对其他人有帮助:

时间久了,记不太清了,翻出来复习。

一.环境:
    OllyDbg v1.10 英文原版
    StringOD v0.4.6.816,我虚拟机现在是这个版本。新的v0.4.8.892应该也可以。
    ODbgScript v1.82.6

设置:


ollydbg.ini里[Plugin StrongOD]节,把驱动改名:
[FONT="Courier"][COLOR="Red"]DriverName=[/COLOR]stillHiM,这里原来是fengyue还是什么。[/FONT]

对付TMD/WL、VMP很管用。
暂时将其它插件从Plugins文件夹中移出,多了可能反而碍事。

二.我刚刚运行的结果
1. 脚本:Log.MistHill.V2.osc
[FONT="Courier"]Script Log Window
Address    Message
43111C     eax:0  ecx:0
43111C     eax:0  ecx:0
43111C     eax:0  ecx:0
43111C     eax:0  ecx:0
43111C     eax:[COLOR="Red"]557135FA[/COLOR]  ecx:32C724F2
43111C     eax:557135FA  ecx:557135FA
43111C     eax:[COLOR="Red"]560BC21[/COLOR]   ecx:7040FE04
43111C     eax:2E77EE79  ecx:7040FE04
43111C     eax:2DA04743  ecx:7040FE04
43111C     eax:1B84290B  ecx:7040FE04
43111C     eax:721F6913  ecx:7040FE04
43111C     eax:4BDB9C0E  ecx:7040FE04
43111C     eax:4DCD6E    ecx:7040FE04
43111C     eax:390A42C7  ecx:7040FE04
43111C     eax:390A42C7  ecx:390A42C7
43111C     eax:0  ecx:0[/FONT]

它的作用是Patch与557135FA的比较,找出560BC21。这些值的具体含义见原帖子。
脚本运行结束后,F9继续运行程序,对话框显示:“当前注册文件不是本机的,请重新注册。”

2. 脚本:WL ByPassII.MistHill.V3.osc
退出OD,重新载入,运行脚本。
正常地,出现脚本对话框:"You can follow my post from here. have fun!"
停在NEAR OEP:
[FONT="Courier"][COLOR="Red"]0040DE8A[/COLOR]   6A 02              PUSH    2
0040DE8C   90                 NOP
0040DE8D   E8 EA747F77        CALL    77C0537C                       ; msvcrt.__set_app_type
0040DE92   59                 POP     ECX
0040DE93   830D 644E4100 FF   OR      DWORD PTR [414E64], FFFFFFFF
0040DE9A   830D 684E4100 FF   OR      DWORD PTR [414E68], FFFFFFFF
0040DEA1   90                 NOP
0040DEA2   E8 34137E77        CALL    77BEF1DB                       ; msvcrt.__p__fmode
...[/FONT]

普通目标,这就算Bypass了。本例因为用户代码部分使用了SDK进行再次验证,还需要处理,具体见原帖的Demo。
这个脚本的作用就是随时Patch注册标记为由上面脚本找到的两个正确值:
[FONT="Courier"]mov Is_Registered_DWORD1, 557135FA
mov Is_Registered_DWORD2, 0560BC21[/FONT]


你得不到这个结果,很可能是调试环境的问题,物理机或虚拟机应该关系不大。

TMD/WL程序的结构就是这样:由实代码+虚代码的很多个块组成。CISC的WL目标通常有4个VMs(见我SDK那个帖子),断在各VM_EXIT_ADDR处收获很大,比如0x4246A0是VM1的Exit。
因为虚代码不能调用API,基本上在各Exit处可观察到它会调用哪个API。但是各VM里的条件判断会改变Exit处的返回地址,既条件不满足时会走错误的流程。

要深入分析VM,还是Deathway的Oreans UnVirtualizer。这需要极大的耐心和经验!
上传的附件:
2013-8-8 11:49
0
雪    币: 627
活跃值: (663)
能力值: ( LV9,RANK:270 )
在线值:
发帖
回帖
粉丝
18
前帖忘记贴出问题,不过不重要了,总之就是无法搞定。经过提醒,这个粗心的家伙承认弄错了目标。
他用的是帖子“这家伙那么恐怖吗?...”里原楼主的附件,而不是我Repack后的版本:
[FONT="Courier"]2012-07-02  19:42   1,855,488 GetDuxiuStr.exe   MD5:3ef63e1dcd1b82e5d40417b81bc93723
2012-12-12  12:29         517 regkey.dat        MD5:ca24cc79273d7e10fb91a74c1ac0a3ea[/FONT]
为什么要Repack?因为需要替换原目标里的两个公钥,这样我的key才能正常工作。

然后,问题又来了。

Q: 是怎么构造这个regkey.dat的呢?
A: 主要参考了hyperchem的经典“如何keygen一个WL保护的程序”。
刚接触WL时,我也是什么都不懂,教程看得“云里雾里的”。在你真正对一个问题感兴趣时,就能深入进去,伴随着学习过程,一遍又一遍的反复看,不放过任何一个细节和“金口玉言”,然后去找相关的资料。
后来总结出自己的一套方法,针对CISC的VM做了一个PDF,但从未公开。RISC虚拟机要繁琐一些,方法是类似的。

不公开是出于对Oreans的尊重,还有其大量合法用户的利益。讲一个真实的故事:
看雪某会员开发了一个.NET的应用,准备销售,购买了最新的WinLicense来保护和授权。看了我这两篇帖子后便“茶饭不思”,还转给Oreans。
便安慰他,没有这么恼火,再好的保护也有可能被攻破。并提了一些建议,比如使用RISC,懂的人会更少一些,与Oreans给他的意见一致。
另外个人感觉WinLicense对.NET并不合适,可考虑其他方案,比如用加密锁多一个保护,当然成本会有少量增加。还有国产的MaxtoCode;相比之,".NET Reactor"可能要贵一些等等。
聊了几次后,亮出我的底牌,希望得到新版WL的主程序,并保证不转给第三方。当然,他虽然有点纠结,但还是拒绝了我的请求,还说MaxtoCode的人也想要。
没关系,肯定地,换作我谁也不会给。比起IDA等来讲,几佰美刀并不算贵,但目前我没有这方面的需求,属业余玩票而已。

Q: "Oreans UnVirtualizer ODBG Plug-in"的使用方法?某论坛的版主做过的视频教程?使用插件时地址为啥填入0060E473?
A: 某版主的视频教程我不清楚。插件作者发过两个视频教程,分别关于CISC和RISC的,原连接早已失效。还好可以在“50UB 求有效的Oreans UnVirtualizer Sample下载地址”下载到。
使用工具的前提条件是,要对虚拟机有清晰的认识,只有靠自己努力,别人帮不了。
地址为啥是0060E473?我在“WinLicense SDK 应用实例分析”的15楼回答过。看帖要仔细,真正感兴趣的话,更应该关注讨论和回复。

Q: Oreans UnVirtualizer ODBG Plug-in下载地址?
A: 这就要批评了。善用论坛搜索!
替你搜了一下,见“Oreans UnVirtualizer 1.3”。
感谢linhanshi版主,更新了"Oreans UnVirtualizer 1.5 & 1.6"的下载。
2013-8-12 10:46
0
雪    币: 561
活跃值: (124)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
好   学习!!
2013-8-16 23:48
0
雪    币: 0
活跃值: (1013)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
mark
2013-8-22 18:57
0
雪    币: 110
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
看来只能膜拜了。。。先收藏
2013-8-22 21:20
0
雪    币: 14
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
对我来说,这样的文章有点难
2013-8-23 09:05
1
雪    币: 633
活跃值: (254)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
专程过来膜拜的
2020-8-5 10:42
0
雪    币: 639
活跃值: (1192)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
24
可2.4.6 的版本 特征应该变了吗?
lea eax,dword ptr ss:[ebp+0x1AF1A1CD]
JMP XXXXXX
那应该如何做呢??
2021-12-3 09:38
0
游客
登录 | 注册 方可回帖
返回
//