关于KrKrZ的CXDEC加密大批量提取解包游戏文件的思路与方法技术的总结初步

逆向工程 实用技术 算法CXDEChvx4柚子社warmsoft魔女的夜宴天使纷扰柠檬即兴曲
浏览数 - 1521发布于 - 2025-11-29 - 14:04

重新编辑于 - 2025-12-01 - 20:08

前言

对照参考表制作需要在论坛的搜索游戏处,拿一份模拟器版本的xp3,garbro解包它

而不是把xp3汇总扔游戏撞库

模拟器版本实时dump出来整理的打包xp3

包含了游戏的全部文件,且未加密,用于恢复电脑版本的参考,免得二次重复性读取游戏还不全(ctrl的情况)

一个个点跑游戏跑断腿吧

如果没有现成的参考副本那就老老实实跑游戏吧

如果你愿意自己手动分类模拟器文件也没关系,模拟器版本大多都是一个包下一堆文件全塞在一个包的目录

慢慢分?耗费时间太长了

所以重点是制作适用于Garbro的通用dat拿到映射表格以快速提取游戏文件

本文参考了https://www.kungal.com/topic/2670

其基本阐述了如何恢复的过程

目的是制作详细教学方便小白入手

说来容易而实际上你要做的事情可不止这么简单

本文补充了大量的具体技术细节

注:本文代码等内容都遵循MIT协议,仅供逆向汉化学习参考,不得用于其他用途

1.使用Python汇总模拟器版本解包后游戏文件

源码如下——豆包生成,

Python
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撞库会以乱码的形式扔进去,无法制作获取映射表的

CXDEC只负责把字符串转变为哈希

你即使输入个1145141919810.png也会有hash

2.dump游戏解密

dumpkey源码如下

如果懒得自己保存了,翻到最下面的github下源码js

或者自己愿意动一下手的话复制源码创建txt粘贴内容改名保存为krkr_hxv4_dumpkey.js

javascript
/**
 * 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-
 */

'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

Python
pip install frida-tools

4.将krkr_hxv4_dumpkey.js复制到游戏目录内

Steam版本的游戏提取需去除DRM后使用

这里就拿魔女的夜宴官中来演示吧

附带讲讲

Steamless打开游戏exe

image.png

选1257项

点Unpack File

image.png出现绿色字,提取成功

image.png游戏目录会有一个叫SabbatOfTheWitch.exe.unpacked.exe

将Steam通用api复制到游戏目录替换

image.png

请注意测试游戏打开时显示这个

覆盖后仍

报错这个然后点确定后弹

image.png

或者直接报Malformed exe/dll detected

通常情况下Steam官中脱壳后或HIKARI FIELD CLIENT下载的游戏程序拖入到提取程序会提示

原因是没绕过程序startup与bootstart的验证

例如说柚子社组乐队的新作,有软电池验证,通常用winHex删除KrKr2前面的软电池,直接删了没事,KrKrZ删除了前面的软电池程序的字节,从而让bootstart检测到程序损坏

所以遇到这种情况使用反编译器xdg32那些或者改bootstart也行,工具箱的讲解用32xdg可以快速解决

实在不会做就拿网盘内提供steamapi加经xdg32处理过的游戏程序补丁,打上覆盖即可继续下一步

针对柚子社还有部分krrkZ的程序,下下来的补丁不放心可使用火绒等杀毒软件扫描

若盘内无支持游戏,请评论发issue

5.提取

先提取key再提取游戏哈希目录

命令行运行命令,注意记得改一下参数的exe名字!!!

Textile
frida -l krkr_hxv4_dumpkey.js -f 游戏.exe

如果想要保存为txt请使用:frida -l krkr_hxv4_dumpkey.js -f 游戏.exe > output.txt 2>&1

部分系统无法使用具体原因未知?

如遇报错用最上面的指令命令行显示后复制到txt也行

运行后cmd会有如下信息,这就是魔女的夜宴官中的解密参数

不要傻到拿文章得到的这个参数去套别的游戏,那肯定行不通......

然后把窗口的信息全部复制保存到txt里头

text
     ____
    / _  |   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会自动生成在游戏目录下

control_block.bin用于放在编译后的文件夹的GameData\Formats里头

这两个文件Garbro制作参数时会使用到

接下来使用CxdecExtractorLoader拿到加密的文件夹映射表

将游戏程序拖到CxdecExtractorLoader.exe

image.png点第二个加载字符串Hash

image.png游戏目录下的

StringHashDumper_Output会有DirectoryHash.log这个就是我们要的

有部分是空目录对于Windows系统无意义

%EmptyString%##YSig##94D4A97C61498621 %EmptyString%##YSig##94D4A97C61498621 %EmptyString%##YSig##94D4A97C61498621 %EmptyString%##YSig##94D4A97C61498621 %EmptyString%##YSig##94D4A97C61498621 %EmptyString%##YSig##94D4A97C61498621

这几个没用的%EmptyString%带这些的行可以删除掉

image.png游戏开久了就会有这个,反选复制前面的内容覆盖掉即可

Garbro读取HxNames.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复制到游戏目录

然后打开游戏后出现这个弹窗就是在撞库了

image.png等待一会后

[src/krkr_hxv4_dumphash.cpp,216,calc_thread,I] try to calc names in dirs.txt [src/krkr_hxv4_dumphash.cpp,218,calc_thread,I] calculate finish, results in files_match.txt, dirs_match.txt

直接关掉命令窗口就可以快速关掉游戏了

即可在游戏目录找到files_match.txt与dirs_match.txt

files_match.txt使用的是utf-16的编码文本编辑器重新保存txt然后编码改成utf-8编码即可

image.png

撞库得到的信息是

001.共通-オナニーマスター.ks.scn,CAF2630573E58914755A99B3444995F3B7FF681F98D2220C7AF9370E5FA29F56

文件,哈希

需要将其转换为“哈希:文件名"的形式方便制作lst映射

文件映射格式转换处理

读取的files.txt编码应当改为utf-8,utf-16le读取报错

Python
import os
import sys

def main():
    # 检查命令行参数是否正确
    if len(sys.argv) != 2:
        print("用法错误!正确格式:python 1.py 输入文件.txt")
        print("示例:python 1.py 1.txt")
        sys.exit(1)

    # 获取命令行传入的输入文件路径
    input_file_path = sys.argv[1]
    # 输出文件保存到与脚本同一目录,命名为"转换结果.txt"
    output_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "转换结果.txt")

    try:
        # 读取输入文件(支持中文/日文等特殊字符)
        with open(input_file_path, "r", encoding="utf-8") as f:
            lines = f.readlines()

        result = []
        for line_num, line in enumerate(lines, 1):
            line = line.strip()  # 去掉换行符、前后空格
            if not line:
                continue  # 跳过空行

            # 分割文件名和哈希值(按逗号分隔,确保只分割1次)
            if "," not in line:
                print(f"警告:第{line_num}行格式错误(无逗号分隔),已跳过:{line}")
                continue
            
            filename, hash_val = line.split(",", 1)
            
            # 替换后缀:将.ks.scn改为.ogg
            ogg_filename = filename.replace(".ks.scn", ".ogg")
            
            # 生成目标格式:哈希值:文件名.ogg
            target_line = f"{hash_val}:{ogg_filename}"
            result.append(target_line)

        # 保存结果(覆盖已有文件,编码为utf-8确保兼容性)
        with open(output_file_path, "w", encoding="utf-8") as f:
            f.write("\n".join(result))

        print("转换完成!")
        print(f"读取文件:{os.path.abspath(input_file_path)}")
        print(f"保存文件:{output_file_path}")
        print(f"成功处理 {len(result)} 条数据")

    except FileNotFoundError:
        print(f"错误:未找到输入文件 {input_file_path}")
        print("请检查文件路径是否正确,或文件是否存在")
    except Exception as e:
        print(f"转换失败:{str(e)}")

if __name__ == "__main__":
    main()

使用方法:python 程序.py files_match.txt

image.png与脚本在同一目录下有转换结果.txt

image.png

还有一个文件夹路径要处理

文件夹路径格式映射处理

.log的编码是ANSI,应当改成utf-8

Python
import re
import sys
import os

def convert_ysig_to_colon(input_str: str) -> str | None:
    """
    核心转换逻辑:
    - 正常格式(如 video/##YSig##01E3087E0C79CE02)→ 转换为 签名:资源类型
    - 空类型格式(%EmptyString%##YSig##xxx)→ 直接删除
    - 非法格式 → 跳过(控制台提示)
    """
    input_str = input_str.strip()
    if not input_str:
        return None

    # 匹配正常YSig格式:[资源类型]##YSig##[十六进制签名]
    normal_pattern = r'^([^#%]+)##YSig##([0-9A-Fa-f]+)$'
    normal_match = re.match(normal_pattern, input_str)
    if normal_match:
        resource_type = normal_match.group(1)
        signature = normal_match.group(2).upper()  # 签名统一转大写
        return f"{signature}:{resource_type}"

    # 匹配空类型格式:直接返回None(删除)
    empty_pattern = r'^%EmptyString%##YSig##[0-9A-Fa-f]+$'
    if re.match(empty_pattern, input_str):
        return None

    # 非法格式:返回None并提示
    print(f"警告:跳过非法格式行:{input_str}")
    return None

def main():
    # 检查命令行参数
    if len(sys.argv) != 2:
        print("错误:用法错误!正确用法:python 1.py 路径.txt")
        sys.exit(1)

    # 获取输入文件路径
    input_file_path = sys.argv[1]

    # 检查文件是否存在
    if not os.path.exists(input_file_path):
        print(f"错误:文件 {input_file_path} 不存在!")
        sys.exit(1)

    # 读取输入文件(UTF-16LE编码,适配DirectoryHash.log)
    try:
        with open(input_file_path, 'r', encoding='utf-16le') as f:
            input_lines = [line.strip() for line in f]  # 读取所有行并去除首尾空格
    except Exception as e:
        print(f"错误:读取文件失败:{str(e)}")
        sys.exit(1)

    # 批量转换(过滤空类型和非法格式)
    result_lines = []
    for line in input_lines:
        converted = convert_ysig_to_colon(line)
        if converted:
            result_lines.append(converted)

    # 生成输出文件路径:强制保存到1.py所在目录
    py_file_dir = os.path.dirname(os.path.abspath(__file__))  # 1.py的绝对路径目录
    input_file_name = os.path.basename(input_file_path)  # 输入文件的文件名(含后缀)
    name_without_ext, ext = os.path.splitext(input_file_name)
    output_file_path = os.path.join(py_file_dir, f"{name_without_ext}_output{ext}")  # 输出文件在1.py目录下

    # 写入输出文件(UTF-8编码,兼容大多数场景)
    try:
        with open(output_file_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(result_lines))
    except Exception as e:
        print(f"错误:写入文件失败:{str(e)}")
        sys.exit(1)

    # 输出统计信息
    total_input = len(input_lines)
    total_valid = len(result_lines)
    total_deleted = total_input - total_valid
    print("处理完成!")
    print(f"统计:输入 {total_input} 行 → 有效输出 {total_valid} 行 → 删除 {total_deleted} 行(空类型/非法格式)")
    print(f"输出文件:{output_file_path}")

if __name__ == "__main__":
    main()

使用方法:python 程序.py DirectoryHash.log

image.png那么把文件夹路径复制到文件映射表下面加回去复制粘贴,改一下名字HxNames.lst

往下添加就是

image.png7.Garbro-Mod的制作dat编写

Github自行搜索Garbro-Mod下源码编译修改

项目遵循MIT

https://github.com/nanami5270/GARbro-Mod/blob/master/LICENSE

在源码下的GARbro-Mod/SchemeTool/Program.cs

此cs正是要编辑的用于创建dat的如果新游戏的dat数据没有,那么可以自己动手手动创建,维护这个lst的过程十分caodan,要是发布了新的patch,就只能跟踪TJS2100这个恶心玩意了

本代码个人理解范围编写部分内容,如写得像屎山免责,问怎么写得就是我问ai瞎写,免责

javascript
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Remoting.Metadata.W3cXsd2001;
using System.Text;
using System.Threading.Tasks;

namespace SchemeTool
{
    class Program
    {
        static void Main(string[] args)
        {
            // Load database
            using (Stream stream = File.OpenRead(".\\GameData\\Formats.dat"))
            {
                GameRes.FormatCatalog.Instance.DeserializeScheme(stream);
            }
#if false
            using (Stream stream = File.Create(".\\GameData\\Formats.json"))
            {
                GameRes.FormatCatalog.Instance.SerializeSchemeJson(stream);
                return;
            }
#endif
            GameRes.Formats.KiriKiri.Xp3Opener format = GameRes.FormatCatalog.Instance.ArcFormats
                .FirstOrDefault(a => a is GameRes.Formats.KiriKiri.Xp3Opener) as GameRes.Formats.KiriKiri.Xp3Opener;

            if (format != null)
            {
                GameRes.Formats.KiriKiri.Xp3Scheme scheme = format.Scheme as GameRes.Formats.KiriKiri.Xp3Scheme;

                // Add scheme information here

#if true
                byte[] cb = File.ReadAllBytes(@"control_block.bin");//这里control_block.bin需放入编译后的文件夹GameData\Formats里头
             
                uint[] cb2 = new uint[cb.Length / 4];
                Buffer.BlockCopy(cb, 0, cb2, 0, cb.Length);
                for (int i = 0; i < cb2.Length; i++)
                    cb2[i] = ~cb2[i];
                var cs = new GameRes.Formats.KiriKiri.CxScheme
                {   //将提取到的东西选中并替换掉输入二字
                    Mask = 输入,//输入mask其定位于第四个* 方便快速定位内容
                    Offset = 输入,//输入Offset其定位于第五个* 
                    PrologOrder = new byte[] { 输入 },//如下以此类推
                    OddBranchOrder = new byte[] { 输入 },
                    EvenBranchOrder = new byte[] { 输入 },
                    ControlBlock = cb2.ToArray()
                };
                var crypt = new GameRes.Formats.KiriKiri.HxCrypt(cs);
                crypt.RandomType = 0;
                crypt.FilterKey = 0x0090d997b400a9b5;
                crypt.NamesFile = "HxNames.lst";//这个文件需放入编译后的文件夹GameData\Formats里头
                var keyA1 = SoapHexBinary.Parse("输入key").Value;//第一个*key
                var keyA2 = SoapHexBinary.Parse("输入nonce").Value;//第二个* nonce
                var keyB1 = SoapHexBinary.Parse("输入key").Value;//第一个*key
                var keyB2 = SoapHexBinary.Parse("输入nonce").Value;//第二个* nonce
                crypt.IndexKeyDict = new Dictionary<string, GameRes.Formats.KiriKiri.HxIndexKey>()
                {
                    { "data.xp3", new GameRes.Formats.KiriKiri.HxIndexKey { Key1 = keyA1, Key2 = keyA2 } },
                    { "update.xp3", new GameRes.Formats.KiriKiri.HxIndexKey { Key1 = keyB1, Key2 = keyB2 } },
                };
#else
                GameRes.Formats.KiriKiri.ICrypt crypt = new GameRes.Formats.KiriKiri.XorCrypt(0x00);
#endif

                // scheme.KnownSchemes.Add("game title", crypt);
            }

            var gameMap = typeof(GameRes.FormatCatalog).GetField("m_game_map", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
                .GetValue(GameRes.FormatCatalog.Instance) as Dictionary<string, string>;

            if (gameMap != null)
            {
                // Add file name here
                 gameMap.Add("游戏.exe", "游戏标题");
            }

            // Save database
            using (Stream stream = File.Create(".\\GameData\\Formats.dat"))
            {
                GameRes.FormatCatalog.Instance.SerializeScheme(stream);
            }
        }
    }
}

直接全选复制粘贴到cs后编辑一下得到的密钥

直接文本编辑器打开cs全选覆盖粘贴

然后使用VS2026编译,在这之前可以阅览一下推荐安装的东西,还有一个较大的硬盘用于储存依赖头文件等

8.编译方法

下一个VScode2026,下好头文件依赖,不过我也顺带更新一下记录一下过程吧

选择

版本Community2026

勾选

C++的桌面开发

VisualStudio扩展开发

双击打开sln或打开VS2026拖入sln

版本选Release不要选预览版还有Debug版本,适用处理器也无需修改

ctrl+shift+B快捷生成解决方案

9.文件补丁工具箱

Textile
☁️全部文件/
    └── 📂软件/
        ├── 📂xdg32处理/
        │   ├── 📂Yuzusoft
        │   │   ├── 星光咖啡馆📦️
        │   │   ├── 新作-柠檬即兴曲(组一辈子的神人乐队)名字待定📦️
        │   │   ├── 天使纷扰📦️
        │   │   ├── 千态万花📦️
        │   │   ├── 魔女的夜宴📦️
        │   │   └── 谜语小丑📦️
        │   └── xdg32如上操作即可去除.mp4 ▶︎
        ├── 📂Steamless/
        │   ├── 配置勾选1257.png 🖻
        │   └── Steamless.v3.1.0.5...by.atom0s.zip📦️
        └── 📂注入/
            ├── 获取文件哈希对应/
            └── 获取解密密钥/

 https://pan.baidu.com/s/12Q-cQh9v3eZjlhQ7O4-YFQ

提取码: 4btt

本文版权遵循 CC BY-NC 协议 本站版权政策

20 条回复

AKATSUKI
发布于 2025-11-29 - 14:24

马上删除所有KrkrRev的内容

舞释
发布于 2025-11-29 - 16:20

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

kinotern
发布于 2025-11-29 - 16:34 (编辑于 2025-11-29 - 16:40)
回复 @舞释#4

l!image.png大佬我是少东西了吗?为啥说tee不是命令?

或许我电脑有装tee就是Git for Windows这类支持工具,我重新写一下

frida -l krkr_hxv4_dumpkey.js -f SabbatOfTheWitch.exe

这个至少可以在控制台用删了保存那步骤

舞释
发布于 2025-11-29 - 16:43
回复 @kinotern#5

好的谢谢啦~

舞释
发布于 2025-11-29 - 16:45
回复 @kinotern#5

但是这样好像无法输出txt文件了~

kinotern
发布于 2025-11-29 - 16:50
回复 @舞释#7

确实我要想想办法,虽然你可以暂时复制保存一下txt,多了一步,折中一下,我们目的是拿到key

舞释
发布于 2025-11-29 - 16:59
回复 @舞释#4

l!image.png大佬我是少东西了吗?为啥说tee不是命令?

image.pngimage.png

text
frida -l krkr_hxv4_dumpkey.js -f limelight_lj.exe > output.txt 2>&1
kinotern
发布于 2025-11-29 - 17:12
回复 @舞释#10

你那边可以使用对吧

舞释
发布于 2025-11-29 - 17:12
回复 @kinotern#11

对的~

舞释
发布于 2025-11-29 - 17:16
回复 @舞释#10

BIN文件也会在文件夹里

舞释
发布于 2025-11-29 - 17:26

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

kinotern
发布于 2025-11-29 - 22:00
回复 @舞释#14

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

标记——已解决

xiaoxinxin
发布于 2025-11-29 - 23:24

佬,那个output.txt输出来为

____ / _ | Frida 17.5.1 - 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 limelight_lj.exe... Failed to load script: 'utf-8' codec can't decode byte 0xa1 in position 376: invalid start byte

Thank you for using Frida!

是什么情况

xiaoxinxin
发布于 2025-11-29 - 23:25
回复 @xiaoxinxin#16

佬,那个output.txt输出来为 &#x20;\\\\ / \_ | Frida 17.5.1 - A world-class dynamic instrumentation toolkit \| (*| | \> \_ | Commands: /*/ |\_| help -> Displ...

需要改什么东西吗,

舞释
发布于 2025-11-30 - 01:34

大佬大佬~
image.pngfiles_match.txt只有60条是正常的吗
image.pngdirs_match.txt里面是空的
image.png这些都是正常的吗?~

kinotern
发布于 2025-11-30 - 03:08
回复 @舞释#18

大佬大佬\~\ !image.pngfiles\_match.txt只有60条是正常的吗\ !image.pngdirs\_match.txt里面是空的\ !image.png这些都是正常的吗?\~

dirsmatch没关系,况且你用了utf-16le的编码,在经过了撞库之后由ANSI转为utf-8方便软件读取

只有十来个文件时不正常的

kinotern
发布于 2025-11-30 - 03:09
回复 @舞释#18

大佬大佬\~\ !image.pngfiles\_match.txt只有60条是正常的吗\ !image.pngdirs\_match.txt里面是空的\ !image.png这些都是正常的吗?\~

不对啊?你咋把游戏源文件给放进去了,这个不是这么用的哈,是拿份模拟版本汇总文件输入进去

kinotern
发布于 2025-11-30 - 03:10
回复 @xiaoxinxin#17

我也纳闷,这一块frida输出我装了git类linux支持的却可以直接导出去,我研究下

xiaoxinxin
发布于 2025-11-30 - 12:05
回复 @kinotern#21

ok,谢谢,解决了,应该是是编码问题

舞释
发布于 2025-11-30 - 14:18
回复 @kinotern#20

哦!原来如此谢谢大佬

(。>︿<。) 已经一滴回复都不剩了哦~