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

该话题被推 逆向工程 实用技术CXDEChvx4魔女的夜宴krkrZ新时代解包方法
浏览数 - 470发布于 - 2025-11-29 - 14:04

重新编辑于 - 2025-11-29 - 19:18

前言

在论坛的搜索游戏处,拿一份模拟器版本的xp3,garbro解包它

因为模拟器包含了游戏的全部文件,且未加密,用于恢复电脑版本的参考

如果你愿意自己手动分类也没关系,我们的重点是制作适用于Garbro通用dat

本文参考了https://www.kungal.com/topic/2670目的是制作详细教学方便小白入手

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

1.使用Python汇总解包后游戏文件

源码如下——豆包生成,本文代码等内容都遵循MIT协议

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

2.dump游戏解密

dumpkey源码如下

复制源码创建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-
 * 
 * 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

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

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

两个报错的原因是没绕过程序startup.tjs

image.png

通常情况下柚子社的新游戏解决后会遇到此类情况

但是使用fuckcxdec所生成的Version.dll会阻止后续撞库打开exe

请于网盘工具箱查找

Malformed exe/dll detected修复程序

链接: https://pan.baidu.com/s/1DFuJ9ro9XYVD8W9ApV92vw 提取码: imtw

5.提取

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

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

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

运行后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

这两个文件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游戏开久了就会有这个,反选复制前面的内容覆盖掉即可

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复制到游戏目录

网盘工具箱

所有所需的文件在此处

https://pan.baidu.com/s/1LcKOcKT2NtENYBfgF2Zi9A 提取码: 36vp

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

14 条回复

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

马上删除所有KrkrRev的内容

kinotern
发布于 2025-11-29 - 14:38
回复 @AKATSUKI#1

马上删除所有KrkrRev的内容

kinotern
发布于 2025-11-29 - 14:39
回复 @AKATSUKI#1

马上删除所有KrkrRev的内容

凭啥,我自己码的字,MIT,MIT,MIT,ok?

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

鲲

6723

#9
发布于 2025-11-29 - 16:54
回复 @kinotern#3

你好,非常对不起给你带来困扰了,上面的是 KrkrRev 的作者本人,我已经为你删除了文章中关于 KrkrRev 的相关内容,然后将那一段文章的截图私发给你了

我已经询问过作者,作者表示不想让工具被简单使用和传播,非常抱歉给你带来不好的体验了

舞释
发布于 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好像无法运行游戏了#

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