前段日子突然对分割商法有点兴趣,于是从 2dj
上把 9-nine
拖了下来。但如何运行却成了难题。由于资源本身是 PKG
版的,因此需要输入 CDKEY
,而显然这是我们所没有的,怎么办呢——
ToC
安装
因为资源提供的是 CD
镜像,所以安装这一步就遇到了麻烦,如图所示:
如果输入错误的话,这个「次へ」的按钮是不能点击的。
分析安装过程,我们在 %temp%
下找到了 nsispkc.dll
。用 IDA
打开,发现它的导出函数只有这个:
进入函数 F5
,发现了 OK
和 NG
:
于是我就把 ng
直接给 PATCH
成 ok
了,像这样:
这样不管是输入了什么产品 ID
都会认为是正确输入了(笑)
在 Patch
完这样的 dll
之后,安装的过程就只需要:
- 随便输入一个激活码
- 到
%TEMP%
里找到对应的.tmp
目录 - 覆盖目录中的
nsispkc.dll
- 再重新随便输个激活码
- 下一步!
就可以了。
运行:准备
安装时需要 CDKEY
,运行时自然也不例外。这里我们使用的是 KrkrExtract
,通过解包,我们发现了检查 CDKEY
的对应 tjs
:
1function ProductKeyCheckOne(config, pk) {2 try {3 pk = string(pk).toUpperCase().replace(/[^A-Z0-9]/g, "");4 //System.inform(input);5 var chk;6 with (config) chk = ProductKeyCheck(.len, pk, .pub, .chk, .sig);7 if (chk !== void && chk.length == config.bin) {8 var tag1 = "%02X%02X%02X%02X".sprintf(chk[7], chk[6], chk[5], chk[4]);9 var tag2 = "%08X".sprintf(config.tag);10 return (tag1 == tag2);11 }12 } catch {}13}14function ProductKeyInput(keyname) {15 return System.inputString(global.ENV_GameName, keyname+"を入力してください", "");16}17function ProductKeyCheckAndInput() {18 var config, keyname = "プロダクトキー";19 if (typeof global.ENV_PKeyName == "String" && global.ENV_PKeyName != "") keyname = global.ENV_PKeyName;20 try {21 Plugins.link("pkutil.dll");22 config = Scripts.evalStorage("pkeyconf.tjs");23 } catch {24 throw new Exception(@"${keyname}チェック処理の初期化に失敗しました");25 }26 // LCID check27 try {28 if (GetUserDefaultLCID() != 0x0411 &&29 GetSystemDefaultLCID() != 0x0411)30 throw new Exception("** JAPAN SALES ONLY. **");31 } catch {32 throw new Exception(@"${keyname}チェック処理の起動に失敗しました");33 }34 // DATE check35 if (typeof global.ENV_LaunchDate == "String" && global.ENV_LaunchDate != "") {36 if ((new Date()).getTime() < (new Date(global.ENV_LaunchDate)).getTime()) {37 throw new Exception(@"${keyname}チェック処理の起動に失敗しました。\nプログラムが実行できません");38 }39 }40
41
42 var key = @"\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${global.ENV_GameId}\\__ProductKey__";43
44 @if (FORCE_PRODUCTKEY)45 ProductKeyWriteRegistory("HKEY_CURRENT_USER"+key, "");46 @endif47
48 var pk = System.readRegValue("HKEY_LOCAL_MACHINE"+key);49 if (pk != "" && ProductKeyCheckOne(config, pk)) return true;50
51 /**/pk = System.readRegValue("HKEY_CURRENT_USER"+key);52 if (pk != "" && ProductKeyCheckOne(config, pk)) return true;53
54 while ((pk = ProductKeyInput(keyname)) !== void) {55 if (pk != "" && ProductKeyCheckOne(config, pk)) {56 ProductKeyWriteRegistory("HKEY_CURRENT_USER"+key, pk);57 return true;58 }59 }60 return false;61}62
63try {64 if (!ProductKeyCheckAndInput()) System.exit();65} catch (e) {66 try { Plugins.unlink("pkutil.dll"); } catch {}67 try { delete global.ProductKeyCheckOne; } catch {}68 try { delete global.ProductKeyInput; } catch {}69 try { delete global.ProductKeyCheckAndInput; } catch {}70 throw e;71}72try { Plugins.unlink("pkutil.dll"); } catch {}73try { delete global.ProductKeyCheckOne; } catch {}74try { delete global.ProductKeyInput; } catch {}75try { delete global.ProductKeyCheckAndInput; } catch {}
脚本的执行从第 65 行开始,进行了 ProductKeyCheckAndInput
。ProductKeyCheckAndInput
位于第 18 行,它做了如下的事情:
- 加载了
pkutil.dll
- 获得了
pkeyconf.tjs
中的config
- 判断了
GetUserDefaultLCID
和GetSystemDefaultLCID
的值是否为0x0411
- 读取了注册表,尝试获得存储在注册表中的
CDKEY
- 检查了激活码,并当检查失败时要求用户输入
检查激活码的过程本身调用了 ProductKeyCheck
。常规的思路应该是在 pkutil.dll
中寻找突破口,但逆向的结果并不如人意,让我充分认识到了我有多菜。于是新的想法就出现了:为什么不直接覆盖掉 pkutil.dll
的行为呢?
运行:实现
想要覆盖掉 pkutil.dll
的行为,我们就要去了解 krkr
的插件究竟该如何编写。好在官方的 SamplePlugin
仓库里有一个简单的例子,能够让我们一观实现的方式。
V2Link
我们知道,krkr
的插件都是以 dll
的形式存在的。krkr
约定以 V2Link
作为初始化入口,V2Unlink
作为反初始化入口。
V2Link
作为初始化入口,其所需要实现的内容非常简单,就是将插件中定义的行为以合适的方式放到 global
中供脚本调用。在官方的示例中我们可以看到:
1iTJSDispatch2* global = TVPGetScriptDispatch();
这行中获得的 global
就是脚本运行时的 global
上下文环境了。
在获得了 global
之后,接下来就只需要将对应的实现放入 global
就万事大吉了。代码如下:
1 ProductKeyCheckFunction = new tProductKeyCheckFunction();2 val = tTJSVariant(ProductKeyCheckFunction);3 ProductKeyCheckFunction->Release();4 global->PropSet(TJS_MEMBERENSURE, TJS_W("ProductKeyCheck"), nullptr, &val, global);
这里我们先不去深究 ProductKeyCheckFunction
和 tProductKeyCheckFunction
究竟从何而来,先看第二行。
第二行中我们将生成的 ProductKeyCheckFunction
转化成了 tTJSVariant
类型。这里其实就是将 C++
的静态类型转换成了 TJS
中的动态类型。
第三行的 Release
是为了释放引用计数而存在的,因为类型已经转化,因此将引用计数减一,从而使 ProductKeyCheckFunction
得以释放。
最后就是将这个构造得到的 tTJSVariant
放入 global
了。我们将这个值作为 global
的成员(MEMBERENSURE
)存储,并将其命名为 ProductKeyCheck
,hint
为空,内容为 val
,作用域(this
)为 global
。
就此,V2Link
里所需要完成工作的大致流程就介绍完了。
ProductKeyFunction
实现
接下来我们来看刚刚忽略的存在,即最关键的实现。
1// 检查 ProductKey 的函数2//---------------------------------------------------------------------------3class tProductKeyCheckFunction : public tTJSDispatch4{5 tjs_error TJS_INTF_METHOD FuncCall(6 tjs_uint32 flag, const tjs_char* membername, tjs_uint32* hint,7 tTJSVariant* result,8 tjs_int numparams, tTJSVariant** param, iTJSDispatch2* objthis);9} *ProductKeyCheckFunction;10//---------------------------------------------------------------------------11tjs_error TJS_INTF_METHOD tProductKeyCheckFunction::FuncCall(12 tjs_uint32 flag, const tjs_char* membername, tjs_uint32* hint,13 tTJSVariant* result,14 tjs_int numparams, tTJSVariant** param, iTJSDispatch2* objthis)15{16 if (membername) return TJS_E_MEMBERNOTFOUND;17
18 tTJSVariant pkeyconf;19 TVPExecuteStorage("pkeyconf.tjs", &pkeyconf, true);20
21 tTJSVariant tagResult;22 auto conf = pkeyconf.AsObject();23 conf->PropGet(0, TJS_W("tag"), nullptr, &tagResult, nullptr);24 tTVInteger tag = tagResult.AsInteger();25
26 iTJSDispatch2* array = TJSCreateArrayObject();27 {28 tTJSVariant value(0);29 array->PropSetByNum(0, 0, &value, array);30 }31 {32 tTJSVariant value(0);33 array->PropSetByNum(0, 1, &value, array);34 }35 {36 tTJSVariant value(0);37 array->PropSetByNum(0, 2, &value, array);38 }39 {40 tTJSVariant value(0);41 array->PropSetByNum(0, 3, &value, array);42 }43 {44 tTJSVariant value(tag & 0xff);45 array->PropSetByNum(0, 4, &value, array);46 }47 {48 tTJSVariant value((tag & 0xff00) >> 8);49 array->PropSetByNum(0, 5, &value, array);50 }51 {52 tTJSVariant value((tag & 0xff0000) >> 16);53 array->PropSetByNum(0, 6, &value, array);54 }55 {56 tTJSVariant value((tag & 0xff000000) >> 24);57 array->PropSetByNum(0, 7, &value, array);58 }59 {60 tTJSVariant value(0);61 array->PropSetByNum(0, 8, &value, array);62 }63 {64 tTJSVariant value(0);65 array->PropSetByNum(0, 9, &value, array);66 }67 {68 tTJSVariant value(0);69 array->PropSetByNum(0, 10, &value, array);70 }71 {72 tTJSVariant value(0);73 array->PropSetByNum(0, 11, &value, array);74 }75 {76 tTJSVariant value(0);77 array->PropSetByNum(0, 12, &value, array);78 }79
80 *result = tTJSVariant(array, array);81 array->Release();82 return TJS_S_OK;83}
观察第 3 行,我们定义的内容是继承 tTJSDispatch
的。并且作为函数,我们需要实现对应的 FuncCall
方法。具体的实现过程从 16 行开始,我们模仿检查 CDKEY
的 tjs
中的操作,通过 TVPExecuteStorage
拿到没有实际传入的 config
内容,并获得 tag
。在拿到 tag
之后,模仿 tjs
中的校验方式,将 tag
中的内容按需写入返回的 result
中就可以了。
值得一提的是,返回数组的大小之所以是 13,原因在于 CDKEY
本身是 base32
的方式存储的。因此长度就是:
1\lceil20\times5\div8\rceil=13
到这里,基本的实现就已经全部完成了。GetUserDefaultLCID
和 GetSystemDefaultLCID
的实现更加简单,这里就不再赘述了。
V2Unink
与 V2Link
相反,V2UnLink
就是为了从 global
中清除成员而存在的。这里直接贴出对应的源码,不多说了。
1extern "C" __declspec(dllexport) HRESULT _stdcall V2Unlink()2{3 if (TVPPluginGlobalRefCount > GlobalRefCountAtInit) return TJS_E_FAIL;4
5 iTJSDispatch2* global = TVPGetScriptDispatch();6
7 if (global)8 {9 global->DeleteMember(0, TJS_W("ProductKeyCheck"), nullptr, global);10 global->DeleteMember(0, TJS_W("GetUserDefaultLCID"), nullptr, global);11 global->DeleteMember(0, TJS_W("GetSystemDefaultLCID"), nullptr, global);12 }13
14 if (global) global->Release();15
16 TVPUninitImportStub();17
18 return TJS_S_OK;19}
运行:加载
到这里,代码编写的工作已经全部结束了,我们需要找一个方式载入。最显而易见的方式当然是覆盖掉 plugins
目录下的同名文件了,但总感觉这样太不优雅了。于是查阅官方文档,发现了以下的说明:
可以看到,插件的加载顺序中,主程序目录的优先级是比 plugins
高的。因此,写好的插件只要放到主目录下就没问题了(
检验
最后检验一下运行吧。我是以 9-nine-はるいろはるこいはるのかぜ
为原本开发的,因为这个版本 2dj
破解给出的文件不对。作为开发的原本,这作的运行自然是没有问题的。另外几作的 PKG
版,包括完整的版本我也测试过了,运行都没有什么问题。这一晚上的成果应该算是这个系列破解的通解了(笑)
不过需要注意的是,纵使实现中绕过了区域检测,在非转区的情形下运行依然存在问题。图片资源的加载失败会导致游戏卡死,因此转区还是必要的。
成果
最后的成果就是这个了,压缩包里有 PATCH
过的 nsispkc.dll
和 pkutil.dll
两个文件:
https://drive.google.com/file/d/1iemgFF13xqLE9M0JqLd-cUl825ai9Rkv/view?usp=sharing
源码的话有时间再丢吧,不过应该不会放到 GitHub
就是了gitea
又要启动了(笑)