首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
逆向工程
发新帖
0
13
[原创]Blowfish 解密 静态分析和动态调试流程记录
发表于: 2026-3-8 15:15
2241
[原创]Blowfish 解密 静态分析和动态调试流程记录
zerosignal
2026-3-8 15:15
2241
在仿真途中遇到了一个 `DSPLib.py` 文件,每次仿真都会加载一次,开头明摆着写着 `VPICRYPTV2` ;要是它放在内部加载,恐怕也不会注意到,放在用户可见的目录中加载,这有些引发好奇心了,于是看看到底从哪里加载到源码的。下面记一下查找解密位置的流程: 首先,是个 `.py`,加载后是 `module`,先看看加载流程,发现了一个内置库 `vpi_tc_internal` ``` [2026-03-07 13:36:28] vpi_tc_internal info: Type: <class 'module'> Dir: ['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'check_header', 'eval_expr', 'get_param_expr_type', 'get_param_value', 'init_module'] __name__: vpi_tc_internal ``` 它没有 `__file__` 字段,说明其是内置或故意隐藏的,应该嵌入在解释器内部;先观察执行效果,它使用 `check_header` 方法检查数据头部,另一个 `init_module` 方法从 module spec 加载,解密并初始化这一模块,这是 spec 的结构: ``` [2026-03-07 13:36:28] spec info: Type: <class '_frozen_importlib.ModuleSpec'> Dir: ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_cached', '_set_fileattr', 'cached', 'has_location', 'loader', 'loader_state', 'name', 'origin', 'parent', 'submodule_search_locations'] __module__: _frozen_importlib ``` 不过,spec 被初始化后,已经编译为字节码了,没有 `__source__` 字段: ``` [2026-03-07 12:50:20] Source exec info: Type: <class 'code'> Dir: ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames'] ``` 劫持 `exec` 会导致相关的 `scipy` 组件初始化出错,看起来没有很好的方法,并且无法保留注释等内容,接下来看下究竟是哪里进行了解密。 仿真由界面调用启动,界面程序为 `pde.exe`,命令行仿真程序为 `ptcl.exe`,在 System Informer 下可以看到创建的进程: ```shell "C:\Program Files\VPI\VPIdesignSuite 11.1\simeng\bin\x64\ptcl.exe" -port 7801 -id 1 -dir "C:\Users\zsig\AppData\Local\Temp\VPIDS111\jobs\6" -x ``` IDA 静态分析 `ptcl.exe`,搜索 `VPICRYPTV2`,寻找到一个字符串,发现字符串初始化结构: ```cpp int sub_140021720() { sub_14123DE40((__int64)&VPICRYPTV2_string, "VPICRYPTV2"); return atexit(sub_1416A3850); } ``` 这是一个字符串对象的初始化过程,`atexit` 是销毁逻辑,初始化时将固定位置的字符串复制到这一对象的数据区,IDA 中暂时标注为 `VPICRYPTV2_string` 对象; 接下来,寻找对象引用,按 `x` 找交叉引用,能够找到两个主要使用点,有一个较短,再向上找引用是 `check_header` 的逻辑,显然,并无解密逻辑;另一个伪代码较长,后续流程较为复杂,应该是要排查的内容: ```cpp if ( (unsigned int)compare_string_objects(v60, &VPICRYPTV1_string) && (unsigned int)compare_string_objects(v60, &VPICRYPTV2_string) ) ``` 上来就是一个字符串比较,清晰明了,进入这一分支的是解密流程,跳过压缩文件相关的逻辑,要看的是 `VPICRYPTV2` 解密逻辑: ```cpp v50 = 0; v62 = 16; v63 = 8; v64 = 5; v65 = 56; v66 = 16; v67 = (void *)operator new(saturated_mul(0x12u, 4u)); Block = (void *)operator new(4096); sub_1412404B0(v69, &off_141A4A5E8); v52 = v57; v24 = get_VPICRYPTV2_key_string(v69, v57, &off_141A4A5E8); v25 = (char *)blowfish_start_decrypt(pExceptionObject, v53, &v46, v24); v50 = v25; j_j_free_0(Block); j_j_free_0(v67); sub_14124D840(a1, v25, (unsigned int)v46, &v47); v26 = (int)v47; v27 = v46 - v47; v46 -= v47; ``` 排查这些步骤中的函数,标注了 `get_VPICRYPTV2_key_string`,其内部逻辑执行了混淆的 blowfish 秘钥构建,密钥是由 `off_141A4A5E8` 位置的三个字符串交叉生成的,这三个原始字符串似乎是开发人员随手复制的,例如 `Error: no file existing`,重新混合组合成二进制秘钥字符串,下面是偏移处的数据: ```cpp .rdata:0000000141A4A5E8 off_141A4A5E8 dq offset aErrorNoFileExi .rdata:0000000141A4A5E8 ; DATA XREF: sub_140E5A0E0:loc_140E5A12D↑o .rdata:0000000141A4A5E8 ; sub_1411D3970:loc_1411D39DE↑o ... .rdata:0000000141A4A5E8 ; "Error: no file existing" .rdata:0000000141A4A5F0 dq offset aProfileBad ; "Profile bad" .rdata:0000000141A4A5F8 dq offset aDiskFull ; "Disk full" .rdata:0000000141A4A600 aErrorNoFileExi db 'Error: no file existing',0 .rdata:0000000141A4A600 ; DATA XREF: .rdata:off_141A4A5E8↑o .rdata:0000000141A4A618 aProfileBad db 'Profile bad',0 ; DATA XREF: .rdata:0000000141A4A5F0↑o .rdata:0000000141A4A624 align 8 .rdata:0000000141A4A628 aDiskFull db 'Disk full',0 ; DATA XREF: .rdata:0000000141A4A5F8↑o .rdata:0000000141A4A632 align 8 ``` 不过,这里的逻辑有些难以辨识,这一 `get_VPICRYPTV2_key_string` 函数给了一个明确的返回值,考虑直接动态调试查看秘钥情况。 由于 `ptcl.exe` 由 `pde.exe` 界面程序创建进程,先调试 `pde.exe`,断点设置在创建进程相关接口: ```asm bp CreateProcessW; bp CreateProcessA; bp ShellExecuteW; bp ShellExecuteExW; ``` 和 `ptcl.exe` 相关的执行停止在 `CreateProcessW`,对于 `CreateProcessW` 有六个参数: 在 x64 调用约定中,前 4 个参数在寄存器(RCX, RDX, R8, R9),后面的参数在栈上。 * `RCX`: lpApplicationName * `RDX`: lpCommandLine * `R8`: lpProcessAttributes * `R9`: lpThreadAttributes `RDX` 指针位置的数据的确是 `ptcl.exe` 相关的命令行指令,从 System Informer 可以看到环境变量和运行参数,使用 `x64dbg.exe` 启动 `ptcl.exe` 开始调试: ``` $env:BNROOT = "C:\Program Files\VPI\VPIdesignSuite 11.1\simeng" $env:HOME = "C:\Users\zsig" $env:PTCL_GPU_SETTINGS = "0:4294836224" $env:PTCL_PYTHON_ENGINE = "C:\ProgramData\VPI\VPIpython\x64\pyenvs\0\Scripts\python37.dll" $env:PTDS_HOME = "C:\Program Files\VPI\VPIdesignSuite 11.1\" $env:PTOLEMY = "C:\Program Files\VPI\VPIdesignSuite 11.1\simeng\ptolemy" $env:PYTHONHOME = "C:\ProgramData\VPI\VPIpython\x64\pyenvs\0" $env:PYTHONPATH = "" $env:TMM_DATA = "C:\Users\zsig\AppData\Local\Temp\VPIDS111" $env:TMM_LOGDIR = "C:\Users\zsig\AppData\Local\Temp\VPIDS111\log" $env:TMP = "C:\Users\zsig\AppData\Local\Temp" $env:USERNAME = "zsig" $env:USERPROFILE = "C:\Users\zsig" Start-Process "D:\U-WindowsUserData\TmpData\x64dbg\release\x64\x64dbg.exe" -ArgumentList @( '"C:\Program Files\VPI\VPIdesignSuite 11.1\simeng\bin\x64\ptcl.exe"', '--', '-port 7802', '-id 1', '-dir "C:\Users\zsig\AppData\Local\Temp\VPIDS111\jobs\2"', '-x' ) ``` x64dbg 跳过 `pde.exe` 的 `CreateProcessW` 进程创建,IDA 内找到 `ptcl.exe` 的断点在 ret 之前: ```asm .text:000000014124094C cmp rsi, 3 .text:0000000141240950 jl loc_1412408C1 .text:0000000141240956 mov rax, rdi .text:0000000141240959 mov rcx, [rsp+98h+var_20] .text:000000014124095E xor rcx, rsp ; StackCookie .text:0000000141240961 call __security_check_cookie .text:0000000141240966 mov rbx, [rsp+98h+arg_0] .text:000000014124096E add rsp, 80h .text:0000000141240975 pop r14 .text:0000000141240977 pop rdi .text:0000000141240978 pop rsi .text:0000000141240979 retn ``` 例如 `.text:0000000141240966` 地址,由于 IDA 静态时默认起始地址 `0x140000000`,对应的实际运行时断点位置为 `ptcl.exe + 0x14124F5D2 - 0x140000000`; x64dbg 下 `Ctrl+G` 在 `x64dbg.exe` 中导航到这一地址位置设置断点,`F9` 继续运行 `ptcl.exe` 和 `pde.exe` 使二者正常完成初始化 HTTP 通信,开始读取文件和解密(bp fopen 会无法初始化,还是需要手动设用户段的断点): ```asm 00007FF7A9D90935 | 48:FFC3 | inc rbx | 00007FF7A9D90938 | 48:3B5C24 48 | cmp rbx,qword ptr ss:[rsp+48] | 00007FF7A9D9093D | 72 A1 | jb ptcl.7FF7A9D908E0 | 00007FF7A9D9093F | 48:8D4C24 38 | lea rcx,qword ptr ss:[rsp+38] | 00007FF7A9D90944 | E8 C7D6FFFF | call ptcl.7FF7A9D8E010 | 00007FF7A9D90949 | 48:FFC6 | inc rsi | 00007FF7A9D9094C | 48:83FE 03 | cmp rsi,3 | 00007FF7A9D90950 | 0F8C 6BFFFFFF | jl ptcl.7FF7A9D908C1 | 00007FF7A9D90956 | 48:8BC7 | mov rax,rdi | rax:L"륐䏼˒", rdi:L"륐䏼˒" 00007FF7A9D90959 | 48:8B4C24 78 | mov rcx,qword ptr ss:[rsp+78] | 00007FF7A9D9095E | 48:33CC | xor rcx,rsp | 00007FF7A9D90961 | E8 CAD71600 | call ptcl.7FF7A9EFE130 | 00007FF7A9D90966 | 48:8B9C24 A0000000 | mov rbx,qword ptr ss:[rsp+A0] | [rsp+A0]:L"ꘀ꩙翷" 00007FF7A9D9096E | 48:81C4 80000000 | add rsp,80 | 00007FF7A9D90975 | 41:5E | pop r14 | r14:L"ꘀ꩙翷" 00007FF7A9D90977 | 5F | pop rdi | rdi:L"륐䏼˒" 00007FF7A9D90978 | 5E | pop rsi | 00007FF7A9D90979 | C3 | ret | RAX : 000000FEC42FE638 L"륐䏼˒" RBX : 0000000000000009 RCX : D51423B4C5910000 RDX : 000000FEC42FE5C8 L" 8˒" RBP : 000000FEC42FE770 RSP : 000000FEC42FE570 &L"椀歳映汵ld˒" RSI : 0000000000000003 RDI : 000000FEC42FE638 L"륐䏼˒" R8 : 0000000000000038 '8' R9 : 00007FFDAC7D216F vcruntime140.00007FFDAC7D216F R10 : 00007FFDAC7C0000 vcruntime140.00007FFDAC7C0000 R11 : 000000FEC42FE530 L"ꗨ꩙翷" R12 : 0000000000000001 R13 : 000000000000000A R14 : 00007FF7AA59A5E8 L"ꘀ꩙翷" R15 : 000002D24456AB40 RIP : 00007FF7A9D90966 ptcl.00007FF7A9D90966 RFLAGS : 0000000000000344 L'̈́' ``` 停止在 `00007FF7A9D90966` (IDA内`0x141240956`附近),RAX 内部地址为 `000000FEC42FE638`,是函数返回的目标字符串对象指针,RAX 内存镜像 `dump 000000FEC42FE638`: ```asm 000000FEC42FE638 50 B9 FC 43 D2 02 00 00 31 34 3A 33 32 6E 00 00 P¹üCÒ...14:32n.. 000000FEC42FE648 52 00 00 00 00 00 00 00 69 00 00 00 00 00 00 00 R.......i....... 000000FEC42FE658 81 23 9B E7 EA D5 00 00 00 00 00 00 00 00 00 00 .#.çêÕ.......... 000000FEC42FE668 CD DF D9 A9 F7 7F 00 00 60 89 12 44 D2 02 00 00 ÍßÙ©÷...`..DÒ... 000000FEC42FE678 80 E9 2F C4 FE 00 00 00 80 E9 2F C4 FE 00 00 00 .é/Äþ....é/Äþ... 000000FEC42FE688 00 00 00 00 46 00 34 00 30 00 38 00 31 00 41 00 ....F.4.0.8.1.A. 000000FEC42FE698 00 00 00 00 00 00 00 00 44 83 03 00 43 00 46 00 ........D...C.F. ``` 前 8 字节为 `50 B9 FC 43 D2 02 00 00`,转换成地址是:`0x000002D243FCB950`,这是数据指针;偏移 16 字节(`+0x10`)处为 `52 00 00 00 00 00 00 00`,十六进制 `0x52` = 十进制 82,说明 Blowfish 算法使用的密钥长度是 82 字节。 去数据区寻找秘钥 `dump 0x000002D243FCB950`: ```asm 000002D243FCB950 36 39 72 31 31 34 6F 31 31 34 3A 33 32 6E 31 31 69r114o114:32n11 000002D243FCB960 31 20 31 30 32 69 31 30 38 65 33 32 65 31 32 30 1 102i108e32e120 000002D243FCB970 69 31 31 35 74 31 30 35 6E 31 30 33 38 30 72 31 i115t105n10380r1 000002D243FCB980 31 31 66 31 30 35 6C 31 30 31 20 39 38 61 31 30 11f105l101 98a10 000002D243FCB990 30 36 38 69 31 31 35 6B 33 32 66 31 31 37 6C 31 068i115k32f117l1 000002D243FCB9A0 30 38 00 00 70 00 75 00 74 00 73 00 5C 00 6F 00 08..p.u.t.s.\.o. ``` 可以看到秘钥为 `69r114o114:32n111 102i108e32e120i115t105n10380r111f105l101 98a10068i115k32f117l108` 看起来仍然是混淆的,这并不是个好现象,不过秘钥指针传递到了 v24,一定是被后续使用到。 继续静态分析,看后续代码 `blowfish_start_decrypt`,`v24` 传递给函数第四个参数,函数内 `a4` 就是这一秘钥字符串: ```cpp // Hidden C++ exception states: #wind=3 void *__fastcall blowfish_start_decrypt(_DWORD *a1, __int64 a2, _DWORD *a3, void *a4) { int v8; // edi __int64 i; // rax int v10; // edx int v11; // ecx void *v12; // r14 __int64 v13; // rax int v14; // eax size_t v15; // rbx void *v16; // rdi void *v18; // [rsp+30h] [rbp-A8h] _BYTE v19[32]; // [rsp+48h] [rbp-90h] BYREF _BYTE v20[32]; // [rsp+68h] [rbp-70h] BYREF void *v21; // [rsp+88h] [rbp-50h] v21 = a4; v8 = 0; for ( i = 0; i < 4; ++i ) { v10 = *(char *)(i + a2); v11 = v10 + 256; if ( v10 >= 0 ) v11 = *(char *)(i + a2); v8 += v11; if ( i < 3 ) v8 <<= 8; } v12 = (void *)operator new(*a3 - 4); v18 = (void *)sub_14123DBC0(v19, a4); v13 = sub_14123DBC0(v20, v18); blowfish_decrypt_init(a1, v13); sub_14123E010(v18); *a1 = 1; v14 = blowfish_crypt_exec((_DWORD)a1, a2, 4, *a3 - 4, (__int64)v12, 0); *a3 = v8; if ( v8 == v14 ) { sub_14123E010(a4); return v12; } else { v15 = v8; v16 = (void *)operator new(v8); memcpy(v16, v12, v15); j_j_free_0(v12); sub_14123E010(a4); return v16; } } ``` `sub_14123DBC0` 使用 `a4` 字符串赋值给了 `v13`,`v13` 在下一个 `blowfish_decrypt_init` 函数中被处理为 Blowfish 解密需要的 S-Box 和 P-Array,存入`a1 + 0x68(104)`和`a1 + 0x24(32)`: ```cpp void __fastcall blowfish_decrypt_init(__int64 a1, _QWORD *a2) { _OWORD *v3; // rax _OWORD *v4; // rcx __int64 v5; // rdi __int64 v6; // rdx _OWORD *v7; // rax _OWORD *v8; // rcx __int64 v9; // rdx _OWORD *v10; // rax _OWORD *v11; // rcx __int64 v12; // rdx _OWORD *v13; // rax _OWORD *v14; // rcx __int64 v15; // rdx int v16; // edx int v17; // r8d __int64 v18; // rsi int v19; // esi int v20; // ebp __int64 v21; // rax int v22; // r14d int v23; // r15d int v24; // r12d __int64 v25; // rax int v26; // r11d unsigned __int64 v27; // rdx __int64 v28; // r10 _QWORD *v29; // rsi unsigned __int64 v30; // rbp int v31; // ecx int v32; // edx int v33; // r8d int v34; // edx int v35; // r9d int v36; // edx int v37; // ecx int v38; // esi int v39; // r14d __int64 v40; // rbp __int64 v41; // r13 int v42; // r13d __int64 v43; // rax _DWORD *v44; // rdx int v45; // [rsp+30h] [rbp-78h] BYREF int v46; // [rsp+34h] [rbp-74h] int v47; // [rsp+38h] [rbp-70h] _QWORD *v48; // [rsp+40h] [rbp-68h] unsigned __int64 v49; // [rsp+48h] [rbp-60h] void *v50; // [rsp+50h] [rbp-58h] __int64 v51; // [rsp+58h] [rbp-50h] _QWORD *v52; // [rsp+60h] [rbp-48h] v51 = -2; v50 = a2; v52 = a2; if ( a2[3] < 0x10u ) v48 = a2; else v48 = (_QWORD *)*a2; v49 = a2[2]; v3 = *(_OWORD **)(a1 + 104); v4 = &unk_141A6C0D0; v5 = 8; v6 = 8; do { *v3 = *v4; v3[1] = v4[1]; v3[2] = v4[2]; v3[3] = v4[3]; v3[4] = v4[4]; v3[5] = v4[5]; v3[6] = v4[6]; v3 += 8; *(v3 - 1) = v4[7]; v4 += 8; --v6; } while ( v6 ); v7 = (_OWORD *)(*(_QWORD *)(a1 + 104) + 1024LL); v8 = &unk_141A6C4D0; v9 = 8; do { *v7 = *v8; v7[1] = v8[1]; v7[2] = v8[2]; v7[3] = v8[3]; v7[4] = v8[4]; v7[5] = v8[5]; v7[6] = v8[6]; v7 += 8; *(v7 - 1) = v8[7]; v8 += 8; --v9; } while ( v9 ); v10 = (_OWORD *)(*(_QWORD *)(a1 + 104) + 2048LL); v11 = &unk_141A6C8D0; v12 = 8; do { *v10 = *v11; v10[1] = v11[1]; v10[2] = v11[2]; v10[3] = v11[3]; v10[4] = v11[4]; v10[5] = v11[5]; v10[6] = v11[6]; v10 += 8; *(v10 - 1) = v11[7]; v11 += 8; --v12; } while ( v12 ); v13 = (_OWORD *)(*(_QWORD *)(a1 + 104) + 3072LL); v14 = &unk_141A6CCD0; v15 = 8; do { *v13 = *v14; v13[1] = v14[1]; v13[2] = v14[2]; v13[3] = v14[3]; v13[4] = v14[4]; v13[5] = v14[5]; v13[6] = v14[6]; v13 += 8; *(v13 - 1) = v14[7]; v14 += 8; --v15; } while ( v15 ); memcpy(*(void **)(a1 + 24), &unk_141A6C080, 4LL * (*(_DWORD *)(a1 + 20) + 2)); v16 = 0; v45 = 0; v17 = 0; v46 = 0; v18 = 10; while ( 1 ) { blowfish_encrypt(a1, v16, v17, (unsigned int)&v45, 0); if ( !--v18 ) break; v17 = v46; v16 = v45; } v19 = v46; v20 = v45; if ( v45 != -1426174275 || v46 != 651634172 ) { v21 = sub_14002CE50(std::cout, "Blowfish: Self Test 1 failed: encrypt^10(0) ="); std::ostream::operator<<(v21, sub_14002DE00); } v47 = 9; v22 = 9; while ( 1 ) { blowfish_decrypt(a1, v20, v19, (unsigned int)&v45, 0); if ( --v22 < 0 ) break; v19 = v46; v20 = v45; } v23 = v46; v24 = v45; if ( v45 || v46 ) { v25 = sub_14002CE50(std::cout, "Blowfish: Self Test 1 failed: decrypt^10(encrypt^10(0)) = "); std::ostream::operator<<(v25, sub_14002DE00); } v26 = 0; LODWORD(v27) = 0; if ( *(_DWORD *)(a1 + 20) + 2 > 0 ) { v28 = 0; v29 = v48; v30 = v49; do { v31 = *((unsigned __int8 *)v29 + (int)v27); v32 = ((int)v27 + 1) % v30; v33 = (v31 << 8) | *((unsigned __int8 *)v29 + v32); v34 = (v32 + 1) % v30; v35 = (v33 << 8) | *((unsigned __int8 *)v29 + v34); v36 = (v34 + 1) % v30; v37 = (v35 << 8) | *((unsigned __int8 *)v29 + v36); v27 = (v36 + 1) % v30; *(_DWORD *)(v28 + *(_QWORD *)(a1 + 24)) ^= v37; ++v26; v28 += 4; } while ( v26 < *(_DWORD *)(a1 + 20) + 2 ); } blowfish_encrypt(a1, 0, 0, *(_QWORD *)(a1 + 24), 0); v38 = 2; v39 = 2; if ( *(_DWORD *)(a1 + 20) + 2 > 2 ) { v40 = 8; do { blowfish_encrypt( a1, *(_DWORD *)(*(_QWORD *)(a1 + 24) + v40 - 8), *(_DWORD *)(*(_QWORD *)(a1 + 24) + v40 - 4), *(_QWORD *)(a1 + 24), v39); v39 += 2; v40 += 8; } while ( v39 < *(_DWORD *)(a1 + 20) + 2 ); } blowfish_encrypt( a1, *(_DWORD *)(*(_QWORD *)(a1 + 24) + 4LL * *(int *)(a1 + 20)), *(_DWORD *)(*(_QWORD *)(a1 + 24) + 4LL * (*(_DWORD *)(a1 + 20) + 1)), *(_QWORD *)(a1 + 104), 0); do { blowfish_encrypt( a1, *(_DWORD *)(v5 + *(_QWORD *)(a1 + 104) - 8), *(_DWORD *)(v5 + *(_QWORD *)(a1 + 104) - 4), *(_QWORD *)(a1 + 104), v38); v38 += 2; v5 += 8; } while ( v38 < 1024 ); v41 = 10; while ( 1 ) { blowfish_encrypt(a1, v24, v23, (unsigned int)&v45, 0); if ( !--v41 ) break; v23 = v46; v24 = v45; } v42 = v47; do { blowfish_decrypt(a1, v45, v46, (unsigned int)&v45, 0); --v42; } while ( v42 >= 0 ); if ( v45 || v46 ) { v43 = sub_14002CE50(std::cout, "Blowfish: Self Test 2 failed: decrypt^10(encrypt^10(0)) = "); std::ostream::operator<<(v43, sub_14002DE00); } if ( *(_DWORD *)(a1 + 20) == *(_DWORD *)(a1 + 4) ) { v44 = *(_DWORD **)(a1 + 24); *(_DWORD *)(a1 + 32) = *v44; *(_DWORD *)(a1 + 36) = v44[1]; *(_DWORD *)(a1 + 40) = v44[2]; *(_DWORD *)(a1 + 44) = v44[3]; *(_DWORD *)(a1 + 48) = v44[4]; *(_DWORD *)(a1 + 52) = v44[5]; *(_DWORD *)(a1 + 56) = v44[6]; *(_DWORD *)(a1 + 60) = v44[7]; *(_DWORD *)(a1 + 64) = v44[8]; *(_DWORD *)(a1 + 68) = v44[9]; *(_DWORD *)(a1 + 72) = v44[10]; *(_DWORD *)(a1 + 76) = v44[11]; *(_DWORD *)(a1 + 80) = v44[12]; *(_DWORD *)(a1 + 84) = v44[13]; *(_DWORD *)(a1 + 88) = v44[14]; *(_DWORD *)(a1 + 92) = v44[15]; *(_DWORD *)(a1 + 96) = v44[16]; *(_DWORD *)(a1 + 100) = v44[17]; } sub_14123E010(v50); } ``` 之后 `blowfish_crypt_exec` 使用了 `a1` 中储存的 S-Box 和 P-Array,进行分块解密,返回值 `v11` 为数据长度,这是最终解密操作的核心: ```cpp v14 = blowfish_crypt_exec((_DWORD)a1, a2, 4, *a3 - 4, (__int64)v12, 0); ``` 数据写入到前面申请的地址空间 `v12 = (void *)operator new(*a3 - 4);`,`v12` 作为第五个参数传递到函数中: ```cpp __int64 __fastcall blowfish_crypt_exec(_DWORD *a1, int a2, int a3, int a4, __int64 a5, int a6) { int v6; // ebp int v10; // eax unsigned int v11; // ebp int v12; // esi __int64 v13; // r14 int v14; // esi __int64 v15; // r14 v6 = a1[2]; v10 = a4 / v6; v11 = a4 / v6 * v6; if ( *a1 ) { if ( v10 > 0 ) { v14 = a6; v15 = (unsigned int)v10; do { blowfish_block_decrypt((_DWORD)a1, a2, a3, a5, v14); a3 += a1[2]; v14 += a1[2]; --v15; } while ( v15 ); } } else if ( v10 > 0 ) { v12 = a6; v13 = (unsigned int)v10; do { blowfish_block_encrypt((_DWORD)a1, a2, a3, a5, v12); a3 += a1[2]; v12 += a1[2]; --v13; } while ( v13 ); } return v11; } ``` 因此,在上一级函数 `blowfish_start_decrypt`动态调试,返回 `v12` 前打一个断点,读取 `v12(R12)` 即为解密后的数据,`a1(RDI) + 0x68` 和 `a1(RDI) + 0x20` 的两个偏置位置则为 S-Box 和 P-Array 数据,现在继续动态调试: ``` 00007FF7A9D9F6E0 | E8 BBF1FFFF | call ptcl.7FF7A9D9E8A0 | 00007FF7A9D9F6E5 | 035F 08 | add ebx,dword ptr ds:[rdi+8] | 00007FF7A9D9F6E8 | 0377 08 | add esi,dword ptr ds:[rdi+8] | 00007FF7A9D9F6EB | 49:83EE 01 | sub r14,1 | 00007FF7A9D9F6EF | 75 DF | jne ptcl.7FF7A9D9F6D0 | 00007FF7A9D9F6F1 | 48:8B5C24 50 | mov rbx,qword ptr ss:[rsp+50] | [rsp+50]:L"가䏺˒" 00007FF7A9D9F6F6 | 8BC5 | mov eax,ebp | 00007FF7A9D9F6F8 | 48:8B6C24 58 | mov rbp,qword ptr ss:[rsp+58] | [rsp+58]:L"돐䏼˒" 00007FF7A9D9F6FD | 48:8B7424 60 | mov rsi,qword ptr ss:[rsp+60] | 00007FF7A9D9F702 | 48:8B7C24 68 | mov rdi,qword ptr ss:[rsp+68] | 00007FF7A9D9F707 | 48:83C4 30 | add rsp,30 | 00007FF7A9D9F70B | 41:5F | pop r15 | 00007FF7A9D9F70D | 41:5E | pop r14 | 00007FF7A9D9F70F | 41:5C | pop r12 | r12:"<PSW>MTlpZ29yNjc=</PSW> 00007FF7A9D9F711 | C3 | ret | ``` 停止在 `00007FF7A9D9F6F6`,对应 IDA 的 `.text:000000014124F6F6`,做内存转储 `dump R12`,直接报错解密后数据即可。 查看对应的 `RDI + 0x20` 和 `RDI + 0x68` 地址位置的 S-Box 和 P-Array,`RDI + 0x20` 可以直接转储,18 个 P-Array 秘钥连续储存,`RDI + 0x68` 的 S-Box 数据是字符串对象,同理找到其数据区地址 `0x000000FEC42FE760`: ``` RDI + 0x20 000000FEC42FE760 BE F0 B2 D2 33 E7 09 5E 9C F5 4D 57 11 9F 29 F8 ¾ð²Ò3ç.^.õMW..)ø 000000FEC42FE770 CD 0F 17 0F 12 A9 74 CE B0 63 04 9D 30 FF 05 80 Í....©tΰc..0ÿ.. 000000FEC42FE780 C4 35 77 93 CB 69 38 BE 0C 14 27 CC FD B7 D0 68 Ä5w.Ëi8¾..'Ìý·Ðh 000000FEC42FE790 FD DC C5 31 F9 F7 CA F5 61 75 DA 4C 3F 41 4D 19 ýÜÅ1ù÷ÊõauÚL?AM. 000000FEC42FE7A0 D8 E4 7B 77 B9 7B CB 32 90 72 55 44 D2 02 00 00 Øä{w¹{Ë2.rUDÒ... --- RDI + 0x68 000000FEC42FE7A8 90 72 55 44 D2 02 00 00 D5 D2 32 F0 48 4F 37 97 .rUDÒ...ÕÒ2ðHO7. 000000FEC42FE7B8 36 0C 61 1E AA 8C F5 64 90 02 00 00 00 00 00 00 6.a.ª.õd........ 000000FEC42FE7C8 30 36 38 69 31 31 35 6B 33 32 66 31 31 37 6C 31 068i115k32f117l1 000000FEC42FE7D8 30 38 31 30 32 69 31 30 38 65 33 32 65 31 32 30 08102i108e32e120 000000FEC42FE7E8 69 31 31 35 74 31 30 35 6E 31 30 33 38 30 72 31 i115t105n10380r1 000000FEC42FE7F8 31 31 66 31 30 35 6C 31 30 31 20 39 38 61 31 30 11f105l101 98a10 000000FEC42FE808 98 E9 2F C4 FE 00 00 00 A0 EA 2F C4 FE 00 00 00 .é/Äþ... ê/Äþ... ``` 获取数据区内容 `dump 0x000000FEC42FE760`: ``` 000002D244557290 F4 18 A3 4C 20 78 81 CD 81 D1 D8 91 10 E1 E9 F2 ô.£L x.Í.ÑØ..áéò 000002D2445572A0 C8 0C 5B 96 FB 1B AD 0A BF 68 BB CC 22 E3 AE AF È.[.û...¿h»Ì"㮯 000002D2445572B0 DD 37 D1 B3 52 A6 35 AD 94 EE 1C 31 D7 ED 9F E4 Ý7ѳR¦5..î.1×í.ä 000002D2445572C0 5C DD 88 27 E6 A0 63 93 43 4F 38 91 6C 72 76 37 \Ý.'æ c.CO8.lrv7 000002D2445572D0 D2 A2 7E 84 9C 3B E7 8D 97 92 13 01 DD 28 F9 5F Ò¢~..;ç.....Ý(ù_ ...省略,总共 4096 字节,4 个连续储存的 S-BOX 秘钥转储保存 ``` 有了 S-Box 和 P-Array,也可以手动做 Blowfish ECB 解码获取数据 ```python import struct import os class BlowfishCustom: def __init__(self, p_array, s_boxes): """ :param p_array: 18个 uint32 列表 :param s_boxes: 4x256个 uint32 列表 (二维列表) """ self.p = p_array self.s = s_boxes def _f(self, x): h = self.s[0][(x >> 24) & 0xFF] + self.s[1][(x >> 16) & 0xFF] h ^= self.s[2][(x >> 8) & 0xFF] h += self.s[3][x & 0xFF] return h & 0xFFFFFFFF def decrypt_block(self, block): xl, xr = struct.unpack(">II", block) # Blowfish 内部处理大端 for i in range(17, 1, -1): xl ^= self.p[i] xr ^= self._f(xl) xl, xr = xr, xl xl, xr = xr, xl xr ^= self.p[1] xl ^= self.p[0] return struct.pack(">II", xl, xr) def load_keys(p_path, s_path): # 读取 P-Array (18 * 4 = 72 bytes) with open(p_path, 'rb') as f: p_data = f.read(72) # 使用 <I 表示内存中通常是小端序存储的 32位整数 p_array = list(struct.unpack('<18I', p_data)) # 读取 S-Boxes (4 * 256 * 4 = 4096 bytes) with open(s_path, 'rb') as f: s_data = f.read(4096) all_s = struct.unpack('<1024I', s_data) s_boxes = [list(all_s[i*256:(i+1)*256]) for i in range(4)] return p_array, s_boxes def decrypt_vpi(file_path, p_array, s_boxes): bf = BlowfishCustom(p_array, s_boxes) with open(file_path, 'rb') as f: vpi_header = f.read(10) data_len_bytes = f.read(4) data_len = int.from_bytes(data_len_bytes, byteorder='big') encrypted_data = f.read() print(f"Header: {vpi_header}") print(f"Encrypted Data Size: {len(encrypted_data)}, Expected: {data_len}") decrypted_data = bytearray() # ECB 模式:每 8 字节一个块独立解密 for i in range(0, len(encrypted_data), 8): block = encrypted_data[i:i+8] if len(block) < 8: # 处理末尾填充 decrypted_data.extend(block) break decrypted_data.extend(bf.decrypt_block(block)) # 保存结果 # 从原文件名提取基础名称,输出到当前目录 base_name = os.path.basename(file_path) # 获取文件名(不含路径) output_name = os.path.splitext(base_name)[0] + ".dec" # 替换扩展名为.dec output_path = os.path.join(os.getcwd(), output_name) # 输出到当前目录 with open(output_path, 'wb') as f: f.write(decrypted_data[:data_len]) # 截断到原始长度 print(f"Decryption finished. Saved to {output_path}") # --- 使用示例 --- p_array, s_boxes = load_keys('dumped/parray_debugger.bin', 'dumped/sbox_debugger.bin') decrypt_vpi(r'DSPlib.py', p_array, s_boxes) ``` 这样就能离线解码出目标文件了: ``` Header: b'VPICRYPTV2' Encrypted Data Size: 230208, Expected: 230207 Decryption finished. Saved to D:\U-WindowsUserData\TmpData\VPI\DSPlib.dec ``` 总之这一代码库的加密部分,使用 Blowfish ECB 已经是加密的老一辈了,而且代码库也没有更多的做混淆或检测;寻找解密部分还算顺利,这次就说到这吧
回复或点赞可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
#调试逆向
#加密算法
收藏
・
0
点赞
・
13
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
git_51951meggadf3df
非常支持你的观点!
2026-9-11 01:54
mb_lthgjpwj
为你点赞!
2026-8-21 08:05
mb_wsoacceo
为你点赞!
2026-8-11 01:28
wx_晨梦
期待更多优质内容的分享,论坛有你更精彩!
2026-7-16 05:41
智童
非常支持你的观点!
2026-7-1 14:19
wjdidi
这个讨论对我很有帮助,谢谢!
2026-5-17 23:14
npc0vo
非常支持你的观点!
2026-4-25 21:49
daohaodaye
+1
这个讨论对我很有帮助,谢谢!
2026-4-15 19:19
fogSea
期待更多优质内容的分享,论坛有你更精彩!
2026-4-8 17:49
mb_zpvlfkkg
为你点赞!
2026-4-8 15:13
dragpn
你的帖子非常有用,感谢分享!
2026-3-16 10:39
顽劣
感谢你的积极参与,期待更多精彩内容!
2026-3-9 22:54
令狐双
感谢你的贡献,论坛因你而更加精彩!
2026-3-9 13:34
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
3
)
xingbing
雪 币:
158
活跃值:
(5671)
能力值:
( LV2,RANK:10 )
在线值:
发帖
2
回帖
1376
粉丝
2
关注
私信
xingbing
2
楼
感谢分享 !学习。
2026-3-9 15:40
0
小迷糊ddd
雪 币:
268
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
152
粉丝
0
关注
私信
小迷糊ddd
3
楼
感谢分享,学习一下。
2026-4-8 11:50
0
mb_wuyyqvbd
雪 币:
209
能力值:
( LV1,RANK:0 )
在线值:
发帖
1
回帖
4
粉丝
0
关注
私信
mb_wuyyqvbd
4
楼
英雄所见略同
2026-4-9 08:53
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
zerosignal
1
发帖
0
回帖
0
RANK
关注
私信
他的文章
[原创]Blowfish 解密 静态分析和动态调试流程记录
2241
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部