工具:原版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直播授课
上传的附件: