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

mitmproxy linux docker 流量捕获

容器

拉取 Redroid (root) 容器,并寻找到其网卡名称,如 br-207943f7b031

mitmproxy

安装 mitmproxy,可选 redirect.py 来控制流,在 8888 端口启动即可

transparent --listen-host :: --listen-port 8888 -s redirect.py

nft 设置

加载 Netfilter NAT 相关的内核模块:

sudo modprobe nf_nat
sudo modprobe nf_nat_ipv4 # 针对 IPv4 的 NAT
sudo modprobe nf_nat_ipv6 # 针对 IPv6 的 NAT
sudo modprobe nf_tproxy # 有些系统或场景需要这个模块来支持透明代理的 redirect

# 检查模块是否已加载
lsmod | grep nf_nat
lsmod | grep nf_tproxy

设定一个 chain,引导流量到 8888

# IPv4 HTTP
sudo nft add rule ip nat mitmproxy_prerouting ip saddr 172.26.0.0/16 tcp dport 80 redirect to :8888
# IPv4 HTTPS
sudo nft add rule ip nat mitmproxy_prerouting ip saddr 172.26.0.0/16 tcp dport 443 redirect to :8888

# IPv6 HTTP
sudo nft add rule ip6 nat mitmproxy_prerouting ip6 saddr ::/0 tcp dport 80 redirect to :8888
# IPv6 HTTPS
sudo nft add rule ip6 nat mitmproxy_prerouting ip6 saddr ::/0 tcp dport 443 redirect to :8888

检查 rulelist:

sudo nft list ruleset

应存在引导流量的 chain ,在 ipv4 ipv6 中,各有一个:

chain mitmproxy_prerouting {
ip saddr 172.26.0.0/16 tcp dport 80 redirect to :8888
ip saddr 172.26.0.0/16 tcp dport 443 redirect to :8888
}

添加跳转规则:

# IPv4 jump rule
sudo nft insert rule ip nat PREROUTING iifname "br-207943f7b031" goto mitmproxy_prerouting

# IPv6 jump rule
sudo nft insert rule ip6 nat PREROUTING iifname "br-207943f7b031" goto mitmproxy_prerouting

这样,透明代理构建完成

证书安装

为了捕获 HTTPS 流,需要将 Android 设备的系统证书做修改,信任 mitmproxy 的证书

确保证书文件存在于主机路径:~/.mitmproxy/mitmproxy-ca-cert.cer

如上证书,安装 mitmproxy 运行之后应当出现

使用 adb ,将证书推送到 Redroid 容器的临时目录:

adb push ~/.mitmproxy/mitmproxy-ca-cert.cer /data/local/tmp/

通过 ADB 连接到容器:

adb shell

在容器内执行以下命令,获取证书的哈希值(用于重命名文件):

# 进入临时目录
cd /data/local/tmp

# 计算哈希值(输出为8位十六进制字符串)
HASH=$(openssl x509 -inform PEM -subject_hash_old -in mitmproxy-ca-cert.cer | head -1)
echo $HASH # 示例输出:c8750f0d

利用 Magisk 的 root 权限将证书复制到系统证书存储:

# 挂载系统分区为可读写
mount -o rw,remount /system

# 复制证书到系统证书目录
cp mitmproxy-ca-cert.cer /system/etc/security/cacerts/

# 重命名证书为 <哈希值>.0
mv /system/etc/security/cacerts/mitmproxy-ca-cert.cer /system/etc/security/cacerts/${HASH}.0

# 设置权限(必须为644)
chmod 644 /system/etc/security/cacerts/${HASH}.0
chown root:root /system/etc/security/cacerts/${HASH}.0

# 重新挂载为只读(可选)
mount -o ro,remount /system

系统分区的 mount 可能存在不同,查看 df,根据 Android 版本选择分区修改

重启容器,使证书生效:

reboot

PaddlePaddle AiStudio Torch 配置 百度训练平台

PaddlePaddle AiStudio 平台提供 V100 GPU,然而默认只有 PaddlePaddle 百度自有框架,Conda 环境不独立,使用 external-libraries 文件夹存在路径缺陷,所以这里记录改环境到 Torch 的过程,包含一些语音处理包的安装。

目标环境:Python 3.11 + Torch 2.5.1 cu118

阅读全文 »

GTNH 传送权限单独指定

起因

在 GTNH 2.7.2 上,对于 Journey Map 传送点的支持需要 op 管理员权限,给管理员权限容易刷物件和其他意外,所以需要限制权限同时又能方便移动。

阅读全文 »

Linux dGPU Nvidia 独显运行应用

在 Ubuntu 22.04 上

系统带有工具 switcherooctl 可以切换系统使用的 gpu 卡,若默认连接显示到集成显卡,使用此工具时,将使用独显运行应用

若无法运行,可以使用 vkcube 来检查 Vulcan 的可用性,工具来自于包 vulkan-tools 直接 apt 安装即可。

可能出现无法调用情况如下:

vkEnumerateInstanceExtensionProperties failed to find the VK_KHR_surface extension. Do you have a compatible Vulkan installable client driver (ICD) installed? Please look at the Getting Started guide for additional information.

此时需要检查 icd 文件是否可用

ls /usr/share/vulkan/icd.d/nvidia_icd.json

若无文件,则需要安装库

sudo apt install vulkan-tools libvulkan1 libvulkan1:i386

若安装后仍不可用,检查是否有 Nvidia-wrapper

$ ls /usr/share/vulkan/icd.d/ 
intel_hasvk_icd.i686.json intel_icd.x86_64.json nvidia_icd.disabled_by_nv_vulkan_wrapper radeon_icd.x86_64.json intel_hasvk_icd.x86_64.json lvp_icd.i686.json nv_vulkan_wrapper.json virtio_icd.i686.json intel_icd.i686.json lvp_icd.x86_64.json radeon_icd.i686.json virtio_icd.x86_64.json

恢复配置:

sudo mv /usr/share/vulkan/icd.d/nvidia_icd.disabled_by_nv_vulkan_wrapper /usr/share/vulkan/icd.d/nvidia_icd.json

此时再切换独显运行可行。

注意,我并未发现哪部分程序给出了 nv_vulkan_wrapper,若可行,从添加 wrapper 的程序关闭可能更合理

使用环境变量运行 windows 软件

在解决 icd 问题后,环境变量也可以用作切换运行的方式,毕竟 switcherooctl launch 的原理本质也是切换环境变量。

例如使用 Steam 的 Proton 9.0-4 运行 Windows 应用,独显启动:

先要确保手动在 Steam 中添加可执行文件到游戏库中,并在命令行启动的 Steam 上获取其编号,或者在集显运行时查看其环境变量中的 SteamGameId

之后,为应用添加必要的环境变量,无需启动 Steam 即可运行应用本体

注意,最好替换 SteamGameId ,这是 Steam 做运行环境隔离的措施,使用程序自己的环境运行更稳定

此外,一定要替换 proton 的运行路径,我的路径只是 Proton 9.0 配置,随着版本更新,指令会有所不同。

此指令从 ps -aux | grep proton 获取,对于后续 Steam 启动 proton 的指令,可以通过如上方案获取启动指令模版。

__NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia __VK_LAYER_NV_optimus=NVIDIA_only VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.json SteamGameId=2907931734 STEAM_COMPAT_DATA_PATH=$HOME/.local/share/Steam/steamapps/compatdata/$SteamGameId STEAM_COMPAT_CLIENT_INSTALL_PATH=~/.local/share/Steam/ python3 "$HOME/.local/share/Steam/steamapps/common/Proton 9.0 (Beta)/proton" waitforexitandrun "/media/zsig/生活日常/Games/Grand Theft Auto V/GTA5.exe"

Wine 运行

Wine + dxvk 手动运行也可以,例如使用 jadeite 启动 Unity 程序

DXVK_HUD="fps,frametimes,version,gpuload" GST_PLUGIN_PATH="" HOST_LANG="zh_CN.UTF-8" HOST_LC_ALL="zh_CN.UTF-8" LANG="zh_CN.UTF-8" LC_ALL="zh_CN.UTF-8" LD_LIBRARY_PATH="/home/zsig/.var/app/moe.launcher.the-honkers-railway-launcher/data/honkers-railway-launcher/runners/wine-10.1-staging-tkg-amd64/lib:/home/zsig/.var/app/moe.launcher.the-honkers-railway-launcher/data/honkers-railway-launcher/runners/wine-10.1-staging-tkg-amd64/lib/wine/x86_64-unix:/home/zsig/.var/app/moe.launcher.the-honkers-railway-launcher/data/honkers-railway-launcher/runners/wine-10.1-staging-tkg-amd64/lib/wine/i386-unix" WINEARCH="win64" WINEFSYNC="1" WINEPREFIX="/home/zsig/.var/app/moe.launcher.the-honkers-railway-launcher/data/honkers-railway-launcher/prefix" WINE_FULLSCREEN_FSR="1" WINE_FULLSCREEN_FSR_MODE="balanced" WINE_FULLSCREEN_FSR_STRENGTH="2" JADEITE_ALLOW_UNKNOWN=1 bash -c "'/home/zsig/.var/app/moe.launcher.the-honkers-railway-launcher/data/honkers-railway-launcher/runners/wine-10.1-staging-tkg-amd64/bin/wine64'  '/home/zsig/.var/app/moe.launcher.the-honkers-railway-launcher/data/honkers-railway-launcher/patch/jadeite.exe' '/home/zsig/Documents/SkyLines/Cities - Skylines II/Cities2.exe' -- "

MPU6050 参数读取 Jetson

编译动态链接库

I2CDevLib仓库
选用Linux上驱动I2C和MPU6050的代码,克隆LinuxI2CDev文件夹到本地,然后进入到文件夹中,创建一个main.cpp用来创建与Python的函数接口,可以自定义。这里的代码没有考虑零偏,只是从DMP取出四元数换算得到结果的,实际用的时候有不小的零偏,可以添加上初始化时的零偏纠正过程。

#include <stdio.h>  
#include "MPU6050/MPU6050_6Axis_MotionApps20.h"

extern "C" {
    MPU6050 mpu;
    bool dmp_initialized = false;

    void initialize_dmp() {
        // 初始化 MPU6050
        mpu.initialize();

        // 检查设备连接
        if (!mpu.testConnection()) {
            printf("MPU6050 connection failed\n");
            return;
        }

        // 初始化 DMP
        if (mpu.dmpInitialize() != 0) {
            printf("DMP initialization failed\n");
            return;
        }

        // 启用 DMP
        mpu.setDMPEnabled(true);

        dmp_initialized = true;
    }

    void get_yaw_pitch_roll(float *yaw, float *pitch, float *roll) {
        if (!dmp_initialized) {
            printf("DMP not initialized\n");
            return;
        }

        // 获取 DMP 数据包大小
        uint16_t packetSize = mpu.dmpGetFIFOPacketSize();
        uint8_t fifoBuffer[64];

        // 检查是否有可用的 DMP 数据包
        if (mpu.dmpPacketAvailable()) {
            // 读取 DMP 数据包
            mpu.dmpGetCurrentFIFOPacket(fifoBuffer);

            // 获取四元数
            Quaternion q;
            mpu.dmpGetQuaternion(&q, fifoBuffer);

            // 获取重力向量
            VectorFloat gravity;
            mpu.dmpGetGravity(&gravity, &q);

            // 计算 yaw, pitch, roll
            float ypr[3];
            mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);

            // 将结果赋值给输出参数
            *yaw = ypr[0];
            *pitch = ypr[1];
            *roll = ypr[2];
        }
    }
}

配置CmakeLists.txt,当然,在配置成动态链接库之前,可以编译成可执行程序验证正确性。

cmake_minimum_required(VERSION 3.10)  

# 项目名称
project(MPU6050_Project)

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# 设置 -fPIC 选项
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# 添加 I2Cdev 库
add_library(I2Cdev I2Cdev/I2Cdev.cpp)
target_include_directories(I2Cdev PUBLIC I2Cdev)

# 添加 MPU6050 库
add_library(MPU6050 MPU6050/MPU6050.cpp MPU6050/MPU6050_6Axis_MotionApps20.cpp)
target_include_directories(MPU6050 PUBLIC MPU6050)

# 链接 I2Cdev 库到 MPU6050 库
target_link_libraries(MPU6050 I2Cdev)

# 添加 main 可执行文件
add_library(mpu6050_lib SHARED main.cpp)
target_include_directories(mpu6050_lib PUBLIC .)

# 链接 MPU6050 库到 main 可执行文件
target_link_libraries(mpu6050_lib MPU6050)

创建build文件夹,进入build文件夹,执行cmake ..,然后make,即可看到libmpu6050_lib.so,记住路径,或者移动到python代码同目录下

调用和获取数据

使用如下代码,可以加载并自动获取yaw, pitch, roll角度。这里使用单独的线程,因为不连续读取时会出现错误数据,原因未知。

# 加载动态链接库
lib = ctypes.CDLL('./cpp/libmpu6050_lib.so')

# 定义初始化函数原型
lib.initialize_dmp.argtypes = []
lib.initialize_dmp.restype = None

# 定义获取角度函数原型
lib.get_yaw_pitch_roll.argtypes = [ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float)]
lib.get_yaw_pitch_roll.restype = None

# 初始化 DMP
lib.initialize_dmp()

# 调用获取角度函数
yaw = ctypes.c_float()
pitch = ctypes.c_float()
roll = ctypes.c_float()


lock = threading.Lock() # 线程锁,用于并发安全
stop_event = threading.Event()

def yaw_read_thread():
global stop_event
# 读取角度
try:
while True:
with lock:
lib.get_yaw_pitch_roll(ctypes.byref(yaw), ctypes.byref(pitch), ctypes.byref(roll))
finally:
stop_event.set()
# print(f"Yaw: {yaw.value}, Pitch: {pitch.value}, Roll: {roll.value}")

# 启动yaw read
threading.Thread(target=yaw_read_thread, daemon=True).start()

蓝牙设备配置 HCI

启动蓝牙设备

检查RFkill列表,蓝牙hci设备是否被Block

rfkill list
$     rfkill list
0: bt_default: Bluetooth
Soft blocked: no
Hard blocked: no
1: phy0: Wireless LAN
Soft blocked: no
Hard blocked: no
2: brcmfmac-wifi: Wireless LAN
Soft blocked: no
Hard blocked: no
3: hci0: Bluetooth
Soft blocked: yes
Hard blocked: no

若blocked yes则

rfkill unblock 3

若需要恢复关闭状态节约电源,使用block命令即可

蓝牙设备属性配置

sudo hciconfig hci0 piscan
sudo hciconfig hci0 iscan

若有需要可以进入bluetoothctl管理,使用discoverable onpairable on来开启蓝牙

蓝牙服务端配置

以L2CAP协议传输数据为例

import bluetooth

def l2cap_server():
server_sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)
psm = 0x1001 # L2CAP PSM(Protocol/Service Multiplexer)
server_sock.bind(("", psm))
server_sock.listen(1)

print(f"Waiting for connection on L2CAP PSM {psm}")

client_sock, client_info = server_sock.accept()
print(f"Accepted connection from {client_info}")

try:
while True:
data = client_sock.recv(1024)
if not data:
break
print(f"Received: {data.decode('utf-8')}")
except IOError:
pass
finally:
client_sock.close()
server_sock.close()

if __name__ == "__main__":
l2cap_server()

客户端在配对后连接

import bluetooth

def l2cap_client():
server_address = "XX:XX:XX:XX:XX:XX" # 替换为服务器的蓝牙地址
psm = 0x1001 # L2CAP PSM(Protocol/Service Multiplexer)

sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)
sock.connect((server_address, psm))

try:
while True:
data = input("Enter message to send: ")
sock.send(data.encode('utf-8'))
except KeyboardInterrupt:
pass
finally:
sock.close()

if __name__ == "__main__":
l2cap_client()

即可正常传输数据

Rknn 模型运行

前提:有转换好Onnx模型

安装Rknn-toolkit2

git clone https://github.com/airockchip/rknn-toolkit2.git --depth 1

应该有如下文件夹,RockChip合并了之前分散的项目,并且在此代码库更新新内容,旧仓库不再更新

├── rknn-toolkit2
├── rknn-toolkit-lite2
└── rknpu2

先要在个人主机(非板端),根据rknn-toolkit2/packages中的版本号来选择python版本,并且安装对应版本的rknn-toolkit2

注意,随着rknn-toolkit2的更新,版本铁定会不同,看好版本号

例如主机Ubuntu22.04.4(x86_64),packages文件夹内有

.
├── md5sum.txt
├── requirements_cp310-2.1.0.txt
├── requirements_cp311-2.1.0.txt
├── requirements_cp36-2.1.0.txt
├── requirements_cp37-2.1.0.txt
├── requirements_cp38-2.1.0.txt
├── requirements_cp39-2.1.0.txt
├── rknn_toolkit2-2.1.0+708089d1-cp310-cp310-linux_x86_64.whl
├── rknn_toolkit2-2.1.0+708089d1-cp311-cp311-linux_x86_64.whl
├── rknn_toolkit2-2.1.0+708089d1-cp36-cp36m-linux_x86_64.whl
├── rknn_toolkit2-2.1.0+708089d1-cp37-cp37m-linux_x86_64.whl
├── rknn_toolkit2-2.1.0+708089d1-cp38-cp38-linux_x86_64.whl
└── rknn_toolkit2-2.1.0+708089d1-cp39-cp39-linux_x86_64.whl

使用conda创建环境

conda create -n rknn python=3.10

激活环境

conda activate rknn

安装依赖和本体

pip install -r requirements_cp310-2.1.0.txt
pip install rknn_toolkit2-2.1.0+708089d1-cp310-cp310-linux_x86_64.whl

此时已安装完成rknn-toolkit2

转换模型

任意创建文件夹,例如

mkdir onnx2rknn
cd onnx2rknn

将转换代码写入到 onnx2rknn.py

from rknn.api import RKNN
import os

if __name__ == '__main__':
platform = 'rk3588'

'''step 1: create RKNN object'''
rknn = RKNN()

'''step 2: load the .onnx model'''
rknn.config(target_platform='rk3588')
print('--> Loading model')
ret = rknn.load_onnx('gmstereo-scale1-sceneflow-124a438f_1x3x480x640_sim.onnx') # 这里填写onnx模型的路径
if ret != 0:
print('load model failed')
exit(ret)
print('done')

'''step 3: building model'''
print('-->Building model')
ret = rknn.build(do_quantization=False)
if ret != 0:
print('build model failed')
exit()
print('done')

'''step 4: export and save the .rknn model'''
RKNN_MODEL_PATH = 'unimatch_stereo_scale1_1x3x480x640_sim.rknn' # 这里填写rknn模型的名称
ret = rknn.export_rknn(RKNN_MODEL_PATH)
if ret != 0:
print('Export rknn model failed.')
exit(ret)
print('done')

'''step 5: release the model'''
rknn.release()

更改需要输入的onnx模型和输出rknn模型位置

板端rknn-toolkit-lite2安装

与主机端同理,到packages文件夹下,创建conda环境,安装指定包

.
├── rknn_toolkit_lite2-2.1.0-cp310-cp310-linux_aarch64.whl
├── rknn_toolkit_lite2-2.1.0-cp311-cp311-linux_aarch64.whl
├── rknn_toolkit_lite2-2.1.0-cp312-cp312-linux_aarch64.whl
├── rknn_toolkit_lite2-2.1.0-cp37-cp37m-linux_aarch64.whl
├── rknn_toolkit_lite2-2.1.0-cp38-cp38-linux_aarch64.whl
└── rknn_toolkit_lite2-2.1.0-cp39-cp39-linux_aarch64.whl

创建

conda create -n rknn python=3.10
conda activate rknn

安装rknn-toolkit-lite2和opencv

pip install rknn_toolkit_lite2-2.1.0-cp310-cp310-linux_aarch64.whl
pip install opencv-python

测试运行

cd ../examples/resnet18
python test.py

可能会报错

W rknn-toolkit-lite2 version: 2.1.0
--> Load RKNN model
done
--> Init runtime environment
I RKNN: [15:54:49.666] RKNN Runtime Information: librknnrt version: 1.4.0 (a10f100eb@2022-09-09T09:07:14)
I RKNN: [15:54:49.666] RKNN Driver Information: version: 0.9.3
E RKNN: [15:54:49.666] 6, 1
E RKNN: [15:54:49.666] Invalid RKNN model version 6
E RKNN: [15:54:49.666] rknn_init, load model failed!
E Catch exception when init runtime!
E Traceback (most recent call last):
File "/home/orangepi/miniconda3/envs/rknn/lib/python3.10/site-packages/rknnlite/api/rknn_lite.py", line 157, in init_runtime
self.rknn_runtime.build_graph(self.rknn_data, self.load_model_in_npu)
File "rknnlite/api/rknn_runtime.py", line 921, in rknnlite.api.rknn_runtime.RKNNRuntime.build_graph
Exception: RKNN init failed. error code: RKNN_ERR_FAIL

Init runtime environment failed

若出现同样的错误,那么应该Rknpu2的lib等在板端是古董级别的,需要更新下

首先到方才克隆的根目录中rknpu2/runtime文件夹下,复制需要的文件

sudo cp Linux/librknn_api/aarch64/librknnrt.so /usr/lib/librknnrt.so
sudo Linux/rknn_server/aarch64/usr/bin/rknn_server /usr/bin/rknn_server

另一个librknn_api.so不需要更新

然后应该能正常跑test.py了

W rknn-toolkit-lite2 version: 2.1.0
--> Load RKNN model
done
--> Init runtime environment
I RKNN: [16:06:56.773] RKNN Runtime Information, librknnrt version: 2.1.0 (967d001cc8@2024-08-07T19:28:19)
I RKNN: [16:06:56.773] RKNN Driver Information, version: 0.9.3
I RKNN: [16:06:56.773] RKNN Model Information, version: 6, toolkit version: 2.1.0+708089d1(compiler version: 2.1.0 (967d001cc8@2024-08-07T11:32:45)), target: RKNPU v2, target platform: rk3588, framework name: PyTorch, framework layout: NCHW, model inference type: static_shape
W RKNN: [16:06:56.787] query RKNN_QUERY_INPUT_DYNAMIC_RANGE error, rknn model is static shape type, please export rknn with dynamic_shapes
W Query dynamic range failed. Ret code: RKNN_ERR_MODEL_INVALID. (If it is a static shape RKNN model, please ignore the above warning message.)
done
--> Running model
resnet18
-----TOP 5-----
[812] score:0.999680 class:"space shuttle"
[404] score:0.000249 class:"airliner"
[657] score:0.000013 class:"missile"
[466] score:0.000009 class:"bullet train, bullet"
[895] score:0.000008 class:"warplane, military plane"

done

板端运行自定义rknn模型

示例代码如下

import time
import cv2
import numpy as np
import platform
from rknnlite.api import RKNNLite

# decice tree for rk356x/rk3588
DEVICE_COMPATIBLE_NODE = '/proc/device-tree/compatible'

INPUT_SIZE = 224

RK3588_RKNN_MODEL = 'unimatch_stereo_scale1_1x3x480x640_sim.rknn' # 这里修改为前面转换得到的rknn

IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float16)
IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float16)

left_image = './im0.png'
right_image = './im1.png'

output_path = 'output.png'

if __name__ == '__main__':

input_height = 480
input_width = 640

print(f"input_height={input_height}")
print(f"input_width={input_width}")

left = cv2.resize(cv2.cvtColor(cv2.imread(left_image), cv2.COLOR_BGR2RGB), (input_width, input_height)).astype(
np.float32) / 255.0
right = cv2.resize(cv2.cvtColor(cv2.imread(right_image), cv2.COLOR_BGR2RGB), (input_width, input_height)).astype(
np.float32) / 255.0

left = (left - IMAGENET_MEAN) / IMAGENET_STD
right = (right - IMAGENET_MEAN) / IMAGENET_STD

left = np.transpose(left, (2, 0, 1))[np.newaxis, :, :, :]
right = np.transpose(right, (2, 0, 1))[np.newaxis, :, :, :]

# RKNN Init
rknn_model = RK3588_RKNN_MODEL
rknn_lite = RKNNLite()

# load RKNN model
print('--> Load RKNN model')
ret = rknn_lite.load_rknn(rknn_model)
if ret != 0:
print('Load RKNN model failed')
exit(ret)
print('done')

# init runtime environment
print('--> Init runtime environment')
ret = rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0)

if ret != 0:
print('Init runtime environment failed')
exit(ret)
print('done')

# Inference
print('--> Running model')
output = ''

t = time.time()
output = rknn_lite.inference(inputs=[left, right])
dt = time.time() - t
print(f"\033[34mElapsed: {dt:.3f} sec, {1 / dt:.3f} FPS\033[0m")

disp = output[0][0]

norm = ((disp - disp.min()) / (disp.max() - disp.min()) * 255).astype(np.uint8)
colored = cv2.applyColorMap(norm, cv2.COLORMAP_PLASMA)
cv2.imwrite(output_path, colored)
print(f"\033[32moutput: {output_path}\033[0m")

print('done')

rknn_lite.release()

ps. 转换似乎有不少问题,推理不出来正确结果,不过流程确实是这样,或许是onnx模型用到了特殊的算子,转换有失误

私钥登录 Linux

在服务器端生成公钥与私钥

ssh-keygen -a 1000 -t ed25519

信任公钥

cat id_rsa.pub >> authorized_keys

本地登录

拉取id_rsa文件到本地(没有.pub),使用openssh登录

ssh -i C:\Users\Lenovo\.ssh\id_rsa [username]@[hostname] -p 22
0%