前言
在论坛的搜索游戏处,拿一份模拟器版本的xp3,garbro解包它
因为模拟器包含了游戏的全部文件,且未加密,用于恢复电脑版本的参考
如果你愿意自己手动分类也没关系,我们的重点是制作适用于Garbro通用dat
本文参考了https://www.kungal.com/topic/2670目的是制作详细教学方便小白入手
本文补充了大量的具体技术细节
1.使用Python汇总解包后游戏文件
源码如下——豆包生成,本文代码等内容都遵循MIT协议
import os
import sys
import time
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
# 自然排序函数
def natural_sort_key(s):
import re
return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', s)]
# 核心文件提取逻辑
def extract_files(root_dir, output_path, recursive=True):
skip_files = {'.DS_Store', 'Thumbs.db', 'desktop.ini'}
skip_prefixes = ('~$', '.tmp', 'temp_')
total_count = 0
start_time = time.time()
try:
with open(output_path, "w", encoding="utf-16-le", newline="\n") as f:
for root, dirs, files in os.walk(root_dir):
sorted_files = sorted(files, key=natural_sort_key)
for file in sorted_files:
if file in skip_files or file.startswith(skip_prefixes):
continue
f.write(file + "\n")
total_count += 1
total_time = time.time() - start_time
result_msg = f"成功提取 {total_count} 个文件\n"
result_msg += f"TXT保存路径:{output_path}\n"
result_msg += f"总耗时:{total_time:.1f} 秒"
return True, result_msg
except Exception as e:
error_msg = f"提取失败:{str(e)}\n请检查路径权限或文件名是否有特殊字符"
return False, error_msg
# 主GUI界面
def main_gui():
# 创建主窗口
root = tk.Tk()
root.title("文件列表提取工具")
root.geometry("550x220")
root.resizable(False, False) # 禁止调整窗口大小
# 界面组件
# 标题标签
title_label = ttk.Label(root, text="文件列表提取工具", font=("微软雅黑", 14, "bold"))
title_label.pack(pady=10)
# 进度提示
progress_label = ttk.Label(root, text="点击下方按钮选择文件夹开始提取", font=("微软雅黑", 10))
progress_label.pack(pady=5)
# 进度条
progress_bar = ttk.Progressbar(root, mode='indeterminate', length=450)
progress_bar.pack(pady=5, padx=20)
progress_bar.pack_forget() # 初始隐藏
# 开始提取按钮
def start_extraction():
# 选择目标文件夹
target_dir = filedialog.askdirectory(title="选择要提取文件列表的文件夹")
if not target_dir:
return
# 选择保存路径
save_path = filedialog.asksaveasfilename(
title="保存文件列表",
defaultextension=".txt",
filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")],
initialfile="文件列表.txt"
)
if not save_path:
return
# 更新界面状态
progress_label.config(text="正在提取文件...请耐心等待")
progress_bar.pack(pady=5, padx=20)
progress_bar.start()
root.update_idletasks() # 立即刷新界面
# 执行提取
success, msg = extract_files(target_dir, save_path)
# 恢复界面状态并显示结果
progress_bar.stop()
progress_bar.pack_forget()
if success:
progress_label.config(text="提取完成!可点击按钮继续操作")
messagebox.showinfo("操作成功", msg)
else:
progress_label.config(text="提取失败!请检查错误信息")
messagebox.showerror("操作失败", msg)
start_btn = ttk.Button(
root,
text="选择文件夹并开始提取",
command=start_extraction,
width=30
)
start_btn.pack(pady=15)
# 退出按钮
exit_btn = ttk.Button(
root,
text="退出程序",
command=root.quit,
width=15
)
exit_btn.pack(pady=5)
# 运行主循环
root.mainloop()
# 主程序入口
if __name__ == "__main__":
# 命令行模式:如果传入路径参数,直接执行不显示GUI
if len(sys.argv) == 2:
target_dir = os.path.abspath(sys.argv[1])
if not os.path.exists(target_dir) or not os.path.isdir(target_dir):
print(f"错误:路径无效或不是文件夹 → {target_dir}")
sys.exit(1)
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
save_path = os.path.join(script_dir, "文件列表.txt")
print(f"命令行模式:开始提取 {target_dir}")
success, msg = extract_files(target_dir, save_path)
print(msg)
sys.exit(0 if success else 1)
# GUI模式:双击运行直接显示主界面
else:
try:
main_gui()
except Exception as e:
print(f"GUI启动失败:{str(e)}")
messagebox.showerror("错误", f"无法启动程序:{str(e)}")
sys.exit(1)
使用方法双击打开py用gui选择路径或
命令行python 程序.py 文件夹路径
创建汇总文件的txt是以utf-16le形式储存,若想查看系统txt直接打开会出现问题,系统自带txt的是以utf-8格式读取,建议subline text来查看
utf-16l采取的原因是游戏内部分文件是以日文来命名的,当使用utf-8格式txt输入cxdec撞库会以乱码的形式扔进去,无法获取映射表
2.dump游戏解密
dumpkey源码如下
复制源码创建txt粘贴内容改名保存为krkr_hxv4_dumpkey.js
/**
* dump wamsoft hxv4 keys (hx decrypt index, cx decrypt index)
* v0.1.1, developed by devseed
*
* usage:
* npm i @types/frida-gum --save
* frida -l krkr_hxv4_dumpkey.js -f dc5ph.exe # frida version 17.2.4
* (the key will show on console, block will dump to control_block.bin)
*
* tested games:
* D.C.5 Plus Happiness ~ダ・カーポ5~プラスハピネス
* エッチで一途なド田舎兄さまと、古式ゆかしい病弱妹 (dlsite, steam)
* KANADE
* 花束を君に贈ろう-Kinsenka-
*
* refer:
* https://github.com/crskycode/GARbro/blob/master/ArcFormats/KiriKiri/HxCrypt.cs
*/
'use strict'
/**
* @param {ArrayBuffer} buf
*/
function buf2hexstr(buf, sep="") {
const arr = new Uint8Array(buf);
const hexs = [];
for(let i=0; i<arr.length; i++) {
let hex = arr[i].toString(16);
hex = ('00' + hex).slice(-2);
hexs.push(hex);
}
return hexs.join(sep);
}
var dllpath;
var cxtpm_load_flag = false;
// change this to frida breaking change in 17.0
// const LoadLibraryW = Module.getExportByName('kernel32.dll', 'LoadLibraryW');
const LoadLibraryW = Process.getModuleByName('kernel32.dll').getExportByName('LoadLibraryW')
Interceptor.attach(LoadLibraryW, {
onEnter(args) {
dllpath = args[0].readUtf16String();
if(dllpath.search("krkr_") > 0) cxtpm_load_flag = true;
},
onLeave(retval) {
if(cxtpm_load_flag==false) return;
cxtpm_load_flag = false;
let m; // change this to frida breaking change in 17.0
var hmod = Process.findModuleByAddress(ptr(retval.toUInt32()));
console.log(`load ${dllpath} at 0x${hmod.base.toString(16)}`);
// .text:1001F0B0 55 push ebp
// .text:1001F0B1 8B EC mov ebp, esp
// .text:1001F0B3 81 EC D4 00 00 00 sub esp, 0D4h
// .text:1001F0B9 A1 48 B2 0A 10 mov eax, ___security_cookie
// .text:1001F0BE 33 C5 xor eax, ebp
// .text:1001F0C0 89 45 FC mov [ebp+var_4], eax
// .text:1001F0C3 8B 45 14 mov eax, [ebp+key] // [ebp+14h] key, [ebp+18h] nonce
// .text:1001F0C6 53 push ebx
// .text:1001F0C7 56 push esi
// .text:1001F0C8 8B 75 08 mov esi, [ebp+this]
// .text:1001F0CB 57 push edi
// .text:1001F0CC 50 push eax
// .text:1001F0CD 8D 85 7C FF FF FF lea eax, [ebp+state0]
var hxpoint = 0; // decrypt hx index
m = Memory.scanSync(hmod.base, hmod.size, "8B 45 14 53 56 8B 75 08 57 50");
if(m.length == 1) hxpoint = m[0].address;
console.log(`hxpoint at 0x${hxpoint.toUInt32().toString(16)}`);
Interceptor.attach(hxpoint, {
onEnter(args){
if(!hxpoint) return;
let key = this.context.ebp.add(0x14).readPointer().readByteArray(32);
let nonce = this.context.ebp.add(0x18).readPointer().readByteArray(16);
console.log(`* key : ${buf2hexstr(key)}`);
console.log(`* nonce : ${buf2hexstr(nonce)}`);
hxpoint = 0;
}});
// 7B5B3C60 | 55 | push ebp |
// 7B5B3C61 | 8BEC | mov ebp,esp |
// 7B5B3C63 | 83EC 34 | sub esp,34 |
// 7B5B3C66 | A1 48B2647B | mov eax,dword ptr ds:[7B64B248] |
// 7B5B3C6B | 33C5 | xor eax,ebp |
// 7B5B3C6D | 8945 FC | mov dword ptr ss:[ebp-4],eax |
// 7B5B3C70 | 807D 10 00 | cmp byte ptr ss:[ebp+10],0 |
// 7B5B3C74 | 53 | push ebx |
// 7B5B3C75 | 56 | push esi |
// 7B5B3C76 | 8B75 08 | mov esi,dword ptr ss:[ebp+8] |
// 7B5B3C79 | 57 | push edi |
// 7B5B3C7A | 8B7D 0C | mov edi,dword ptr ss:[ebp+C] |
// 7B5B3C7D | 8BD9 | mov ebx,ecx | ecx:"ツ0"
var cxpoint = 0; // decrypt cx content
m = Memory.scanSync(hmod.base, hmod.size, "89 45 fc 80 7D 10 00");
if(m.length == 1) cxpoint = m[0].address;
console.log(`cxpoint at 0x${cxpoint.toUInt32().toString(16)}`);
Interceptor.attach(cxpoint, {
onEnter(args){
if(!cxpoint) return;
let filterkey = this.context.ecx.add(0x8).readByteArray(8);
let mask = this.context.ecx.add(0x10).readU32();
let offset = this.context.ecx.add(0x14).readU32();
let randtype = this.context.ecx.add(0x18).readU8();
let block = this.context.ecx.add(0x20).readByteArray(4096);
let order = this.context.ecx.add(0x3020).readByteArray(0x11);
console.log(`* filterkey : ${buf2hexstr(filterkey)}`);
console.log(`* mask : 0x${mask.toString(16)}`);
console.log(`* offset : 0x${offset.toString(16)}`);
console.log(`* randtype : ${randtype.toString()}`);
console.log(`* order : ${buf2hexstr(order, " ")}`);
File.writeAllBytes("control_block.bin", block);
// order compatible for garbro
const O = new Uint8Array(order);
const S3 = [0, 1, 2];
const S6 = [2, 5, 3, 4, 1, 0];
const S8 = [0, 2, 3, 1, 5, 6, 7, 4];
let O3 = [0, 1, 2];
let O6 = [0, 1, 2, 3, 4, 5];
let O8 = [0, 1, 2, 3, 4, 5, 6, 7];
for (let i=0; i<3; i++) O3[O[14+i]]=S3[i];
for (let i=0; i<6; i++) O6[O[8+i]]=S6[i];
for (let i=0; i<8; i++) O8[O[i]]=S8[i];
console.log(`* PrologOrder (garbro) : ${O3[0]}, ${O3[1]}, ${O3[2]}`);
console.log(`* OddBranchOrder (garbro) : ${O6[0]}, ${O6[1]}, ${O6[2]}, ${O6[3]}, ${O6[4]}, ${O6[5]}`);
console.log(`* EvenBranchOrder (garbro) : ${O8[0]}, ${O8[1]}, ${O8[2]}, ${O8[3]}, ${O8[4]}, ${O8[5]}, ${O8[6]}, ${O8[7]}`);
cxpoint = 0;
}});
}
});
源码:https://github.com/YuriSizuku/GalgameReverse/blob/master/project/krkr/src/krkr_hxv4_dumpkey.js
3.环境配置
电脑需有python
pip install frida-tools
4.将krkr_hxv4_dumpkey.js复制到游戏目录内
Steam版本的游戏提取需去除DRM后使用
这里就拿魔女的夜宴官中来演示吧
附带讲讲
Steamless打开游戏exe

选1257项
点Unpack File
出现绿色字,提取成功
游戏目录会有一个叫SabbatOfTheWitch.exe.unpacked.exe
将Steam通用api复制到游戏目录替换

请注意测试游戏打开时显示这个
两个报错的原因是没绕过程序startup.tjs

通常情况下柚子社的新游戏解决后会遇到此类情况
但是使用fuckcxdec所生成的Version.dll会阻止后续撞库打开exe
请于网盘工具箱查找
Malformed exe/dll detected修复程序
链接: https://pan.baidu.com/s/1DFuJ9ro9XYVD8W9ApV92vw 提取码: imtw
5.提取
先提取key再提取游戏哈希目录
命令行运行命令,注意记得改一下参数的exe名字!!!
frida -l krkr_hxv4_dumpkey.js -f 游戏.exe
运行后cmd会有如下信息,这就是魔女的夜宴官中的解密参数
不要傻到拿我文章得到的这个参数去套别的游戏,那肯定行不通......
然后把窗口的信息全部复制保存到txt里头
____
/ _ | Frida 17.3.2 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Local System (id=local)
Spawning `SabbatOfTheWitch.exe`...
Spawned `SabbatOfTheWitch.exe`. Resuming main thread!
[Local::SabbatOfTheWitch.exe ]-> load c:\users\用户名\appdata\local\temp\krkr_24b154f5970d_584099171_10304\38a43677e8d5.dll at 0x7c000000
hxpoint at 0x7c01f0c3
cxpoint at 0x7c013c6d
* key : e6662ea4b50ccd083d56e13e0bd52ef3a75048052ccc77d57d1bc5a873e0bf14
* nonce : fefe820b57060e50b7cc2580db04d993
* filterkey : d99230e02623f4a0
* mask : 0x226
* offset : 0x1c8
* randtype : 1
* order : 04 06 02 00 07 01 03 05 03 00 05 04 02 01 01 02 00
* PrologOrder (garbro) : 2, 0, 1
* OddBranchOrder (garbro) : 5, 0, 1, 2, 4, 3
* EvenBranchOrder (garbro) : 1, 6, 3, 7, 0, 4, 2, 5
那么这个参数回保存在游戏目录下的key_output.txt,并还有一个文件是control_block.bin
这两个文件Garbro制作参数时会使用到
接下来使用CxdecExtractorLoader拿到加密的文件夹映射表
将游戏程序拖到CxdecExtractorLoader.exe
点第二个加载字符串Hash
游戏目录下的
StringHashDumper_Output会有DirectoryHash.log这个就是我们要的
有部分是空目录对于Windows系统无意义
%EmptyString%##YSig##94D4A97C61498621 %EmptyString%##YSig##94D4A97C61498621 %EmptyString%##YSig##94D4A97C61498621 %EmptyString%##YSig##94D4A97C61498621 %EmptyString%##YSig##94D4A97C61498621 %EmptyString%##YSig##94D4A97C61498621
这几个没用的%EmptyString%带这些的行可以删除掉
游戏开久了就会有这个,反选复制前面的内容覆盖掉即可
lst映射表格式要求
文件:
CF9D48435E0122C5CFB2BB9ACA41DAE27996035E5374D2A6B8BDA5ABF9C2FEBC:ama_102_0030.ogg
文件夹:
A174FE004F5BC2DD:bgimage/
说人话就是
经CXDEC加密的文件名:正确文件.png
经CXDEC加密的文件夹名:正确的文件夹名字/
6.撞库
前面提到的用python汇总到的文件名集合也就是文件列表.txt
将其改名为files.txt
https://github.com/YuriSizuku/GalgameReverse/blob/master/project/krkr/src/krkr_hxv4_dumphash.cpp
已编译好dll,要下的看到最后面下载链接处
将version.dll与前面汇总拿到的files.txt复制到游戏目录
网盘工具箱
所有所需的文件在此处


大佬我是少东西了吗?为啥说tee不是命令?

更改了version.dll好像无法运行游戏了#