ExIfDev/Prineo_RE

GitHub: ExIfDev/Prineo_RE

逆向工程 PriPara 街机游戏引擎,解析自定义加密和模型格式,实现跨平台角色移植。

Stars: 0 | Forks: 0

## 关于 PriPara(内部代号 Prineo)是由 SynSophia 开发的街机节奏游戏。 ## 目标 本项目的目标是逆向街机系列的模型格式,以便将独占角色移植到 Nintendo Switch 上的新版游戏中。 ## 截图 在逆向工程过程中截取的一些有趣截图。 ![Early Noesis addon import](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/8c4c825a97062738.png) *Noesis 插件的早期版本 —— 首次部分模型导入 (14-09-2025)* ![C# model reader](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/c9885956eb062740.png) *用于测试反序列化实现的 C# 模型读取器 (06-12-2025)* ![Hooking hashing function](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/eec166f800062743.png) *在虚拟机中运行解密后的转储文件,并通过 MinHook 和 Xenos 利用 DLL 注入 Hook 哈希函数,使用 ReturnAddress 内置函数追踪哈希函数调用 (08-12-2025)* ![Imported scene](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/8767fc5d36062746.png) *通过 Noesis 插件从游戏中导入的场景 (27-12-2025)* ![Imported model](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/300cb03d65062748.png) *通过 Noesis 插件从加密资源导入的蒙皮模型* ![Ported model](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/657cec1467062751.png) ![Ported model2](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/50e7e24ee7062755.png) *从 PriPara 街机版移植到 Nintendo Switch 版的服装,模型使用 Noesis 插件导出* ## 自定义加密实现 游戏实现了标准加密原语的修改版本。 ### FNV-1 - 操作顺序颠倒 - 缺少初始偏移基 **实现:** https://github.com/ExIfDev/Prineo_RE/blob/main/Noesis/fmt_prineo.py#L1130-L1140 ### Mersenne Twister - 使用减法代替加法 - 自定义乘数 *0x13F8769B* - 掩码至 31 位 - 使用自定义常数的变异 twist 操作 - 修改了掩码常数 **实现:** https://github.com/ExIfDev/Prineo_RE/blob/main/Noesis/fmt_prineo.py#L1074-L1128 ## 文件格式研究 - 游戏:PriPara (Prineo) - 坐标系手性:右手 - 字节序:LE - 三角形绕序:CW - 类型:稀疏块 - 最大多边形数:65535 - 最大骨骼数:255 以下文档指的是 SSZL 解压后的文件。 ## .BIN (通用容器) ``` const byte[] ident = {0x00, 0x13, 0x10, 0x09}; struct Header { uint32 VERSION;//1 uint32 FILE_SIZE; byte[4] Ident; int32 unk1; int32 unk2; int32 unk3; uint32 DATA_OFFSET; //absolute offset uint32 DATA_SIZE; }; ``` ## .MDJ ``` //the MDJ file can contain a variety of buffers related to geometry and scenes, it does //not have a header, its assumed to start with one of the following buffers //and always terminate with the "end/0/" marker struct Pose //pose applied to a skeleton (=restPose in a model container) //usually this pose is the same as in the "base" buffer. //if AnimParams are present then what follows is the animation curves. //AnimParams can be null when there are no keyframes. { char[4] ident = "pose"; uint16 strLen; char[strLen] POSE_NAME; byte[2] unk; uint16 BONE_COUNT; byte[8] unkdata; struct Bone[BONE_COUNT] { uint16 serializer_id;//518 uint16 strLen; string[strLen] BONE_NAME; //Base transform float sclX, sclY, sclZ; float rotX, rotY, rotZ; float tslX, tslY, tslZ; //for all channels (9) "Sx", "Sy", "Sz", "Rx", "Ry", "Rz", "Tx", "Ty", "Tz" struct AnimationParams //6 bytes { uint16 unk2; //possibly an uint32 since unk3 is always 0 uint16 unk3; uint16 CHANNEL_MASK; }; //if anim params are not null then.... uint32 KEYFRAME_COUNT; struct Keyframe[KEYFRAME_COUNT] { float VALUE;//radians float TIME;//centiseconds }: }; }; struct TextureDefs //define texture name and id bindings { string[4] ident ="imag"; uint16 TEX_COUNT; struct TexDef[TEX_COUNT] { uint16 serializer_id;//1029 uint16 unk; uint16 TEX_ID; uint16 strLen; string[strLen] texName; }; }; struct MaterialChunk //defines parameters of materials and shaders { string[4] ident ="mate"; uint16 MAT_COUNT; struct Material[MAT_COUNT] { uint16 serializer_id;//1024 uint16 strLen; string[strLen] MatName; uint32 TYPE;//..maybe hash? looked up in a registry //Texture slots //ambient uint16 aCount; for (int i = 0; i < aCount; i++) { uint16 serializer_id; if (serializer_id == 1029) { uint32 unk; } else { uint32 unk2; } } //diffuse uint16 dCount; for (int i = 0; i < dCount; i++) { uint16 serializer_id; if (serializer_id == 1029) { uint32 unk; } else { uint32 unk2; } } //specular uint16 sCount; for (int i = 0; i < sCount; i++) { uint16 serializer_id; if (serializer_id == 1029) { uint32 unk; } else { uint32 unk2; } } //possibly parameter slots uint16 unk1Count; for (int i = 0; i < unk1Count; i++) { uint16 serializer_id; float unk12; } uint16 unk2Count; for (int i = 0; i < unk2Count; i++) { uint16 serializer_id; float unk22; } byte HAS_TEXTURE; if (HAS_TEXTURE == 1) { uint16 TEX_COUNT; struct TexureMap[TEX_COUNT] { uint16 serializer_id; uint32 TEX_ID; } } uint16 strLen; string[strLen] unkNullString; }; //two possible chunks could follow: struct MPSS_Subchunk //clear usage unknown { string header = "MPSS"; uint16 count; struct MPSS_Entry[count] { uint16 unk32; byte[16] payload; } } struct MPDS_Subchunk //string to value shader parameter chunk { string header = "MPDS"; uint16 count; struct MPDS_Entry[count] { uint16 strLen; string[strLen] Name; byte[32] value; } } }; struct LightChunk //defines parameters of lights in a scene { char[4] ident = "ligh"; int16 LIGHT_COUNT; struct light[LIGHT_COUNT] { uint16 serializer_id; uint16 strLen; char[strLen] light_name; byte[49] unk40; //transform matrix along with color vals } } struct ModeBuffer//marks the start of a submesh list and its buffers { char[4] ident = "mode"; uint16 serializer_id;//513 uint16 strLen; char[strLen] unkName; //possibly the root node byte[4] unk; uint16 unkC; }; struct MeshMeta//a submesh starts with some metadata about its bounding size,name, and its boneMap { uint16 serializer_id;//770 0x0302 uint16 strLen; char[strLen] MESH_NAME; byte[4] unk; uint16 boneMapCount; struct HashedBoneMap[boneMapCount] { uint32 boneHash;//SSFNV1a hashed bone name } float unk3; byte unk2; byte[24] AABB; } struct MeshBegin //indicates the beginning of a submesh { string[4] ident ="verb"; }; struct FaceBuffer { string[4] ident = "surf"; int16 FACE_COUNT; struct Face [FACE_COUNT]//stride: 6 //type: triangle list // winding:clockwise { uint16 A; uint16 B; uint16 C; }; }; struct VertexBuffer { string[4] ident = "coor"; uint16 VERTEX_COUNT; struct Vertex [VERTEX_COUNT] //stride 12 { float x; float y; float z; }; }; struct NormalBuffer { string[4] ident ="norm"; uint16 NORM_COUNT; //should be the same as VERTEX_COUNT struct Normal[NORM_COUNT]//stride 12 { float nx; float ny; float nz; }; }; struct BinormalBuffer//binormal buffer (not present in any file) { string[4] ident ="bino"; uint16 BINO_COUNT; //should be the same as VERTEX_COUNT struct BiNormal[NORM_COUNT]//stride 12 { float bx; float by; float bz; }; }; struct UV0Buffer { string[4] ident ="tex0"; uint16 UV0_COUNT; //should be the same as VERTEX_COUNT struct UV[UV0_COUNT] //stride 8 { float u; float v; }; }; struct UV1Buffer { string[4] ident ="tex1"; uint16 UV1_COUNT; //should be the same as VERTEX_COUNT struct UV[UV1_COUNT] //stride 8 { float u; float v; }; }; struct VertexColors { string[4] ident = "colo"; uint16 VERTEX_COUNT; //should be the same as VERTEX_COUNT struct UV[UV1_COUNT] //stride 4 { byte r,g,b,a; }; } struct TangentBuffer //signless tangent vector { char[4] ident ="tan "; uint16 TANG_COUNT; //should be the same as VERTEX_COUNT struct Tang[TANG_COUNT]//stride 12 { float tx; float ty; float tz; }; }; struct WeightBuffer //Weight values for each bone assigned to a vertex { char[4] ident ="weig"; uint16 WEIGHT_COUNT; struct VertexWeight[WEIGHT_COUNT] //stride 16 { float w1; float w2; float w3; float w4; }; }; struct VBIBuffer //Vertex Bone Index, in order, bone indices that influence a vertex //this table is local to the current vertex buffer { char[4] ident ="bone"; uint16 VERTEX_COUNT; struct VBI[VERTEX_COUNT] { byte boneIDX1; byte boneIDX2; byte boneIDX3; byte boneIDX4; }; }; // struct Skeleton //Bone Node Tree, defines the bone relationships { char[4] ident ="skel"; struct BoneName[BONE_COUNT] //Bone count read from POSE chunk { uint16 serializer_id; //257=root 258=child uint16 strLen; char[strLen] BONE_NAME; uint32 unk2; uint16 CHILD_COUNT; }; }; struct RestPose //defines the rest pose of the skeleton { char[4] ident ="base"; uint16 strLen; char[strLen] POSE_NAME; int16 BONE_COUNT; struct BoneTrs[BONE_COUNT] { uint16 serializer_id;//1537 uint16 strLen; char[strLen] BONE_NAME; float sx, sy, sz; //scale float rx, ry, rz; //rotation float tx, ty, tz; //translation }; }; struct MeshEnd //indicates the end of a mesh { string[4] ident ="vere"; uint16 strLen; char[strLen] NAME; byte[2] padding; }; struct EndFlag //indicates the end of the file { string[4] ident ="end " } ``` ## _tex.BIN (纹理容器) ``` //information is related to the decompressed file struct Header { uint32 VERSION; uint32 FILE_SIZE; uint32 tex_ident; uint32 unk; uint32 TEXTURE_COUNT; }; struct TextureDecl[TEXTURE_COUNT] { uint32 HASH;//hashed texture name minus the extension uint32 OFFSET;//absolute offset uint32 SIZE; }; //follows texture buffers at OFFSET with SIZE //TGA or DDS ``` ## 致谢 - @REDxEYE 协助逆向材质缓冲区
标签:3D建模, DLL注入, FNV-1, Hook技术, Mersenne Twister, Nintendo Switch, Noesis, PriPara, Python, SynSophia, 云资产清单, 哈希算法, 密码学, 手动系统调用, 数据序列化, 文件解析, 无后门, 模型格式, 流量审计, 游戏安全, 游戏移植, 游戏逆向, 生成式AI安全, 虚拟机, 街机游戏, 计算机图形学, 资源解密, 逆向工具, 逆向工程