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

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

重新编辑于 - 2025-12-21 - 12:17

前言

本文Python代码与文章内容都遵循MIT协议

Textile
Copyright <2025> <Kinotern>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

仅供逆向汉化学习参考,不得用于其他非法用途

如果不想从零跑游戏制作对照参考表

需要要在论坛的搜索游戏,找到游戏下个模拟器版本的

拿一份模拟器版本的xp3,garbro解包它

没有模拟器副本的话自行跑游戏获取文件KrKrDump实时导出

请注意不是把原始xp3汇总扔游戏撞库

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

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

总好过一个个点选项跑游戏跑断腿吧

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

本文重点在于制作映射表格以快速对CXDECExtracter提取的游戏文件分类重命名

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

上述其基本阐述了如何恢复的过程,也离不开大佬的探索

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

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

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

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

源码如下

git记录

25/11/5

修复了GUI下汇总时窗口无反应

重新修复了汇总时导出的txt编码问题改为带BOM的utf-16le、

将默认导出文件名改为files

Python
import os
import sys
import time
import threading
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:
            # 显式写入UTF-16LE的BOM字符
            f.write('\ufeff')
            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 extract_in_thread(target_dir, save_path):
        # 执行提取操作
        success, msg = extract_files(target_dir, save_path)
        
        # 通过after方法线程安全地更新GUI
        root.after(0, update_gui_after_extraction, success, msg)
    
    # 提取完成后更新GUI
    def update_gui_after_extraction(success, msg):
        # 恢复界面状态
        progress_bar.stop()
        progress_bar.pack_forget()
        
        # 显示结果
        if success:
            progress_label.config(text="提取完成!可点击按钮继续操作")
            messagebox.showinfo("操作成功", msg)
        else:
            progress_label.config(text="提取失败!请检查错误信息")
            messagebox.showerror("操作失败", msg)
    
    # 开始提取按钮
    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="files.txt"
        )
        if not save_path:
            return

        # 更新界面状态
        progress_label.config(text="正在提取文件...请耐心等待")
        progress_bar.pack(pady=5, padx=20)
        progress_bar.start()
        root.update_idletasks()  # 立即刷新界面

        # 创建并启动后台线程执行提取操作
        extraction_thread = threading.Thread(
            target=extract_in_thread,
            args=(target_dir, save_path),
            daemon=True  # 设为守护线程,程序关闭时自动结束
        )
        extraction_thread.start()



    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或VSCode来查看

采取utf-16l的文本编码的原因是游戏内部分文件是以日文来命名的

当使用utf-8格式txt输入cxdec撞库会以乱码的形式扔进去,无法制作获取映射表的

CXDEC的dll只负责把字符串转变为哈希你即使输入个1145141919810.png也会有hash

2.解密前准备条件

有足够的硬盘空间存储文件

还有基础的python使用认识

3.环境配置

电脑需有python

运行后缺少import库的自行pip install

实在不想输入命令也给你顺带编译好了py

4.Steam的KrKr游戏处理(潘多拉魔盒)

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或krkrsteam.dll

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

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

例如

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

我下过一个学习版本的柚子社组乐队,业界知名逆向巨佬Dir-A制作的Version.dll好用但是这不和撞库用的version.dll重名了啊

windows的文件管理器也太难对付了......

所以遇到这种情况使用反编译器xdg32那些或者改bootstart也行,就可以不依赖dll做到撞库

工具箱的讲解用32xdg可以快速解决

实在不会做也有方案

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

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

若盘内无支持游戏,请评论发issue,我要亲自把潘多拉的魔盒给炸了

5.提取

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

https://github.com/YeLikesss/KrkrExtractForCxdecV2

将游戏程序拖到CxdecExtractor.exe

image.png

点第一个,把xp3一个个拖到窗口提取

image.png

提取时出现窗口未响应不要慌,等恢复相应状态即可

软件没有多线程,耐心等待

image.png

image.png

完成后关闭游戏再重新拖到提取程序打开

然后点第二个加载字符串Hash

image.png游戏目录下的

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

有部分是空目录(如:%EmptyString%##YSig##94D4A97C61498621)留一个就行

最下面的一大段的重复行,不用管着先

image.png

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

撞库得到的信息是

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

就这样子的就没问题了

7.程序自动恢复原始情况

基本思路是既然获取了映射

批量重命名然后将空文件夹命名为空再移到上级目录去

再根据文件映射表改名就可以用了

比较完善得版本

Python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
文件批量重命名工具
根据files_match.txt和DirectoryHash.log进行文件/文件夹重命名
"""

import os
import shutil
import time
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from pathlib import Path
import json
from datetime import datetime
import threading

class FileRenamerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("文件批量重命名工具")
        self.root.geometry("900x700")
        
        # 工作目录
        self.working_dir = Path.cwd()
        
        # 映射数据
        self.file_mappings = {}  # 哈希 -> 文件名
        self.dir_mappings = {}   # 哈希 -> 目录名
        self.ready_dirs = []     # 需要处理的ready目录
        
        # UI批处理优化
        self.ui_update_queue = []  # UI更新队列
        self.ui_update_timer = None  # UI更新定时器
        self.ui_batch_interval = 50  # 批处理间隔(毫秒)
        
        # 性能监控
        self.performance_stats = {
            "file_rename_time": 0,
            "dir_rename_time": 0,
            "file_move_time": 0,
            "total_time": 0,
            "files_processed": 0,
            "dirs_processed": 0
        }
        self.performance_log = []  # 性能日志记录
        
        # 配置选项
        self.skip_warnings = False  # 是否跳过警告
        
        self.setup_ui()
        
    def setup_ui(self):
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 配置网格权重
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        
        # 工作目录选择
        ttk.Label(main_frame, text="工作目录:").grid(row=0, column=0, sticky=tk.W, pady=5)
        self.dir_var = tk.StringVar(value=str(self.working_dir))
        ttk.Entry(main_frame, textvariable=self.dir_var, width=60).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5)
        ttk.Button(main_frame, text="浏览", command=self.browse_directory).grid(row=0, column=2, padx=5)
        
        # 文件映射导入
        ttk.Label(main_frame, text="文件映射文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
        self.file_map_var = tk.StringVar()
        ttk.Entry(main_frame, textvariable=self.file_map_var, width=60).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=5)
        ttk.Button(main_frame, text="选择", command=lambda: self.browse_file(self.file_map_var)).grid(row=1, column=2, padx=5)
        
        # 目录映射导入
        ttk.Label(main_frame, text="目录映射文件:").grid(row=2, column=0, sticky=tk.W, pady=5)
        self.dir_map_var = tk.StringVar()
        ttk.Entry(main_frame, textvariable=self.dir_map_var, width=60).grid(row=2, column=1, sticky=(tk.W, tk.E), padx=5)
        ttk.Button(main_frame, text="选择", command=lambda: self.browse_file(self.dir_map_var)).grid(row=2, column=2, padx=5)
        
        # 按钮区域
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=3, column=0, columnspan=3, pady=10)
        
        self.import_button = ttk.Button(button_frame, text="导入映射", command=self.load_mappings)
        self.import_button.pack(side=tk.LEFT, padx=5)
        
        self.preview_button = ttk.Button(button_frame, text="预览更改", command=self.preview_changes)
        self.preview_button.pack(side=tk.LEFT, padx=5)
        
        self.execute_button = ttk.Button(button_frame, text="开始恢复", command=self.execute_renaming)
        self.execute_button.pack(side=tk.LEFT, padx=5)
        
        self.clear_button = ttk.Button(button_frame, text="清空日志", command=self.clear_log)
        self.clear_button.pack(side=tk.LEFT, padx=5)
        
        # 跳过警告选项
        self.skip_warnings_var = tk.BooleanVar(value=self.skip_warnings)
        self.skip_warnings_check = ttk.Checkbutton(button_frame, text="跳过警告", variable=self.skip_warnings_var, command=self.update_skip_warnings)
        self.skip_warnings_check.pack(side=tk.LEFT, padx=5)
        
        # 状态信息
        self.status_var = tk.StringVar(value="就绪")
        ttk.Label(main_frame, textvariable=self.status_var).grid(row=4, column=0, columnspan=3, pady=5)
        
        # 进度条
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100, length=400)
        self.progress_bar.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
        
        # 进度标签
        self.progress_label = ttk.Label(main_frame, text="0%")
        self.progress_label.grid(row=5, column=3, padx=5)
        
        # 映射信息显示
        notebook = ttk.Notebook(main_frame)
        notebook.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
        
        # 文件映射标签
        file_frame = ttk.Frame(notebook)
        self.file_tree = ttk.Treeview(file_frame, columns=("hash", "filename"), show="headings", height=10)
        self.file_tree.heading("hash", text="哈希值")
        self.file_tree.heading("filename", text="文件名")
        self.file_tree.column("hash", width=300)
        self.file_tree.column("filename", width=300)
        
        scrollbar = ttk.Scrollbar(file_frame, orient=tk.VERTICAL, command=self.file_tree.yview)
        self.file_tree.configure(yscrollcommand=scrollbar.set)
        
        self.file_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        notebook.add(file_frame, text="文件映射")
        
        # 目录映射标签
        dir_frame = ttk.Frame(notebook)
        self.dir_tree = ttk.Treeview(dir_frame, columns=("hash", "dirname"), show="headings", height=10)
        self.dir_tree.heading("hash", text="哈希值")
        self.dir_tree.heading("dirname", text="目录名")
        self.dir_tree.column("hash", width=300)
        self.dir_tree.column("dirname", width=300)
        
        scrollbar2 = ttk.Scrollbar(dir_frame, orient=tk.VERTICAL, command=self.dir_tree.yview)
        self.dir_tree.configure(yscrollcommand=scrollbar2.set)
        
        self.dir_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar2.pack(side=tk.RIGHT, fill=tk.Y)
        notebook.add(dir_frame, text="目录映射")
        
        # 日志输出
        ttk.Label(main_frame, text="操作日志:").grid(row=6, column=0, sticky=tk.W, pady=5)
        self.log_text = scrolledtext.ScrolledText(main_frame, width=100, height=15)
        self.log_text.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 配置网格权重
        main_frame.rowconfigure(7, weight=1)
        
        self.log("应用程序已启动")
        
    def browse_directory(self):
        directory = filedialog.askdirectory(initialdir=self.working_dir)
        if directory:
            self.working_dir = Path(directory)
            self.dir_var.set(str(self.working_dir))
            self.log(f"工作目录已设置为: {self.working_dir}")
            
    def browse_file(self, var):
        # 根据变量判断文件类型
        if var == self.file_map_var:
            filetypes = [("文本文件", "*.txt"), ("所有文件", "*.*")]
            title = "选择文件映射文件"
        elif var == self.dir_map_var:
            filetypes = [("日志文件", "*.log"), ("所有文件", "*.*")]
            title = "选择目录映射文件"
        else:
            filetypes = [("所有文件", "*.*")]
            title = "选择文件"
            
        filename = filedialog.askopenfilename(
            initialdir=self.working_dir,
            title=title,
            filetypes=filetypes
        )
        if filename:
            var.set(filename)
            
    def log(self, message):
        timestamp = datetime.now().strftime("%H:%M:%S")
        log_message = f"[{timestamp}] {message}"
        self.log_text.insert(tk.END, log_message + "\n")
        self.log_text.see(tk.END)
        print(log_message)
        
    def clear_log(self):
        self.log_text.delete(1.0, tk.END)
        self.log("日志已清空")
        self.log("日志已清空")
        
    def load_mappings(self):
        """加载文件映射和目录映射"""
        try:
            # 清空现有数据
            self.file_mappings.clear()
            self.dir_mappings.clear()
            self.ready_dirs.clear()
            
            for item in self.file_tree.get_children():
                self.file_tree.delete(item)
            for item in self.dir_tree.get_children():
                self.dir_tree.delete(item)
            
            # 加载文件映射
            file_map_path = self.file_map_var.get()
            if file_map_path and os.path.exists(file_map_path):
                try:
                    # 尝试UTF-16LE编码
                    with open(file_map_path, 'r', encoding='utf-16le') as f:
                        for line_num, line in enumerate(f, 1):
                            line = line.strip()
                            if line and ',' in line:
                                parts = line.split(',', 1)
                                if len(parts) == 2:
                                    filename = parts[0].strip()
                                    hash_value = parts[1].strip()
                                    # 文件映射:哈希值 -> 文件名
                                    self.file_mappings[hash_value] = filename
                                    self.file_tree.insert("", tk.END, values=(hash_value, filename))
                    self.log(f"已加载 {len(self.file_mappings)} 个文件映射 (UTF-16LE)")
                except UnicodeDecodeError:
                    # UTF-16LE失败,尝试UTF-8
                    with open(file_map_path, 'r', encoding='utf-8') as f:
                        for line_num, line in enumerate(f, 1):
                            line = line.strip()
                            if line and ',' in line:
                                parts = line.split(',', 1)
                                if len(parts) == 2:
                                    filename = parts[0].strip()
                                    hash_value = parts[1].strip()
                                    self.file_mappings[hash_value] = filename
                                    self.file_tree.insert("", tk.END, values=(hash_value, filename))
                    self.log(f"已加载 {len(self.file_mappings)} 个文件映射 (UTF-8)")
                except Exception as e:
                    messagebox.showerror("错误", f"读取文件映射时出错: {str(e)}")
                    self.log(f"错误: {str(e)}")
            
            # 加载目录映射
            dir_map_path = self.dir_map_var.get()
            if dir_map_path and os.path.exists(dir_map_path):
                seen_hashes = set()  # 用于去重
                try:
                    # 尝试UTF-16LE编码
                    with open(dir_map_path, 'r', encoding='utf-16le') as f:
                        for line_num, line in enumerate(f, 1):
                            line = line.strip()
                            if line:
                                if '##YSig##' in line:
                                    parts = line.split('##YSig##')
                                    if len(parts) == 2:
                                        dir_path = parts[0].rstrip('/')
                                        hash_value = parts[1]
                                        
                                        # 去重:如果已经处理过这个哈希值,跳过
                                        if hash_value in seen_hashes:
                                            continue
                                        seen_hashes.add(hash_value)
                                        
                                        # 处理特殊规则
                                        if dir_path.strip() == '%EmptyString%' or dir_path.strip() == '':
                                            dir_name = ""  # 空字符串,不重命名目录
                                            self.ready_dirs.append(hash_value)
                                        elif dir_path == 'k2compat':
                                            dir_name = "k2compat"
                                        else:
                                            # 使用完整路径作为目录名,保持父子关系
                                            dir_name = dir_path
                                            
                                        self.dir_mappings[hash_value] = dir_name
                                        self.dir_tree.insert("", tk.END, values=(hash_value, dir_name))
                    self.log(f"已加载 {len(self.dir_mappings)} 个目录映射(已去重) (UTF-16LE)")
                except UnicodeDecodeError:
                    # UTF-16LE失败,尝试UTF-8
                    with open(dir_map_path, 'r', encoding='utf-8') as f:
                        for line_num, line in enumerate(f, 1):
                            line = line.strip()
                            if line:
                                if '##YSig##' in line:
                                    parts = line.split('##YSig##')
                                    if len(parts) == 2:
                                        dir_path = parts[0].rstrip('/')
                                        hash_value = parts[1]
                                        
                                        if hash_value in seen_hashes:
                                            continue
                                        seen_hashes.add(hash_value)
                                        
                                        if dir_path == '%EmptyString%' or dir_path == '':
                                            dir_name = ""  # 空字符串,不重命名目录
                                            self.ready_dirs.append(hash_value)
                                        elif dir_path == 'k2compat':
                                            dir_name = "k2compat"
                                        else:
                                            # 使用完整路径作为目录名,保持父子关系
                                            dir_name = dir_path
                                            
                                        self.dir_mappings[hash_value] = dir_name
                                        self.dir_tree.insert("", tk.END, values=(hash_value, dir_name))
                    self.log(f"已加载 {len(self.dir_mappings)} 个目录映射(已去重) (UTF-8)")
                except Exception as e:
                    messagebox.showerror("错误", f"读取目录映射时出错: {str(e)}")
                    self.log(f"错误: {str(e)}")
                
                if self.ready_dirs:
                    self.log(f"找到 {len(self.ready_dirs)} 个需要处理的ready目录")
                
            self.status_var.set(f"已加载 {len(self.file_mappings)} 文件映射, {len(self.dir_mappings)} 目录映射")
            
        except Exception as e:
            messagebox.showerror("错误", f"加载映射时出错: {str(e)}")
            self.log(f"错误: {str(e)}")
            
    def preview_changes(self):
        """预览将要进行的更改"""
        if not self.file_mappings and not self.dir_mappings:
            messagebox.showwarning("警告", "请先导入映射文件")
            return
            
        self.log("开始预览更改...")
        
        # 预览文件重命名
        file_changes = 0
        for hash_value, filename in self.file_mappings.items():
            # 查找哈希值对应的文件
            # 文件可能位于data目录下的子目录中(根据哈希前2位分组)
            hash_prefix = hash_value[:2].upper()
            search_dir = self.working_dir / "data" / hash_prefix
            
            if search_dir.exists():
                for item in search_dir.iterdir():
                    if item.is_file() and item.name == hash_value:
                        self.log(f"文件: {item} -> {filename}")
                        file_changes += 1
                        break
                else:
                    # 如果没有找到,可能在data目录的直接子目录中
                    for subdir in self.working_dir.glob("data/*"):
                        if subdir.is_dir():
                            for item in subdir.iterdir():
                                if item.is_file() and item.name == hash_value:
                                    self.log(f"文件: {item} -> {filename}")
                                    file_changes += 1
                                    break
            
        # 预览目录重命名
        dir_changes = 0
        for hash_value, dirname in self.dir_mappings.items():
            target_dir = self.working_dir / "data" / hash_value
            if target_dir.exists():
                self.log(f"目录: {target_dir} -> {dirname}")
                dir_changes += 1
            else:
                if not self.skip_warnings:
                    self.log(f"警告:未找到哈希值对应的文件目录: {hash_value}")
                
        # 预览ready目录处理
        ready_changes = 0
        for hash_value in self.ready_dirs:
            ready_dir = self.working_dir / "data" / hash_value
            if ready_dir.exists():
                for item in ready_dir.iterdir():
                    if item.is_file():
                        self.log(f"移动: {item} -> {ready_dir.parent / item.name}")
                        ready_changes += 1
        if ready_changes > 0:
            self.log(f"共 {ready_changes} 个文件需要从ready目录移动")
        else:
            self.log("没有需要从ready目录移动的文件")
    def execute_renaming(self):
        """执行实际的重命名操作"""
        if not self.file_mappings and not self.dir_mappings:
            messagebox.showwarning("警告", "请先导入映射文件")
            return
            
        # 确认对话框
        if not self.skip_warnings:
            response = messagebox.askyesno("确认", "确定要执行重命名操作吗?此操作不可逆!")
            if not response:
                self.log("用户取消操作")
                return
        
        self.log("开始执行重命名操作...")
        self.status_var.set("正在执行重命名...")
        self.progress_var.set(0)
        self.progress_label.config(text="0%")
        
        # 禁用按钮
        self.import_button.config(state="disabled")
        self.preview_button.config(state="disabled")
        self.execute_button.config(state="disabled")
        
        # 在后台线程中执行
        thread = threading.Thread(target=self._execute_renaming_thread, daemon=True)
        thread.start()
        
    def _execute_renaming_thread(self):
        """在后台线程中执行重命名"""
        try:
            start_time = time.time()
            total_operations = len(self.file_mappings) + len(self.dir_mappings) + len(self.ready_dirs)
            completed_operations = 0
            
            # 1. 处理文件重命名
            self.log("开始处理文件重命名...")
            for hash_value, filename in self.file_mappings.items():
                # 查找哈希值对应的文件
                hash_prefix = hash_value[:2].upper()
                search_dir = self.working_dir / "data" / hash_prefix
                
                file_found = False
                if search_dir.exists():
                    for item in search_dir.iterdir():
                        if item.is_file() and item.name == hash_value:
                            # 构建目标路径
                            target_path = self.working_dir / filename
                            target_path.parent.mkdir(parents=True, exist_ok=True)
                            
                            # 移动/重命名文件
                            try:
                                shutil.move(str(item), str(target_path))
                                self.log(f"✓ 文件重命名: {item.name} -> {filename}")
                                file_found = True
                                break
                            except Exception as e:
                                self.log(f"✗ 文件重命名失败: {item.name} -> {filename}: {str(e)}")
                
                if not file_found:
                    # 如果没有找到,可能在data目录的直接子目录中
                    for subdir in self.working_dir.glob("data/*"):
                        if subdir.is_dir():
                            for item in subdir.iterdir():
                                if item.is_file() and item.name == hash_value:
                                    target_path = self.working_dir / filename
                                    target_path.parent.mkdir(parents=True, exist_ok=True)
                                    
                                    try:
                                        shutil.move(str(item), str(target_path))
                                        self.log(f"✓ 文件重命名: {item.name} -> {filename}")
                                        file_found = True
                                        break
                                    except Exception as e:
                                        self.log(f"✗ 文件重命名失败: {item.name} -> {filename}: {str(e)}")
                            if file_found:
                                break
                
                if not file_found:
                    self.log(f"✗ 未找到哈希值对应的文件: {hash_value}")
                
                completed_operations += 1
                progress = (completed_operations / total_operations) * 100
                self.progress_var.set(progress)
                self.progress_label.config(text=f"{progress:.1f}%")
                
            # 2. 处理目录重命名
            self.log("开始处理目录重命名...")
            ready_dirs_to_process = []  # 存储需要处理的ready目录
            for hash_value, dirname in self.dir_mappings.items():
                source_dir = self.working_dir / "data" / hash_value
                
                if not source_dir.exists():
                    if not self.skip_warnings:
                        self.log(f"警告:未找到哈希值对应的文件目录: {hash_value}")
                    completed_operations += 1
                    continue
                
                # 处理空目录名(%EmptyString%或空字符串)
                if dirname == "":
                    # 添加到ready目录处理列表
                    ready_dirs_to_process.append((source_dir, self.working_dir))
                    self.log(f"✓ 标记为ready目录: {source_dir.name}")
                    completed_operations += 1
                    continue
                
                # 构建目标目录路径
                target_dir = self.working_dir / dirname
                target_dir.parent.mkdir(parents=True, exist_ok=True)
                
                # 检查目标目录是否存在
                if target_dir.exists():
                    # 如果目标目录为空,删除它
                    try:
                        if not any(target_dir.iterdir()):
                            target_dir.rmdir()
                            self.log(f"✓ 删除空目标目录: {target_dir}")
                    except Exception as e:
                        self.log(f"✗ 无法删除目标目录: {target_dir}: {str(e)}")
                
                # 重命名目录
                try:
                    source_dir.rename(target_dir)
                    self.log(f"✓ 目录重命名: {source_dir.name} -> {dirname}")
                except Exception as e:
                    self.log(f"✗ 目录重命名失败: {source_dir.name} -> {dirname}: {str(e)}")
                
                completed_operations += 1
                progress = (completed_operations / total_operations) * 100
                self.progress_var.set(progress)
                self.progress_label.config(text=f"{progress:.1f}%")
            
            # 3. 处理ready目录(移动文件到父目录)
            self.log("开始处理ready目录...")
            for ready_dir, target_parent in ready_dirs_to_process:
                if not ready_dir.exists():
                    self.log(f"✗ ready目录不存在: {ready_dir}")
                    completed_operations += 1
                    continue
                
                # 移动所有文件到目标父目录
                files_moved = 0
                for item in ready_dir.iterdir():
                    if item.is_file():
                        target_path = target_parent / item.name
                        try:
                            shutil.move(str(item), str(target_path))
                            self.log(f"✓ 移动文件: {item.name} -> {target_parent.name}")
                            files_moved += 1
                        except Exception as e:
                            self.log(f"✗ 移动文件失败: {item.name}: {str(e)}")
                
                # 如果目录为空,删除它
                try:
                    if not any(ready_dir.iterdir()):
                        ready_dir.rmdir()
                        self.log(f"✓ 删除空ready目录: {ready_dir.name}")
                    else:
                        self.log(f"⚠ ready目录非空,保留: {ready_dir.name}")
                except Exception as e:
                    self.log(f"✗ 无法删除ready目录: {ready_dir.name}: {str(e)}")
                
                completed_operations += 1
                progress = (completed_operations / total_operations) * 100
                self.progress_var.set(progress)
                self.progress_label.config(text=f"{progress:.1f}%")
            
            # 4. 处理self.ready_dirs中的目录
            self.log("开始处理额外的ready目录...")
            for hash_value in self.ready_dirs:
                ready_dir = self.working_dir / "data" / hash_value
                
                if not ready_dir.exists():
                    self.log(f"✗ ready目录不存在: {ready_dir}")
                    completed_operations += 1
                    continue
                
                # 移动所有文件到工作目录
                files_moved = 0
                for item in ready_dir.iterdir():
                    if item.is_file():
                        target_path = self.working_dir / item.name
                        try:
                            shutil.move(str(item), str(target_path))
                            self.log(f"✓ 移动文件: {item.name} -> 工作目录")
                            files_moved += 1
                        except Exception as e:
                            self.log(f"✗ 移动文件失败: {item.name}: {str(e)}")
                
                # 如果目录为空,删除它
                try:
                    if not any(ready_dir.iterdir()):
                        ready_dir.rmdir()
                        self.log(f"✓ 删除空ready目录: {ready_dir.name}")
                    else:
                        self.log(f"⚠ ready目录非空,保留: {ready_dir.name}")
                except Exception as e:
                    self.log(f"✗ 无法删除ready目录: {ready_dir.name}: {str(e)}")
                
                completed_operations += 1
                progress = (completed_operations / total_operations) * 100
                self.progress_var.set(progress)
                self.progress_label.config(text=f"{progress:.1f}%")
            
            # 完成
            end_time = time.time()
            total_time = end_time - start_time
            
            self.log(f"重命名操作完成!总耗时: {total_time:.2f}秒")
            self.status_var.set("操作完成")
            self.progress_var.set(100)
            self.progress_label.config(text="100%")
            
            messagebox.showinfo("完成", f"重命名操作已完成!\n总耗时: {total_time:.2f}秒")
            
        except Exception as e:
            self.log(f"✗ 执行过程中发生错误: {str(e)}")
            messagebox.showerror("错误", f"执行过程中发生错误: {str(e)}")
            
        finally:
            # 重新启用按钮
            self.root.after(0, self._reenable_buttons)
            
    def _reenable_buttons(self):
        """重新启用按钮"""
        self.import_button.config(state="normal")
        self.preview_button.config(state="normal")
        self.execute_button.config(state="normal")
        
    def update_skip_warnings(self):
        """更新跳过警告设置"""
        self.skip_warnings = self.skip_warnings_var.get()
        self.log(f"跳过警告: {'启用' if self.skip_warnings else '禁用'}")

if __name__ == "__main__":
    root = tk.Tk()
    app = FileRenamerApp(root)
    root.mainloop()

8.文件补丁工具箱

Textile
☁️全部文件/
└── 📂软件/
├── 📂xdg32已处理程序/
│ ├── 📝1.解压密码0d000721
│ ├── 📝2.直接覆盖掉原始程序即可
│ ├── 📝3.可避免文件校验不通过
│ ├── 📝4.懒得自己逆也有弄好的版本
│ └── 📂补丁/
│ ├── 📂NekoWorks/
│ │ ├── 📦️Vol.1
│ │ ├── 📦️vol.2
│ │ ├── 📦️vol.3
│ │ ├── 📦️vol.4
│ │ ├── 📦️Extra
│ │ ├── 📦️After
│ │ └── 📂附带画集/
│ │ ├── 📦️vol.0
│ │ ├── 📦️vol.1
│ │ ├── 📦️vol.2
│ │ ├── 📦️vol.3
│ │ ├── 📦️vol.4
│ │ ├── 📦️Extra
│ │ ├── 📦️After
│ │ └── 📦️十周年
│ ├── 📂SisterPosition/
│ │ └── 📦️哥哥与妹妹的乡间生活
│ ├── 📂Yuzusoft/
│ │ ├── 📦️魔女的夜宴
│ │ ├── 📦️谜语小丑
│ │ ├── 📦️千恋万花
│ │ ├── 📦️星光咖啡馆
│ │ ├── 📦️天使纷扰
│ │ ├── 📦️新作-柠檬即兴曲(组乐队)名字待定
│ │ └── 📦️PARQUET
│ └── 📂Lose/
│ ├── 📦️LastRun
│ └── 📦️Pure Station
├── 📂CxdecExtractor/
│ └── 📦️CxdecExtractor.rar
├── 📂其他/
│ └── 📂PianoTrans/
│ └── 📦️PianoTrans-v1.0.7z
├── 📂Steamless/
│ ├── 🖻配置勾选1257.png
│ └── 📦️Steamless.v3.1.0.5.-.by.atom0s.zip
└── 📂注入/
├── 📂获取文件哈希对应/
│ ├── 📂源码/
│ │ └── 📦️krkr_hxv4_dumphash.zip
│ └── 📂编译后文件/
│ └── 📦️编译后文件.zip
└── 📂获取解密密钥/
└── 📦️获取解密密钥.7z

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

提取码: 4btt

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

39 条回复

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文件了~

舞释
发布于 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

哦!原来如此谢谢大佬

aionfatedio
发布于 2025-12-03 - 20:47

基本到最后一步了结果编译那块死活过不去。试了一下病弱妹,现在有control_block.bin,有HxNames.lst。HxNames.lst前几行是

javascript
5BA0F09AC06320DDC5A6ACBEBEDD832024CD075FA185D3413DFECFF0D21943ED:appconfig.tjs
8DE715758FCC3D48A1918EDF85F7E3B0D0802607D7ED9E3F10E0A5B7CB917703:currev.ini
D9FB4859A254D7B9EDA6621CFBE7DFD9D428082090CA08E32A9314E7116548E9:startup.tjs
1F1FA2B4921F0AC7AFAB6600687F69E244CFB9DC262B4202CF504F6CED353E4A:bgimage\base.stage
8907AAFB1750F86E7C2626EB5F24B3698EA658BF47C8F94DBF2B5DED4C456000:bgimage\bg01\bg01_01.png
96CC624760F1F89665317CBE7F151C12FC73B1BFB6B02D3DE89D2EF41782D3BD:bgimage\bg01\bg01_02.png
B962E6A451088690BD76D0C586704341DAB877C7808A0327780A0EF27AF4E43C:bgimage\bg01\bg01_03.png
B7BA29DB35B0C6B5BD16CAB0274F64BAA3A5FA97E5FB215CE4C8CFFF370897AF:bgimage\bg01\bg01_04.png
B485E1EF3C7F4BF3712053C152876887D2A9ACC2FDC4791D49D79B5DD0F1CF07:bgimage\bg02\bg02_夕.png
76D370BD763E8302A821A8814DDF33102B76B78257FF7D2CA323C169DA437C9F:bgimage\bg02\bg02_夕_ガラス戸.png
99139F684640B0E44B30A5A26FD3BB20971DFEA5FF608690B4B3A98E5D44DD81:bgimage\bg02\bg02_夜_消灯.png

确保我没弄错,UTF-8编码。然后进VS2026编译,默认其实没有GameData\Formats\文件夹所以我自己创了个,把control_block.bin和HxNames.lst扔进去然后切release开始生成解决方案,看结果数据库里没有sickly_days这项。因为Program.cs写的是

cs
……

                scheme.KnownSchemes.Add("sickly_days", 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)
            {
                gameMap.Add("sickly_days_crack.exe", "sickly_days");
            }

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

然后把SchemeTool.exe编译出来,control_block.bin和HxNames.lst放相对路径运行一下才更新Formats.dat。进数据库有sickly_days但解密后还是字符乱码,也就是映射没生效。跟着教程来估计哪一步错了,求助大佬。

kinotern
发布于 2025-12-04 - 01:49
回复 @aionfatedio#24

基本到最后一步了结果编译那块死活过不去。试了一下病弱妹,现在有control\_block.bin,有HxNames.lst。HxNames.lst前几行是 javascript 5BA0F09AC06320DDC5A6ACBEBEDD832024CD075FA185D3413DFECFF0D219...

请注意bin与lst是放入编译后创建的Formats的而不是编译前放入

aionfatedio
发布于 2025-12-04 - 11:49 (编辑于 2025-12-04 - 11:53)
回复 @kinotern#25

编译后并没有自动生成Release\GameData\Formats文件夹且列表中并没有对应游戏名称是哪里出了问题?
fork的是nanami的mod版本,编译release后没有Formats文件夹就自己建了个,发现游戏列表里没有sickly_days。另外注意到sln在编译时默认会不编译生成schemetool,这个工具有用吗。大佬给的编译教程已经很详细了,但是初次上手操作还是有些难度,希望大佬指教

kinotern
发布于 2025-12-06 - 13:57
回复 @aionfatedio#24

基本到最后一步了结果编译那块死活过不去。试了一下病弱妹,现在有control\_block.bin,有HxNames.lst。HxNames.lst前几行是 javascript 5BA0F09AC06320DDC5A6ACBEBEDD832024CD075FA185D3413DFECFF0D219...

哦哦我之前记错了,我重新编写了文档了

aionfatedio
发布于 2025-12-07 - 09:20 (编辑于 2025-12-07 - 09:52)
回复 @kinotern#27

大佬,GARbroV2后续步骤怎么做?加密方式重新再选一下,选HxCrypt,然后点Creat之后,把一些基本数据填了填,撞库的映射表HxNames.lst还需要吗,希望能完善后续教程。
---
我大概明白了,但目前文件夹根据哈希映射都还原出来了,文件名还是处于加密状态,但我填了
image.png它却显示image.png目前是文件夹名已经还原状态
image.png但似乎文件还在加密,是哪里出了问题吗?

kinotern
发布于 2025-12-07 - 10:45
回复 @aionfatedio#28

我也还在研究他,私聊探讨吧,这个我昨天才发现这个好东西

kinotern
发布于 2025-12-07 - 10:45
回复 @aionfatedio#28

offest要不试试去0x看看?

bfloat16
发布于 2025-12-08 - 04:22
回复 @aionfatedio#28

cxdec hash的是xp3里面的文件名,hxcrypt加密的是xp3里面的文件内容,两个都要处理

柚香希冀
发布于 2025-12-08 - 12:03

佬佬,带sig的xp3有头绪吗

kinotern
发布于 2025-12-08 - 12:55

佬佬,带sig的xp3有头绪吗

举例举例具体游戏

kinotern
发布于 2025-12-08 - 12:59
回复 @bfloat16#31

浮点小兄弟,你有头绪吗,具体要做些啥呢😉