逆向 Blowfish 解密逻辑流程记录

逆向 Blowfish 解密 流程记录

在仿真途中遇到了一个 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 下可以看到创建的进程:

"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,寻找到一个字符串,发现字符串初始化结构:

int sub_140021720()
{
sub_14123DE40((__int64)&VPICRYPTV2_string, "VPICRYPTV2");
return atexit(sub_1416A3850);
}

这是一个字符串对象的初始化过程,atexit 是销毁逻辑,初始化时将固定位置的字符串复制到这一对象的数据区,IDA 中暂时标注为 VPICRYPTV2_string 对象;

接下来,寻找对象引用,按 x 找交叉引用,能够找到两个主要使用点,有一个较短,再向上找引用是 check_header 的逻辑,显然,并无解密逻辑;另一个伪代码较长,后续流程较为复杂,应该是要排查的内容:

if ( (unsigned int)compare_string_objects(v60, &VPICRYPTV1_string)
&& (unsigned int)compare_string_objects(v60, &VPICRYPTV2_string) )

上来就是一个字符串比较,清晰明了,进入这一分支的是解密流程,跳过压缩文件相关的逻辑,要看的是 VPICRYPTV2 解密逻辑:

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,重新混合组合成二进制秘钥字符串,下面是偏移处的数据:

.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.exepde.exe 界面程序创建进程,先调试 pde.exe,断点设置在创建进程相关接口:

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.exeCreateProcessW 进程创建,IDA 内找到 ptcl.exe 的断点在 ret 之前:

.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+Gx64dbg.exe 中导航到这一地址位置设置断点,F9 继续运行 ptcl.exepde.exe 使二者正常完成初始化 HTTP 通信,开始读取文件和解密(bp fopen 会无法初始化,还是需要手动设用户段的断点):

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

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

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_decryptv24 传递给函数第四个参数,函数内 a4 就是这一秘钥字符串:

// 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 字符串赋值给了 v13v13 在下一个 blowfish_decrypt_init 函数中被处理为 Blowfish 解密需要的 S-Box 和 P-Array,存入a1 + 0x68(104)a1 + 0x24(32)

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 为数据长度,这是最终解密操作的核心:

v14 = blowfish_crypt_exec((_DWORD)a1, a2, 4, *a3 - 4, (__int64)v12, 0);

数据写入到前面申请的地址空间 v12 = (void *)operator new(*a3 - 4);v12 作为第五个参数传递到函数中:

__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) + 0x68a1(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 + 0x20RDI + 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 解码获取数据

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 已经是加密的老一辈了,而且代码库也没有更多的做混淆或检测;寻找解密部分还算顺利,这次就说到这吧