惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
Cloudbric
Cloudbric
云风的 BLOG
云风的 BLOG
A
About on SuperTechFans
The GitHub Blog
The GitHub Blog
IT之家
IT之家
F
Full Disclosure
B
Blog RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hugging Face - Blog
Hugging Face - Blog
B
Blog
H
Help Net Security
The Cloudflare Blog
Recorded Future
Recorded Future
P
Proofpoint News Feed
P
Proofpoint News Feed
C
Cisco Blogs
T
Tailwind CSS Blog
P
Palo Alto Networks Blog
D
Docker
爱范儿
爱范儿
Know Your Adversary
Know Your Adversary
博客园 - 聂微东
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Y
Y Combinator Blog
雷峰网
雷峰网
AWS News Blog
AWS News Blog
D
DataBreaches.Net
博客园 - 司徒正美
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - Franky
C
Cybersecurity and Infrastructure Security Agency CISA
Blog — PlanetScale
Blog — PlanetScale
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Latest news
Latest news
Google DeepMind News
Google DeepMind News
Martin Fowler
Martin Fowler
MongoDB | Blog
MongoDB | Blog
C
CERT Recently Published Vulnerability Notes
阮一峰的网络日志
阮一峰的网络日志
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
C
CXSECURITY Database RSS Feed - CXSecurity.com
酷 壳 – CoolShell
酷 壳 – CoolShell
C
Cyber Attacks, Cyber Crime and Cyber Security
腾讯CDC
小众软件
小众软件
G
Google Developers Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Scott Helme
Scott Helme
O
OpenAI News

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Learning Pingora 05 - Connect with TLS | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 02: Astro❤️Password | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog 对博客与笔记的思考 | Yesterday17's Blog Project Anni 之旅(3)自动化 Flutter 应用 CI/CD 上架流程 | Yesterday17's Blog AsobiStage 直接播放链接 | Yesterday17 如何在后分P时代进行投稿——sswa使用详解 | Yesterday17's Blog JSON RPC 与 LSP 协议基础 | Yesterday17's Blog Grajapa Shueisha / BookEnd 加密方式调查 | Yesterday17's Blog 【2022篇+WriteUp】如何再收一个新年红包? | Yesterday17's Blog 如何将良心云的良心功能清理干净 | Yesterday17's Blog 【油猴脚本】bilibili 投稿页面返回旧版+旧版页面强制允许分P上传 | Yesterday17's Blog Cloudr1v1 授权方式分析 | Yesterday17 Typora 1.0.2 逆向实录 | Yesterday17's Blog Project Anni 之旅(2)ValueAfterTable——toml-rs的实现与限制 | Yesterday17's Blog IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记 | Yesterday17's Blog Chaos; Child 汉化补丁 神秘编码探索 | Yesterday17's Blog 镣铐与舞蹈——个性与共性之迷思 | Yesterday17's Blog Go 学习笔记 02 - 找准 io 之道 | Yesterday17's Blog NAT Slipstreaming v1 原理浅析 | Yesterday17's Blog 静流的青春纪念册——「サクラノ刻 -櫻の森の下を歩む-」体验版感言 | Yesterday17's Blog Project Anni 之旅 01 - 从 clap-builder 到 derive | Yesterday17's Blog [Google CTF 2021] CPP WriteUp | Yesterday17's Blog 获取 アソビステージ 的实际播放链接 | Yesterday17's Blog 90 行 Rust 代码实现 AsyncTeeReader | Yesterday17's Blog 或许还算有价值一读的文章列表 | Yesterday17's Blog 从零开始的 Seedbox 之旅 | Yesterday17's Blog [随笔]技术型博客行文迷思(1) | Yesterday17's Blog 浅谈 git fetch 的工作方式 | Yesterday17's Blog 『ソーサレス*アライヴ! ~the World's End Fallen Star~』通关感想" | Yesterday17's Blog Rust std::fmt 格式语法简述 | Yesterday17's Blog 日亚修改居住国的解决方案 | Yesterday17's Blog [Windows/Linux] GC553 的 Switch 完美采集之路 | Yesterday17's Blog 【翻译】Subtyping and Variance / 子类型与变型 | Yesterday17's Blog Berd's Red Envelope 2021 WriteUp | Yesterday17's Blog 【中英对照】ALSA 音频 API 使用教程/A Tutorial on Using the ALSA Audio API | Yesterday17's Blog 从 cue_scanner.l 看 CUE Sheet 的词法单元 | Yesterday17's Blog Postman 历史记录导出的解决方案 | Yesterday17's Blog 《恋爱绮谭 不存在的夏天》通关感想 | Yesterday17's Blog [微机实验/TD-PITE] 微机接口综合实验 | Yesterday17's Blog [微机实验/TD-PITE] 键盘扫描及数码管显示实验 | Yesterday17's Blog [微机实验/TD-PITE] 数码管显示实验 | Yesterday17's Blog Airsonic Advanced+Google Drive+Caddy 部署纪实 | Yesterday17's Blog X-NUCA 2020 - hellowasm 题解 | Yesterday17's Blog [微机实验/TD-PITE] 8251 串行接口实验 | Yesterday17's Blog Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog EP.01 「夜の向日葵」 | Yesterday17's Blog [微机实验/TD-PITE] 8254 定时/计数器实验+选做实验 | Yesterday17's Blog [JLU CTF/2020] babywasm WriteUp | Yesterday17's Blog PHP 反序列化与经典利用 | Yesterday17's Blog WebAssembly 逆向简述 | Yesterday17's Blog 『彼女、お借りします』一期完结点评 | Yesterday17's Blog [微机实验/TD-PITE] D/A 转换实验+选做实验 | Yesterday17's Blog [微机实验/TD-PITE] A/D 转换实验+选做实验 | Yesterday17's Blog 开源项目申请 JetBrains Open Source License 简单流程 | Yesterday17's Blog 微软拼音与 JetBrains 搜索快捷键冲突的解决方案 | Yesterday17's Blog [微机实验/TD-PITE] 8259 中断优先级实验+选做实验 | Yesterday17's Blog IFTTT 测试(续) | Yesterday17 IFTTT 测试 | Yesterday17's Blog [微机实验/TD-PITE] 存储器扩展实验+选做实验 | Yesterday17's Blog 新版 GCC 针对 -fdump-translation-unit 的替代方案 | Yesterday17's Blog 一次 HSTS 策略配置的排错之旅 | Yesterday17's Blog YukiNative 踩坑记——Windows 的消息队列 | Yesterday17's Blog 我是我自己——论获取 HTTPS 证书时的验证步骤 | Yesterday17's Blog 【设计文档】对 PUG 的大规模设计修订(1.1) | Yesterday17's Blog GS65 折腾记(2)加装固态,分区,Grub2 引导 Manjaro LiveCD | Yesterday17's Blog 「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录 | Yesterday17's Blog GSuite 探索篇(1)使用 Service Account 向 Google Drive 传输文件 | Yesterday17's Blog 『サクラノ詩 -櫻の森の上を舞う-』通关感想 | Yesterday17's Blog 《ATRI -My Dear Moments-》通关感想 | Yesterday17's Blog [工具][VSCode 扩展] AegiKit——方便 Aegisub 使用的工具箱 | Yesterday17's Blog 贝塞尔曲线、字体矢量化与曲线运算 | Yesterday17's Blog NAT 类型初探 | Yesterday17's Blog
绕过「9-nine-」的 CDKEY 验证——KrkrPlugin 正(?)向实录 | Yesterday17's Blog
Yesterday17 · 2021-09-12 · via Yesterday17's Blog

前段日子突然对分割商法有点兴趣,于是从 2dj 上把 9-nine 拖了下来。但如何运行却成了难题。由于资源本身是 PKG 版的,因此需要输入 CDKEY,而显然这是我们所没有的,怎么办呢——

ToC

  • 安装
  • 运行:准备
  • 运行:实现
    • V2Link
    • ProductKeyFunction 实现
    • V2Unink
  • 运行:加载
  • 检验
  • 成果
  • 参考

安装

因为资源提供的是 CD 镜像,所以安装这一步就遇到了麻烦,如图所示:

如果输入错误的话,这个「次へ」的按钮是不能点击的。

分析安装过程,我们在 %temp% 下找到了 nsispkc.dll。用 IDA 打开,发现它的导出函数只有这个:

进入函数 F5,发现了 OKNG

于是我就把 ng 直接给 PATCHok 了,像这样:

这样不管是输入了什么产品 ID 都会认为是正确输入了(笑)

Patch 完这样的 dll 之后,安装的过程就只需要:

  1. 随便输入一个激活码
  2. %TEMP% 里找到对应的 .tmp 目录
  3. 覆盖目录中的 nsispkc.dll
  4. 再重新随便输个激活码
  5. 下一步!

就可以了。

运行:准备

安装时需要 CDKEY,运行时自然也不例外。这里我们使用的是 KrkrExtract,通过解包,我们发现了检查 CDKEY 的对应 tjs

function ProductKeyCheckOne(config, pk) {

try {

pk = string(pk).toUpperCase().replace(/[^A-Z0-9]/g, "");

//System.inform(input);

var chk;

with (config) chk = ProductKeyCheck(.len, pk, .pub, .chk, .sig);

if (chk !== void && chk.length == config.bin) {

var tag1 = "%02X%02X%02X%02X".sprintf(chk[7], chk[6], chk[5], chk[4]);

var tag2 = "%08X".sprintf(config.tag);

return (tag1 == tag2);

}

} catch {}

}

function ProductKeyInput(keyname) {

return System.inputString(global.ENV_GameName, keyname+"を入力してください", "");

}

function ProductKeyCheckAndInput() {

var config, keyname = "プロダクトキー";

if (typeof global.ENV_PKeyName == "String" && global.ENV_PKeyName != "") keyname = global.ENV_PKeyName;

try {

Plugins.link("pkutil.dll");

config = Scripts.evalStorage("pkeyconf.tjs");

} catch {

throw new Exception(@"${keyname}チェック処理の初期化に失敗しました");

}

// LCID check

try {

if (GetUserDefaultLCID() != 0x0411 &&

GetSystemDefaultLCID() != 0x0411)

throw new Exception("** JAPAN SALES ONLY. **");

} catch {

throw new Exception(@"${keyname}チェック処理の起動に失敗しました");

}

// DATE check

if (typeof global.ENV_LaunchDate == "String" && global.ENV_LaunchDate != "") {

if ((new Date()).getTime() < (new Date(global.ENV_LaunchDate)).getTime()) {

throw new Exception(@"${keyname}チェック処理の起動に失敗しました。\nプログラムが実行できません");

}

}

var key = @"\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${global.ENV_GameId}\\__ProductKey__";

@if (FORCE_PRODUCTKEY)

ProductKeyWriteRegistory("HKEY_CURRENT_USER"+key, "");

@endif

var pk = System.readRegValue("HKEY_LOCAL_MACHINE"+key);

if (pk != "" && ProductKeyCheckOne(config, pk)) return true;

/**/pk = System.readRegValue("HKEY_CURRENT_USER"+key);

if (pk != "" && ProductKeyCheckOne(config, pk)) return true;

while ((pk = ProductKeyInput(keyname)) !== void) {

if (pk != "" && ProductKeyCheckOne(config, pk)) {

ProductKeyWriteRegistory("HKEY_CURRENT_USER"+key, pk);

return true;

}

}

return false;

}

try {

if (!ProductKeyCheckAndInput()) System.exit();

} catch (e) {

try { Plugins.unlink("pkutil.dll"); } catch {}

try { delete global.ProductKeyCheckOne; } catch {}

try { delete global.ProductKeyInput; } catch {}

try { delete global.ProductKeyCheckAndInput; } catch {}

throw e;

}

try { Plugins.unlink("pkutil.dll"); } catch {}

try { delete global.ProductKeyCheckOne; } catch {}

try { delete global.ProductKeyInput; } catch {}

try { delete global.ProductKeyCheckAndInput; } catch {}

脚本的执行从第 65 行开始,进行了 ProductKeyCheckAndInputProductKeyCheckAndInput 位于第 18 行,它做了如下的事情:

  1. 加载了 pkutil.dll
  2. 获得了 pkeyconf.tjs 中的 config
  3. 判断了 GetUserDefaultLCIDGetSystemDefaultLCID 的值是否为 0x0411
  4. 读取了注册表,尝试获得存储在注册表中的 CDKEY
  5. 检查了激活码,并当检查失败时要求用户输入

检查激活码的过程本身调用了 ProductKeyCheck。常规的思路应该是在 pkutil.dll 中寻找突破口,但逆向的结果并不如人意,让我充分认识到了我有多菜。于是新的想法就出现了:为什么不直接覆盖掉 pkutil.dll 的行为呢?

运行:实现

想要覆盖掉 pkutil.dll 的行为,我们就要去了解 krkr 的插件究竟该如何编写。好在官方的 SamplePlugin 仓库里有一个简单的例子,能够让我们一观实现的方式。

我们知道,krkr 的插件都是以 dll 的形式存在的。krkr 约定以 V2Link 作为初始化入口,V2Unlink 作为反初始化入口。

V2Link 作为初始化入口,其所需要实现的内容非常简单,就是将插件中定义的行为以合适的方式放到 global 中供脚本调用。在官方的示例中我们可以看到:

iTJSDispatch2* global = TVPGetScriptDispatch();

这行中获得的 global 就是脚本运行时的 global 上下文环境了。

在获得了 global 之后,接下来就只需要将对应的实现放入 global 就万事大吉了。代码如下:

ProductKeyCheckFunction = new tProductKeyCheckFunction();

val = tTJSVariant(ProductKeyCheckFunction);

ProductKeyCheckFunction->Release();

global->PropSet(TJS_MEMBERENSURE, TJS_W("ProductKeyCheck"), nullptr, &val, global);

这里我们先不去深究 ProductKeyCheckFunctiontProductKeyCheckFunction 究竟从何而来,先看第二行。

第二行中我们将生成的 ProductKeyCheckFunction 转化成了 tTJSVariant 类型。这里其实就是将 C++ 的静态类型转换成了 TJS 中的动态类型。

第三行的 Release 是为了释放引用计数而存在的,因为类型已经转化,因此将引用计数减一,从而使 ProductKeyCheckFunction 得以释放。

最后就是将这个构造得到的 tTJSVariant 放入 global 了。我们将这个值作为 global成员MEMBERENSURE)存储,并将其命名为 ProductKeyCheckhint 为空,内容为 val,作用域(this)为 global

就此,V2Link 里所需要完成工作的大致流程就介绍完了。

ProductKeyFunction 实现

接下来我们来看刚刚忽略的存在,即最关键的实现。

// 检查 ProductKey 的函数

//---------------------------------------------------------------------------

class tProductKeyCheckFunction : public tTJSDispatch

{

tjs_error TJS_INTF_METHOD FuncCall(

tjs_uint32 flag, const tjs_char* membername, tjs_uint32* hint,

tTJSVariant* result,

tjs_int numparams, tTJSVariant** param, iTJSDispatch2* objthis);

} *ProductKeyCheckFunction;

//---------------------------------------------------------------------------

tjs_error TJS_INTF_METHOD tProductKeyCheckFunction::FuncCall(

tjs_uint32 flag, const tjs_char* membername, tjs_uint32* hint,

tTJSVariant* result,

tjs_int numparams, tTJSVariant** param, iTJSDispatch2* objthis)

{

if (membername) return TJS_E_MEMBERNOTFOUND;

tTJSVariant pkeyconf;

TVPExecuteStorage("pkeyconf.tjs", &pkeyconf, true);

tTJSVariant tagResult;

auto conf = pkeyconf.AsObject();

conf->PropGet(0, TJS_W("tag"), nullptr, &tagResult, nullptr);

tTVInteger tag = tagResult.AsInteger();

iTJSDispatch2* array = TJSCreateArrayObject();

{

tTJSVariant value(0);

array->PropSetByNum(0, 0, &value, array);

}

{

tTJSVariant value(0);

array->PropSetByNum(0, 1, &value, array);

}

{

tTJSVariant value(0);

array->PropSetByNum(0, 2, &value, array);

}

{

tTJSVariant value(0);

array->PropSetByNum(0, 3, &value, array);

}

{

tTJSVariant value(tag & 0xff);

array->PropSetByNum(0, 4, &value, array);

}

{

tTJSVariant value((tag & 0xff00) >> 8);

array->PropSetByNum(0, 5, &value, array);

}

{

tTJSVariant value((tag & 0xff0000) >> 16);

array->PropSetByNum(0, 6, &value, array);

}

{

tTJSVariant value((tag & 0xff000000) >> 24);

array->PropSetByNum(0, 7, &value, array);

}

{

tTJSVariant value(0);

array->PropSetByNum(0, 8, &value, array);

}

{

tTJSVariant value(0);

array->PropSetByNum(0, 9, &value, array);

}

{

tTJSVariant value(0);

array->PropSetByNum(0, 10, &value, array);

}

{

tTJSVariant value(0);

array->PropSetByNum(0, 11, &value, array);

}

{

tTJSVariant value(0);

array->PropSetByNum(0, 12, &value, array);

}

*result = tTJSVariant(array, array);

array->Release();

return TJS_S_OK;

}

观察第 3 行,我们定义的内容是继承 tTJSDispatch 的。并且作为函数,我们需要实现对应的 FuncCall 方法。具体的实现过程从 16 行开始,我们模仿检查 CDKEYtjs 中的操作,通过 TVPExecuteStorage 拿到没有实际传入的 config 内容,并获得 tag。在拿到 tag 之后,模仿 tjs 中的校验方式,将 tag 中的内容按需写入返回的 result 中就可以了。

值得一提的是,返回数组的大小之所以是 13,原因在于 CDKEY 本身是 base32 的方式存储的。因此长度就是:

\lceil20\times5\div8\rceil=13

到这里,基本的实现就已经全部完成了。GetUserDefaultLCIDGetSystemDefaultLCID 的实现更加简单,这里就不再赘述了。

V2Unink

V2Link 相反,V2UnLink 就是为了从 global 中清除成员而存在的。这里直接贴出对应的源码,不多说了。

extern "C" __declspec(dllexport) HRESULT _stdcall V2Unlink()

{

if (TVPPluginGlobalRefCount > GlobalRefCountAtInit) return TJS_E_FAIL;

iTJSDispatch2* global = TVPGetScriptDispatch();

if (global)

{

global->DeleteMember(0, TJS_W("ProductKeyCheck"), nullptr, global);

global->DeleteMember(0, TJS_W("GetUserDefaultLCID"), nullptr, global);

global->DeleteMember(0, TJS_W("GetSystemDefaultLCID"), nullptr, global);

}

if (global) global->Release();

TVPUninitImportStub();

return TJS_S_OK;

}

运行:加载

到这里,代码编写的工作已经全部结束了,我们需要找一个方式载入。最显而易见的方式当然是覆盖掉 plugins 目录下的同名文件了,但总感觉这样太不优雅了。于是查阅官方文档,发现了以下的说明:

可以看到,插件的加载顺序中,主程序目录的优先级是比 plugins 高的。因此,写好的插件只要放到主目录下就没问题了(

检验

最后检验一下运行吧。我是以 9-nine-はるいろはるこいはるのかぜ 为原本开发的,因为这个版本 2dj 破解给出的文件不对。作为开发的原本,这作的运行自然是没有问题的。另外几作的 PKG 版,包括完整的版本我也测试过了,运行都没有什么问题。这一晚上的成果应该算是这个系列破解的通解了(笑)

不过需要注意的是,纵使实现中绕过了区域检测,在非转区的情形下运行依然存在问题。图片资源的加载失败会导致游戏卡死,因此转区还是必要的。

成果

最后的成果就是这个了,压缩包里有 PATCH 过的 nsispkc.dllpkutil.dll 两个文件:

https://drive.google.com/file/d/1iemgFF13xqLE9M0JqLd-cUl825ai9Rkv/view?usp=sharing

源码的话有时间再丢吧,不过应该不会放到 GitHub 就是了毕竟容易被发现。估计自建 gitea 又要启动了(笑)

参考

  1. 解包工具:https://github.com/xmoeproject/KrkrExtract/releases/tag/4.0.1.5
  2. 插件实现样例:https://github.com/krkrz/SamplePlugin/blob/master/basetest/Main.cpp
  3. https://iyn.me/i/post-45.html
  4. 插件相关文档:https://krkrz.github.io/krkr2doc/kr2doc/contents/Plugins.html