逆向 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:0000000141 A4A5E8 off_141A4A5E8 dq offset aErrorNoFileExi .rdata:0000000141 A4A5E8 ; DATA XREF: sub_140E5 A0E0 :loc_140E5 A12D↑o .rdata:0000000141 A4A5E8 ; sub_1411D3970:loc_1411D39DE↑o ... .rdata:0000000141 A4A5E8 ; "Error: no file existing" .rdata:0000000141 A4A5F0 dq offset aProfileBad ; "Profile bad" .rdata:0000000141 A4A5F8 dq offset aDiskFull ; "Disk full" .rdata:0000000141 A4A600 aErrorNoFileExi db 'Error: no file existing' ,0 .rdata:0000000141 A4A600 ; DATA XREF: .rdata:off_141A4A5E8 ↑o .rdata:0000000141 A4A618 aProfileBad db 'Profile bad' ,0 ; DATA XREF: .rdata:0000000141 A4A5F0↑o .rdata:0000000141 A4A624 align 8 .rdata:0000000141 A4A628 aDiskFull db 'Disk full' ,0 ; DATA XREF: .rdata:0000000141 A4A5F8↑o .rdata:0000000141 A4A632 align 8
不过,这里的逻辑有些难以辨识,这一
get_VPICRYPTV2_key_string
函数给了一个明确的返回值,考虑直接动态调试查看秘钥情况。
由于 ptcl.exe 由 pde.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.exe 的 CreateProcessW
进程创建,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+G 在 x64dbg.exe
中导航到这一地址位置设置断点,F9 继续运行
ptcl.exe 和 pde.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_decrypt,v24
传递给函数第四个参数,函数内 a4 就是这一秘钥字符串:
void *__fastcall blowfish_start_decrypt (_DWORD *a1, __int64 a2, _DWORD *a3, void *a4) { int v8; __int64 i; int v10; int v11; void *v12; __int64 v13; int v14; size_t v15; void *v16; void *v18; _BYTE v19[32 ]; _BYTE v20[32 ]; void *v21; 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):
void __fastcall blowfish_decrypt_init (__int64 a1, _QWORD *a2) { _OWORD *v3; _OWORD *v4; __int64 v5; __int64 v6; _OWORD *v7; _OWORD *v8; __int64 v9; _OWORD *v10; _OWORD *v11; __int64 v12; _OWORD *v13; _OWORD *v14; __int64 v15; int v16; int v17; __int64 v18; int v19; int v20; __int64 v21; int v22; int v23; int v24; __int64 v25; int v26; unsigned __int64 v27; __int64 v28; _QWORD *v29; unsigned __int64 v30; int v31; int v32; int v33; int v34; int v35; int v36; int v37; int v38; int v39; __int64 v40; __int64 v41; int v42; __int64 v43; _DWORD *v44; int v45; int v46; int v47; _QWORD *v48; unsigned __int64 v49; void *v50; __int64 v51; _QWORD *v52; 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; int v10; unsigned int v11; int v12; __int64 v13; int v14; __int64 v15; 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 解码获取数据
import structimport osclass 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) 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 ): with open (p_path, 'rb' ) as f: p_data = f.read(72 ) p_array = list (struct.unpack('<18I' , p_data)) 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 () 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" 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
已经是加密的老一辈了,而且代码库也没有更多的做混淆或检测;寻找解密部分还算顺利,这次就说到这吧