首页
社区
课程
招聘
[原创] 分享一次还原某网游 3D骨骼动画文件为collada过程 --- 长篇
发表于: 2017-9-22 00:28 18627

[原创] 分享一次还原某网游 3D骨骼动画文件为collada过程 --- 长篇

2017-9-22 00:28
18627

写在前面的话: 因为一些兴趣近期对一款网游的资源文件进行的分析提取,历经10天挑灯夜战, 最终成功解开了PKG压缩包, 还原了 骨骼3D文件, 动作3D文件, 重要的是借由逆向,间接学到了不少初级3D数学知识,过程思路为主,代码为辅,将在本文中一一介绍。 (解开了也没钱没用途 写个帖子以防自己忘记)

 

-

文中用到的主要工具: C32asm,Ollydbg,x64_dbg,WinDbg,IDA Pro,PCHunter,3DMAX

导读目录:

资源文件

一一

1.1 静态分析

游戏文件夹内有许多.pkg为后缀的文件,且文件大小不俗,一般都是存放着压缩过的游戏图片,音乐,建模等数据,过去热门的游戏有各种提取/解压工具, 甚至有的游戏可以用winrar直接打开, 但很不幸,此次的目标两者皆否。

-

打开C32Asm,16进制选取游戏文件夹内的某pkg文件后可以看到。

一眼能认出的就是 文件名字还有 下面的 被压缩过的数据, 由经验判断, 78 01 极可能是 zip算法的标头, 于是从420偏移处开始选择,一直到结尾空白区域把压缩数据复制出来保存。

选择了 6694 字节进行解压, 解压成功,的确是zip算法, 但是,文本文件各种乱码, 图片文件各种花屏, 却又能看到部分明文, 数据的确是正确的,是什么原因呢?  经过对比以及几次失败的尝试后发现。 我选取的是 6694 个字节, 但回到图1,压缩数据前的那4个字节数值却是 F6190000 反转(变量储存在内存的反转关系不用多说了吧) 后为 000019F6 = 6646,  这多出来的48字节怎么回事呢? 填充字节? 偷字节? 还是其中的某些字节有过处理? 如此的话静态分析就不会有结果了, 到了这里不得不动态调试游戏一探究竟。

一二

动态分析

由于是TP保护的网游,使用OD附加前需要搞定几处保护,简单讲 path nop掉gamebase.dll的沙盘创建,提前占用R0调试端口,然后运行网游后,使用PCHunter 恢复几处R3HOOK ,调试器能附加后,使用WinDbg附加找到主动抛异常的几个线程记下, 并重新运行游戏继续使用PCHunter搞定几个异常线程,就可以用OD附加了。

通过PCHunter可以发现,游戏运行后打开了所有的PKG文件句柄, 所以我们要对ReadFile以及SetFilePointer下断,返回用户代码进行多次来回运行后, 观察定位了几个关键函数, (由于时间过去10多天就不给出详细的跟踪过程了)

首先,ReadFile 读取都是1016字节一次,读一次后,SetFilePointer就会把偏移+8 然后再读下一段1016字节, 也就是说,每1024个字节会舍弃掉最后8字节,  这样就解释得通了, 为什么前面手动选取到6694个字节, 但数据前的标头却写的6646字节。  6646/1016*8取整=6 6*8刚好就是多出来的字节了。 多出来的这些就是不需要的,读取的时候跳过。测试后能成功解压出正常的数据,这个点算是解决了。

第二个点,文件名对应数据的偏移怎么拿到, 在游戏里找好第一次需要读取PKG包的点,对SetFilePointer下断后,第一次的返回就是临近获得偏移的地方,后发现偏移量每次都是由一个数*0x400后得到的, 而这个数,在文件名的前面28个字节处,代表这个文件名的数据从第几个0x400偏移开始.. 
//定义此结构用于读取包内文件信息
  PkgFileInfo =record
     FileDataGroup:Cardinal;  //数据偏移位于第几组 FileDataGroup*0x400=偏移
     unknow:Cardinal;
     unknow2:Cardinal;
     unknow3:Cardinal;
     unknow4:Cardinal;
     unknow5:Cardinal;
     UnCompfileSize:Cardinal;    //解压后的大小
     FileName:array[0..255] of Char; //文件名
  end;
知道文件名列表的规则,知道了数据读取规则,剩下的就是解压缩了。

写到这里才发现写得敷衍了些, 因为过去时间太久加上本文的重点在后面,解压这里就简单带过吧。搞定PKG文件花了半天时间, 而接下来的9天都是与3D信息文件的战斗...

骨骼文件

二一

可能性分析

解开了pkg包内的数据后,有图片,有脚本,有字符串表,都是明文可以直接打开的, 唯独3D数据,skel,anim,col等等 骨骼,蒙皮,动作等文件有过处理,尝试用各种3D软件都无法读取, C32asm打开也看不出什么名堂, 因为我从没接触过3D方面, 不管是游戏还是建模等等方面, 所以一时语塞了。这时候拖关系得到一个重要的信息,这款游戏的3D文件都是通过一个编码器从Collada数据格式转换过来的, 而这个编码器!你猜怎么着,它居然就在我们公司楼下的美术的电脑上, 今晚就潜伏进去偷过来(开玩笑)。
拿到了这个编码器,就有了前进的方向,目前需要进行测试收集信息,那么就离不开所谓的Collada数据格式。

二二

浅识collada DAE文件

通过搜索得知,collada是一种数据规范,意为让不同的3D软件都能读同一个格式取到相同的信息,各工具间导出导入方便交换3D数据。 一般这样的文件在3DMAX里导出为*.dae文件.

参考资料:
百度百科COLLADA介绍
collada快速入门
DAE模型与骨骼动画解析渲染

简而言之,目前需要逆向编码器对dae文件的编码过程,从而通过游戏文件逆向还原成dae文件就对了。打开3DMAX, 创建3个简单的骨骼,设置自动关键帧,选择其中一个节点做平移动作和旋转动作,并导出为dae格式,导出时不需要三角算法和单一矩阵。

DAE文件为XML格式,打开后看到

观察得知<library_animations>节点下存储了动画信息,<library_visual_scenes>节点下存储了骨骼信息,目前只关注这两个节点就可以了。由于骨骼文件的数据较少,决定先从简单的开始分析,多次观察统计后不难发现, node 节点包含一块骨骼(点)的信息, 如果有有子骨骼的话,就会在node节点下再包含一个node节点,刚才创建的3个连着的骨骼,其实上就是父子关系, 不过现阶段骨骼里面的各种数值代表什么意思完全不清楚。

二三

逆向编码器-骨骼

对编码器动手之前,还是需要静态分析一下的,尝试用编码器编码刚才导出的Test.Dae文件, 编码器是一个控制台程序,直接运行就退出了,推测是通过参数调用, 载入OD,设置命名行参数为"1111111111111",并单步运行, 直到结束都没在堆栈或寄存器等看到类似11111111的字符,连GetCommandLine都得不到执行, 转换思路搜索字符串来到可疑的地方。
013E99E2  |.  68 A4684701   push converte.014768A4                   ;  collada\
013E99E7  |.  C785 D4FEFFFF>mov [local.75],0xF
013E99F1  |.  C785 D0FEFFFF>mov [local.76],0x0
013E99FB  |.  C685 C0FEFFFF>mov byte ptr ss:[ebp-0x140],0x0
013E9A02  |.  E8 49C0FFFF   call converte.013E5A50
013E9A07  |.  8D85 68FDFFFF lea eax,[local.166]
013E9A0D  |.  50            push eax                                 ; /pFindFileData = 0014FA18
013E9A0E  |.  68 B0684701   push converte.014768B0                   ; |collada\*.dae
013E9A13  |.  C645 FC 02    mov byte ptr ss:[ebp-0x4],0x2            ; |
013E9A17  |.  FF15 10604701 call dword ptr ds:[<&KERNEL32.FindFirstF>; \FindFirstFileA
013E9A1D  |.  8BF8          mov edi,eax
013E9A1F  |.  83FF FF       cmp edi,-0x1
013E9A22  |.  75 2D         jnz short converte.013E9A51
013E9A24  |.  E8 67B10200   call converte.01414B90
013E9A29  |.  8B10          mov edx,dword ptr ds:[eax]
013E9A2B  |.  68 C0684701   push converte.014768C0                   ;  No Collada files found
013E9A30  |.  8BC8          mov ecx,eax
013E9A32  |.  FF52 04       call dword ptr ds:[edx+0x4]
013E9A35  |.  68 E4684701   push converte.014768E4                   ;  \n
013E9A3A  |.  E8 075F0600   call converte.0144F946
013E9A3F  |.  68 C0684701   push converte.014768C0                   ;  No Collada files found
013E9A44  |.  E8 FD5E0600   call converte.0144F946
013E9A49  |.  83C4 08       add esp,0x8
013E9A4C  |.  E9 A9010000   jmp converte.013E9BFA
013E9A51  |>  6A 00         push 0x0                                 ; /pSecurity = NULL
013E9A53  |.  68 D8684701   push converte.014768D8                   ; |Converted
013E9A58  |.  FF15 14604701 call dword ptr ds:[<&KERNEL32.CreateDire>; \CreateDirectoryA
原来编码器会判断有无collada文件夹,有的话才对里面的*.dae文件进行处理, 配置好后运行, 得到 Test.skel 和Test.anim 两个文件, 因为刚才的DAE文件里只有骨骼和动画,刚好就得到这两个文件。
用C32Asm打开Test.skel 可以看到。

通过多次生成不同的骨骼文件静态对比后 可以看到 骨骼名字结束后, 空了4个字节,然后是64个字节的骨骼信息, 然后4个字节表示下方有几个子节点,然后才是下一个骨骼的数据,现在要做的就是理解这64个字节是怎么转换得来的了,由经验得知, 3F800000是 浮点数1.000000 的十六进制数据, Skel文件内多次看到, 那么这64个字节其实是16个浮点数?  回去看一下dae文件,是不是都是浮点数?   难道!!! 所谓的编码器,就是简单的把DAE文件中的浮点数转换成16进制??????


然而!!!  并不是... skel中的浮点数和dae中的肉眼看没有任何关系。进行到这里时,本来让我继续我是拒绝的.又没人给钱.

过了一天,灵光一闪,3DMAX在导出DAE文件时,collada选项那里,有一个三角算法和一个单一矩阵可供选择,于是,导出单一矩阵后,可以看到Testjuzheng.dae内,bone001骨骼信息node节点那里有了的变化
<node name="Bone001" id="Bone001" sid="Bone001" type="JOINT">
<matrix sid="matrix">
0.679118 -0.734029 -0.000000 9.930389 0.000000 0.000000 1.000000 0.000000 -0.734029 -0.679118 0.000000 -8.927395 0.000000 0.000000 0.000000 1.000000
</matrix>
在看编码后的Testjuzheng.skel

这 16 个浮点数!!! 刚好对应到了 Skel文件的 Bone001下的16个浮点数, 虽然16个数排列顺序不一样, 但这是一个重大突破。 像饿了好几天的求生者发现食物一样,我立马去验证Bone002的数据………………  很遗憾, 对不上...... 这感觉就像女神和你的告白下一秒说是愚人节一样, 但无风不起浪, 第一个数据完全相同这一点足以给我动力继续下去了。到了这一步, 不得不开始动态调试, 分析Bone002的浮点数据到了骨骼文件怎么就变了。

用OD载入编码器,搜索字符串"ibrary_visual_scenes",来到调用函数,下方紧接着就是 "node" 的字符串,从node 下方进入处理过程后,单步跟踪,定位到了骨骼节点小数字符串转换成 16 进制的过程,下面不远就是 这64个字节进行转换的位置了, 由于太简单就不上图了, 直接上关键的算法。
由于OD不支持查看XMM寄存器的数值,这里改用x64_dbg调试

这个函数,就是Bone002摇身一变成为Skel文件里数据的关键,函数一共三个参数,一个为上一个节点(Bone001)的 16个浮点数, 一个为本次节点(Bone002 dae中)的16个浮点数, 还有一个为输出地址, 那么搞定这个算法骨骼就可以通过Skel文件还原成DAE文件中的数值了, 通过跟踪理解, 尝试做了这样一张图

IDA Pro中的伪代码为:

讲真,IDA的伪代码我并看不懂。口头描述代码如下
float a[16]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}; //DAE中的排序
float b[16]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};

a={1,5,9,13,2,6,10,14,3,7,11,15,4,8,12,16};
b={1,5,9,13,2,6,10,14,3,7,11,15,4,8,12,16};  //转换成编码器内存中的排序 
a和b 进入函数过程的话.a为上一个节点信息, b为本次
用第二次循环举例
将 本次节点(b)信息的 第二组 2 6 10 14 传给寄存器XMM0 并进行洗牌操作后,各寄存器状态如下
XMM0  2  2  2  2
XMM1  6  6  6  6
XMM2 10 10 10 10
XMM3 14 14 14 14

xmm4-xmm7的数据为:
XMM4 13  9  5  1    //这里是 上个节点a的所有数据 
XMM5 14 10  6  2
XMM6 15 11  7  3
XMM7 16 12  8  4

然后和 XMM0-XMM3 和 XMM4-XMM7 分别相乘后数据如下:

XMM0  26  18  10  2
XMM1  84  60  36  12
XMM2 150 110  70  30
XMM3 224 168 112  56

ADD  484 356 228  100 (累加操作)

这其中的一次循环 484 356 228 100 就是保存到skel中的数据了。

二四

还原思路

那么现在, 如何利用已知的484 356 228 100(Skel的信息中的一组)和 1..16(a 第一个节点的信息) 来求得未知的 1..16(b)?,通过已知的条件无法知道累加操作前个寄存器中的数据,第一步就卡住了。 看样子通过上面的汇编代码逆出算法并不容易。 

回归初中数学,列方程!! 
例4个未知数 x y z v
现有条件 
x*1+y*2+z*3+v*4=100
x*5+y*6+z*7+v*8=228
x*9+y*10+z*11+v*12=356
x*13+y*14+y*15+v*16=484
这样就列出一个4元一次方程组了, 但是这个例子是无解的,实际应用中,那16个浮点数必有0, 代入就有解了, 以刚才生成的Testjuzheng.Skel举例, 把bone02和bone01的各数据代入数学工具,得出:



DAE文件中的数据 (注意Skel中的排列顺序与Dae不同)
bone001={0.679118 -0.734029 -0.000000 9.930389 0.000000 0.000000 1.000000 0.000000 -0.734029 -0.679118 0.000000 -8.927395 0.000000 0.000000 0.000000 1.000000}
bone002={0.586148 0.810204 0.000000 29.247797 -0.810204 0.586148 0.000000 0.000001 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 1.000000}

Skel文件中Bone02的数据
Sbone002={0.992777  0.000000  0.119974  0.000000 0.119974 0.000000 -0.992777  0.000000 0.000000  1.000000  0.000000  0.000000 29.793094  0.000000 -30.396126 1.000000}

现在取Sbone002的第一组{0.992777 0.00000 0.119974 0.000000}
列出方程:
x*0.679118+y*-0.734029+z*-0.000000+v*9.930389=0.992777
x*0.000000+y*0.000000+z*1.000000+v*0.000000=0.000000
x*-0.734029+y*-0.679118+z*0.000000+v*-8.927395=0.119974
x*0.000000+y*0.000000+y*0.000000+v*1.000000=0.000000
使用Math解得:

结果分别对应了Dae文件中bone002的1,5,9,13位, 结果正确, 这样就通过四元一次方程将Skel中的数据 还原成 DAE的 单一矩阵 中的信息了。当然这样的算法并不科学, 只是一时为达目的所选择的, 真正的解法将在后面介绍。还原游戏中一个骨骼文件后,导入3DMAX中可以看到正确的骨骼 一只鬼手

骨骼文件的还原至此算是告破

二五

真正的算法

其实这里的内容是我还原动画文件的时候不得已才接触到的,提前简单的写在这里, 其实这16个浮点数是一个变换矩阵,表达了一个点的X,Z,Y坐标信息以及X,Z,Y轴的旋转信息,骨骼文件的骨骼(点)定位时,都会与父点相乘(DAE文件中没有体现这一点), 当矩阵(bone001)相对于世界坐标的时候,与世界坐标点相乘结果不会改变,因为世界坐标的XYZ轴和XYZ平移坐标都是0(不确定),  当bone002定位时,就会与他的父点(bone001)相乘, 所用的是矩阵乘法, 也就是上面那个关键算法,不过编码器为了效率的使用浮点CPU指令,排列顺序才会和DAE中(正常的4x4数组)不一样, 那么 在逆求结果时, 应该使用矩阵除法,而不是使用四元一次方程,  关于乘除法后面动画还原时会介绍

动画文件

三一

逆向编码器-动画

成功摸清骨骼文件后,信心大增,却没想到动画文件才是陷入泥潭的开始.
由于前面利用单一矩阵的成功, 这次也想通过单一矩阵导出动画,结果编码器不认动画的单一矩阵,编码失败,头大,不得已3DMAX导出原始格式。并对编码器进行分析。 
搜索字符串"<library_animations>" 定位到编码函数, OD一看过程真J2长,各种跳转各种条件, 跑了几次后,大概猜出了流程,不过还是上IDA。

过程似乎进入动画节点后 似乎只对 平移X,Y,Z 和 旋转X,Y,Z感兴趣,往下看



基本确定编码器只对平移信息和旋转信息的 input 和 output 感兴趣。回到Test.DAE文件可以看到。

除了input和ouput外,还有interpolation,intan,outtan, 信息,好像是动画插值和旋转相关的东西, 为了确定这些是否不需要, 把关于这三点的信息全删了, 3DMAX仍然可以打开以及做动作, 编码器也能正常编码, 看来需要关注的是 intput 和 output


回头查看anim动画文件,可以看到,通过多次生成不同的文件对比可以得出结果如下

可以看到,关于Test.anim的信息相对于dae文件中来看
<float_array id="Bone001-rotateX.ANGLE-input-array" count="2">0.000000 1.800000</float_array>
count=2 ,表示有两组动作(关键帧),起点一组,1.800000 秒的时候为一组,不要问我怎么知道input里时间的,看anim文件里第二组的开头 44EA0000=1872, 这是毫秒表示这组动作的开始时间,约等于1.8 , 所以dae文件的input里所谓的输入就是时间, 后来通过搜索确认如此。


接下来分析编码器, 是怎么把平移X,Y,Z和旋转X,Y,Z中各取一个数据转换成64字节的。 通过OD跟踪分析, 发现X,Y,Z分开计算, 先乘以3.141592,再除以180.0000,然后进入一个关键算法,看到就头大。OD 代码就不贴了,上IDA,
int __cdecl sub_B8EAE0(int a1, unsigned int a2)
{
  __m128d v2; // xmm0@1
  int v3; // esi@1
  __m128 v4; // xmm0@1
  int result; // eax@1
  unsigned int v6; // [sp+8h] [bp+8h]@1

  v2 = _mm_cvtps_pd((__m128)a2);
  v3 = a1;
  *(_DWORD *)a1 = 1065353216;
  *(_DWORD *)(a1 + 4) = 0;
  *(_DWORD *)(a1 + 8) = 0;
  *(_DWORD *)(a1 + 12) = 0;
  *(_DWORD *)(a1 + 16) = 0;
  _libm_sse2_cos_precise(v2);
  *(float *)&v2.m128d_f64[0] = v2.m128d_f64[0];
  v6 = LODWORD(v2.m128d_f64[0]);
  *(_DWORD *)(v3 + 20) = LODWORD(v2.m128d_f64[0]);
  v4 = (__m128)_mm_cvtps_pd((__m128)a2);
  result = _libm_sse2_sin_precise();
  v4.m128_f32[0] = *(double *)&v4.m128_u64[0];
  *(_DWORD *)(v3 + 28) = 0;
  *(_DWORD *)(v3 + 24) = v4.m128_i32[0];
  *(_DWORD *)(v3 + 36) = (unsigned __int128)_mm_xor_ps(v4, (__m128)xmmword_C16DB0);
  *(_DWORD *)(v3 + 32) = 0;
  *(_QWORD *)(v3 + 40) = v6;
  *(_DWORD *)(v3 + 48) = 0;
  *(_DWORD *)(v3 + 52) = 0;
  *(_DWORD *)(v3 + 56) = 0;
  *(_DWORD *)(v3 + 60) = 1065353216;
  return result;
}
这时候IDA的好处体现出来了,看上去像圆相关的计算,这里先暂时放一放,OD继续跟踪后,发现旋转X,Y,Z的一个小小的数字信息计算后都输出成了64个字节的浮点数据,然后依次进入了逆向骨骼时那个关键函数,一共调用了5次,才得出最后输出到anim文件中的 64 字节, 5次,数据早就变幻莫测了,整个头爆炸, 4元一次方程用不上了。 似乎又陷入了绝境。

课后补充

(可跳过剩下内容直接看下一章节),如果要强行逆向也是可以的,旋转X,Y,Z在转换成所谓的64字节时,观察排列可得知X,Y,Z进行矩阵相乘时有关系可循, 比如X转换64字节后某一个位置固定是0或1,那后一个64字节与X相乘时,某一些位置结果不变, 这样可以固定得到一个原数据, 然后一步一步推算回去,不过脑子代价太大,我个人选择另寻他路,实在不行时再强行逆向算法。 在后期得到正在的算法后,可以知道这里的5次"关键函数"累加正是旋转X,Y,Z,的矩阵信息(3X3) 平移信息4字节, 缩放信息 4字节, 各5个信息组合起来, 而X,Y,Z转换计算可以在后面的算法代码看到。

举个例子,用OD定位转换代码后,分别观察XYZ转换后的数据如下

得到

然后,X与Y进入关键函数,组成FXY, 再和Z进入关键函数,结果就是存到动画文件里的3X3矩阵了。
还记得关键函数的相乘然后累加吗? 仔细看看 X Y Z中有数据的位置,是有固定数据的, 再看看FXY的结果是
FXY
00 E2 7E 3F 00 00 00 00 69 1F BF BD 00 00 00 00 
6B 02 DA 3A 97 F5 7F 3F 9E 5E 91 3C 00 00 00 00
A3 17 BF 3D BC 01 92 BC A3 D7 7E 3F 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 80 3F

由于X第一行是1,0,0,0 与Y相乘累加 1*Y第一行,0*Y第二行,0*Y第三行,0*Y第四行,累加后结果还是等于Y的第一行。

这样我们在手里只有FXY的数据的时候,可直接推出
X矩阵的第一行为:
00 00 80 3F 00 00 00 00 00 00 00 00 00 00 00 00
Y矩阵的第一行为:
00 E2 7E 3F 00 00 00 00 69 1F BF BD 00 00 00 00 


继续看在确定 X[2][1]=0,X[2][4]=0,y[2]={0,1,0,0},y[4]={0,0,0,1},y[3][2]=0,y[3][4]=0,x[3][1]=0,x[3][4]=0 已知Y[1]的情况下
X[2][1]*y[1]={0,0,0,0}
X[2][2]*Y[2]={0,X[2][2],0,0}
X[2][3]*Y[3]={?,0,?,0}
X[2][4]*Y[4]={0,0,0,0}
4个结果分别累加起来后就=FXY[2],
已知FXY[2]={6B 02 DA 3A,97 F5 7F 3F,9E 5E 91 3C,00 00 00 00}
这时候我们就知道
FXY[2][1]=X[2][3]*Y[3][1]
FXY[2][2]=X[2][2] 得到 X[2][2]=FXY[2][2];
FXY[2][3]=X[2][3]*Y[3][3]

继续往下看数据第三行,
x[3][1]*y[1]={0,0,0,0}
x[3][2]*y[2]={0,x[3][2],0,0}
x[3][3]*y[3]={?,0,?,0}
x[3][4]*y[4]={0,0,0,}
已知FXY[3]={A3 17 BF 3D,BC 01 92 BC,A3 D7 7E 3F,00 00 00 00}
可得到
FXY[3][1]=x[3][1] 得到  x[3][1]=FXY[3][1]
FXY[3][2]=x[3][2] 得到  x[3][2]=FXY[3][2]
FXY[3][3]=X[3][3]*Y[3][3]

将这个FXY[3][1]=x[3][1]作为条件代入上面的FXY[2][1]=X[2][3]*Y[3][1]

得到
FXY[2][1]=X[2][3]*FXY[3][1]  再得到 X[2][3]=FXY[2][1]/FXY[3][1]

这样, X 的第二行就凑齐了 
00 00 00 00,FXY[2][2],FXY[2][1]/FXY[3][1],00 00 00 00
等于
00 00 00 00 97 F5 7F 3F BC 01 92 3C 00 00 00 00 

.......
这样慢慢寻求他们的关系,也能在只有FXY的情况下还原出X和 Y,64字节的状态,同理也能还原出Z, 然后再去逆向圆相关计算的函数达到目的, 但我接下来选择正向思考 寻求简单的解法。

三二

64字节的真正意义

到了这一步,不得不转变思路,想从逆向算法还原数据似乎异常困难了。必须思考这64个字节,到底是什么, 回忆骨骼文件,不是单一矩阵导出的样子,对比后发现

translate: 平移坐标 X Y Z
rotateZ:    Z轴旋转 0 0 1 Z
rotateY:    Y轴旋转 0 1 0 Y
rotateX:    X轴旋转 1 0 0 X

和动画同样的 6个起效的数字, 3DMAX转换成单一矩阵的时候产生的结果和SKEL文件中的64个字节一样,那其实动画的6个数,在转换成anim文件中的 64字节时也是同样的算法? 那3DMAX能用现有的6个数转换成64个字节的16个浮点数,那必定存在一种现有算法来正向转换, 不需要逆向思考还原.. 搜索关键字 "矩阵转换",终于发现了这6个数字其中3个的真身-欧拉角 

欧拉角Vector3(x,y,z)代表的是旋转物体, 
而一直提到的64个字节,就是4X4的变换矩阵,其中包含了欧拉角转换成3X3的旋转矩阵以及平移和缩放信息。

参考资料:④:【3D计算机图形学】变换矩阵、欧拉角、四元数

三三

矩阵转换欧拉角及乘除法

得知了欧拉角这一重要信息后,开始寻找矩阵转换欧拉角的算法。 抄来一段
//转自:https://www.mianbaoban.cn/blog/post/66293
/*  输入欧拉角,能看到四元数,以及再转换回去成欧拉角
    Yaw范围(-180~180)
    Pitch范围(-90~90)
    Roll范围(-180~180)
*/
#include "stdio.h"
#include "math.h"
#include "conio.h"
main()
{
float theta_z , theta_y ,theta_x ;
float cos_z_2;
float cos_y_2;
float cos_x_2;
float sin_z_2;
float sin_y_2;
float sin_x_2;
float Pitch;
float Roll;
float Yaw;
float Q[4];
float   T[3][3];
do{
printf("\nYaw = ");
scanf("%f",&theta_z);
printf("\nPitch = ");
scanf("%f",&theta_y);
printf("\nRoll = ");
scanf("%f",&theta_x);
theta_z = theta_z*3.1416/180;
theta_y = theta_y*3.1416/180;
theta_x = theta_x*3.1416/180;
 cos_z_2 = cos(0.5*theta_z);
 cos_y_2 = cos(0.5*theta_y);
 cos_x_2 = cos(0.5*theta_x);
 sin_z_2 = sin(0.5*theta_z);
 sin_y_2 = sin(0.5*theta_y);
 sin_x_2 = sin(0.5*theta_x);
Q[0] = cos_z_2*cos_y_2*cos_x_2 + sin_z_2*sin_y_2*sin_x_2;
Q[1] = cos_z_2*cos_y_2*sin_x_2 - sin_z_2*sin_y_2*cos_x_2;
Q[2] = cos_z_2*sin_y_2*cos_x_2 + sin_z_2*cos_y_2*sin_x_2;
Q[3] = sin_z_2*cos_y_2*cos_x_2 - cos_z_2*sin_y_2*sin_x_2;
printf("\nQ=[ %f %f %f %f]\n\n",Q[0],Q[1],Q[2],Q[3]) ;
printf("alpha = %f\n\n",acos(Q[0])*2*180/3.1416) ;
    T[0][0] =   Q[0]*Q[0]+Q[1]*Q[1]-Q[2]*Q[2]-Q[3]*Q[3] ;
    T[0][1] =                    2*(Q[1]*Q[2]-Q[0]*Q[3]);
    T[0][2] =                    2*(Q[1]*Q[3]+Q[0]*Q[2]);
    T[1][0] =                    2*(Q[1]*Q[2]+Q[0]*Q[3]);
    T[1][1] =   Q[0]*Q[0]-Q[1]*Q[1]+Q[2]*Q[2]-Q[3]*Q[3] ;
    T[1][2] =                    2*(Q[2]*Q[3]-Q[0]*Q[1]);
    T[2][0] =                    2*(Q[1]*Q[3]-Q[0]*Q[2]);
    T[2][1] =                    2*(Q[2]*Q[3]+Q[0]*Q[1]);
    T[2][2] =   Q[0]*Q[0]-Q[1]*Q[1]-Q[2]*Q[2]+Q[3]*Q[3] ;
    printf("T[0][0] = %9f,T[0][1] = %9f,T[0][2] = %9f\n",T[0][0],T[0][1],T[0][2]);
    printf("T[1][0] = %9f,T[1][1] = %9f,T[1][2] = %9f\n",T[1][0],T[1][1],T[1][2]);
    printf("T[2][0] = %9f,T[2][1] = %9f,T[2][2] = %9f\n\n",T[2][0],T[2][1],T[2][2]);
    Pitch = asin(-T[2][0]);
    Roll  = atan( T[2][1]/T[2][2]);
    Yaw   = atan( T[1][0]/T[0][0]);
    if(T[2][2]<0)
    {
        if(Roll < 0)
        {
           Roll = Roll+3.1416;
        }
        else
        {
           Roll = Roll-3.1416;
        }
    }
    if(T[0][0]<0)
    {
        if(T[1][0]>0)
        {
            Yaw = Yaw + 3.1416;
        }
        else
        {
            Yaw = Yaw - 3.1416;
        }
    }
    printf("Yaw   = %f\nPitch = %f\nRoll  = %f\n",Yaw*180/3.1416,Pitch*180/3.1416,Roll*180/3.1416) ;
}while(1);
    printf("Hello, world\n");
    getch();
}

参考:⑤ 四元数,欧拉角及姿态矩阵的相互转换

编译后输入Test.dae中bone001的欧拉角数据测试

提取第二组的旋转数据依次输入 68.805801 5.354727 1.021242 得到

可以看到转换出了3X3的旋转矩阵,这时候再来看看test.anim中bone001的第二组矩阵信息

转换行列顺序后   
   c[4][4]={0.359952,-0.931611,0.050351,9.930389},    {0.928291,0.363023,0.080552,0.000000},{-0.093322,0.017745,0.995477,-8.927394},{0.000000,0.000000,0.000000,1.000000}

3X3的矩阵内数据和欧拉角转换过来的一样,另外c[1][4],c[2][4],c[3][4],这里保存的是平移XYZ信息, 和dae文件中的数值一样,看起来平移数值没有经过转换, C[4][0]这里开始猜测是保存缩放信息..

如此,把源代码中3X3矩阵转换欧拉角的代码抄出来就可以做到动画矩阵信息的还原了。

矩阵乘除法

在查阅各种资料时,偶然看到一条这样的信息: 子节点计算坐标是要乘以父节点, 当时看不懂什么意思, 怎么乘,谁乘谁,  现在知道了 64 字节是一个4X4的矩阵后, 开始搜索 矩阵乘法。

举例两个2X2的矩阵矩阵相乘,

大多数说法是 第一个矩阵第一行的每个数字(25和5),各自乘以第二个矩阵第一列对应位置的数字(2和1),然后将乘积相加( 20 x 2 + 5 x 1)=45
拆解来就是
c[1][0]=20 x 2 + 5 x 1=45
c[1][1]=20 x 1 + 5 x 4=40
c[2][0]=15 x 2 + 10 x 1=40
c[2][1]=15 x 1 + 10 x 4 =55

参考:⑥ 矩阵乘法的本质是什么?
参考:⑦理解矩阵乘法

这些操作,和还原骨骼文件时的那个关键函数很像,为了验证矩阵乘法论,抄了代码进行测试
#include<stdio.h>
#include<stdlib.h>
#define col 3
#define row 3        //行列数量
class matrix//类的定义
{
private:
    double m[col][row];//矩阵设置为私有的,
public:
    matrix(){}//无参数的构造函数
    matrix(double a[col][row]);//有参数的构造函数
    matrix Add(matrix &b);//加法运算声明
    matrix Sub(matrix &b);//减法运算声明
    matrix Mul(matrix &b);//乘法运算声明
    matrix Div(matrix &b);//除法运算声明
    matrix Inverse();//求逆运算声明
    ~matrix();//析构函数声明
    void display();//显示函数声明
};
matrix::matrix(double a[col][row])//构造函数的定义
{
    int i,j;
    for(i=0;i<col;i++)
        for(j=0;j<row;j++)
            m[i][j]=a[i][j];
}
matrix matrix::Add(matrix &b)//加法运算
{
    int i,j;
    matrix*c=(matrix*)malloc(sizeof(matrix));
    for(i=0;i<col;i++)
        for(j=0;j<row;j++)
            c->m[i][j]=m[i][j]+b.m[i][j];
    return(*c);
}
matrix matrix::Sub(matrix &b)//减法运算
{
    int i,j;
    matrix*c=(matrix*)malloc(sizeof(matrix));
    for(i=0;i<col;i++)
        for(j=0;j<row;j++)
            c->m[i][j]=m[i][j]-b.m[i][j];
    return *c;
}
matrix matrix::Mul(matrix &b)//乘法运算
{
    int i,j,k;
    double sum=0;
    matrix*c=(matrix*)malloc(sizeof(matrix));
    for(i=0;i<col;i++)
    {
        for(j=0;j<row;j++)
        {
            for(k=0;k<row;k++)
                sum+=m[i][k]*(b.m[k][j]);
            c->m[i][j]=sum;
            sum=0;
        }
    }
    return(*c);
}
matrix matrix::Div(matrix &b)//除法运算
{
    //除法直接求解,参见主函数    
    matrix c;
    return(c);
}
matrix matrix::Inverse()//求逆运算
{                       //参考博客:http://www.cnblogs.com/rollenholt/articles/2050662.html
    int i,j,k,M=col,N=2*col;
    double b[col][col*2];
    matrix*c=(matrix*)malloc(sizeof(matrix));
    for(i=0;i<M;i++)     //赋值        
        for(j=0;j<M;j++)                    
            b[i][j]=m[i][j];      
    for(i=0;i<M;i++)    //扩展      
        for(j=M;j<N;j++)        
        {             
            if(i==(j-M))                             
                b[i][j]=1;                      
            else                           
                b[i][j]=0;                     
        }  
    /***************下面进行求逆运算*********/
        for(i=0;i<M;i++)    
        {         
            if(b[i][i]==0)    
            {            
                for(k=i;k<M;k++)             
                {                 
                    if(b[k][i]!=0)            //作者的博客里面此处为b[k][k],貌似是不正确的,
                                              //因为这对比如说是{0,0,1,1,0,1,0,1,1}的矩阵就会判断为不可逆,                    
                    {                         //而实际上该矩阵是可逆的,这里应该是作者笔误,待进一步求证        
                        for(int j=0;j<N;j++)                     
                        {                         
                            double temp;                        
                            temp=b[i][j];                       
                            b[i][j]=b[k][j];                      
                            b[k][j]=temp;                    
                        }                 
                        break;                 
                    }            
                }            
                if(k==M)
                {
                    printf("该矩阵不可逆!\n"); 
                    exit(0);
                }
            }        
            for(j=N-1;j>=i;j--)                  
                b[i][j]/=b[i][i];   

            for(k=0;k<M;k++)     
            {         
                if(k!=i)      
                {              
                    double temp=b[k][i];         
                    for(j=0;j<N;j++)                              
                        b[k][j]-=temp*b[i][j];             
                }       
            }   
        } 
    /**********************导出结果******************/
        for(i=0;i<M;i++)           
            for(j=3;j<N;j++)     //此处如果4X4矩阵 将两个3改为4   
                c->m[i][j-3]=b[i][j];    
    return (*c);
}

matrix::~matrix()
{}
void matrix::display()
{
    int i,j;
    for(i=0;i<col;i++)
    {
        for(j=0;j<row;j++)
            printf("%f  ",m[i][j]);
        printf("\n");
    }
}
void main()
{
    double a[3][3]={{1,0,1},{0,1,1},{0,3,1}};
    double b[3][3]={{0,0,1},{1,0,1},{0,1,0}};
    matrix ma(a),mb(b),mc;
    int flag;
    printf("----------------------------------------------------\n请选择要进行的操作:\n1、打印\t2、加法");
    printf("\t3、减法\n4、乘法\t5、除法\t6、求逆\n7、退出\n");
    printf("-----------------------------------------------------\n");
    scanf("%d",&flag);
    while((flag==1)||(flag==2)||(flag==3)||(flag==4)||(flag==5)||(flag==6)||(flag==7))
    {
        if(flag==1)
        {
            printf("矩阵a为:\n");
            ma.display();
            printf("矩阵b为:\n");
            mb.display();
        }
        if(flag==2)//矩阵加法运算
        {
            printf("矩阵加法运算结果:\n");
            mc=ma.Add(mb);
            mc.display();
        }
        else if(flag==3)//矩阵减法运算
        {
                printf("矩阵减法运算结果:\n");
            mc=ma.Sub(mb);
            mc.display();
        }
        else if(flag==4)//矩阵乘法运算
        {
                printf("矩阵乘法运算结果:\n");
            mc=ma.Mul(mb);
            mc.display();
        }
        else if(flag==5)//矩阵除法运算
        {
            printf("矩阵除法运算结果:\n");
            printf("矩阵的除法分成两类:\n 1、A\\B=inverse(A)*B \n 2、B/A=B*inverse(A)\n");
            printf("采用第1类,则a\\b的结果为:\n");
            mc=ma.Inverse();
            mc=mc.Mul(mb);
            mc.display();
            printf("采用第2类,则a/b的结果为:\n");
            mc=mb.Inverse();
            mc=ma.Mul(mc);
            mc.display();
        }
        else if (flag==6)//矩阵求逆运算
        {
            printf("矩阵a求逆运算结果为:\n");
            mc=ma.Inverse();
            mc.display();

            printf("矩阵b求逆运算结果为:\n");
            mc=mb.Inverse();
            mc.display();
        }
        else {exit(0);}
    printf("----------------------------------------------------\n请选择要进行的操作:\n1、打印\t2、加法");
    printf("\t3、减法\n4、乘法\t5、除法\t6、求逆\n7、退出\n");
    printf("-----------------------------------------------------\n");
    scanf("%d",&flag);
    }
}

参考:⑧矩阵的加、减、乘、除、求逆运算的实现

修改代码为4X4后,输入Testjuzheng.dae中Bone001和Bone002的矩阵数据,并相乘,结果为

结果等于编码器转换后的数据, 那么骨骼部分的关键算法就是矩阵相乘。
现在只需要反过来相除就可以了。矩阵的相除必须先求一个矩阵的逆矩阵,再和另一个矩阵相乘才是结果, 而有的矩阵是无法求出逆矩阵的, 什么样的逆矩阵无法逆出呢? 就是上面那个4四元方程无解的矩阵无法求逆。。 哈哈

参考:⑨为什么matrix(矩阵)不可以相除?

现在,花了很多天把骨骼和动画的数据全都摸清楚了,算法也抄到了,写还原工具却只花了半小时.... 我就要成功了!!!!

三四

最后的最后仍有阻碍

目前已经把骨骼和动画文件数据还原出来并保存到dae文件了,用3DMAX打开后也能动得起来, 并且动作和游戏差不多, 但这差的一点足以致命,因为动作中,有时候会突然的整个人物身子超快速的转一圈,转得莫名其妙。定位有问题的关键帧和部位后, 打开DAE文件的 output组, 会看到类似的排列
75.727524 95.727524 125.727524 135.727524 155.727524 -75.727524 -35.5465
细心的同学发现没有,一直的正数突然变成了负数, 这是因为物体在做旋转动作时,打个比方,转到了180度,下一步要转到190度, 但矩阵中没有190度这个概念, 所以用-170 表示190度这个位置, 虽然最终位置上是一样的, 但3DMAX会突然右转350(170+180)度,来达到这个左转190度的位置。 非常头疼。同时, 假如3DMAX在DAE文件中指定了190度的在转换成矩阵的时候也会变成-170,这就导致了,从欧拉角转换为矩阵时, 信息丢失,同理转回来也不对了, 比如552到了矩阵就变成-167,矩阵转回来还是-167。

可以看到 欧拉角输入的信息和输出的信息不一定是对等的,尤其是超过180度和90度的时候加上浮点精确度问题, -180变 179 , 179又变-179

到了这一步岂能轻易放弃,花了一个晚上的时间研究了个简单的算法修复这个问题, 就不展开讲了,写不动了。 最后成功的还原出动画

总结

其实回头一看,并没有多烧脑的算法,只是麻烦较多, 但为什么还原PKG花了半天,还原3D信息文件却花了9天了,还是开发经验的差距, 一眼看出PKG的算法省了不少时间, 而3D信息由于没接触过, 只能靠猜测,靠逆向,靠搜索收集信息一个综合的过程才能达到目的, 如果一开始就知道矩阵,知道欧拉角,相信就没这么多事了, 逆向熟悉的领域才能事半功倍,而有时候正向思考也能大幅度减少逆向分析的时间和难度。

如有转载希望标明出处

参考总结:

百度百科COLLADA介绍
collada快速入门
DAE模型与骨骼动画解析渲染
④:【3D计算机图形学】变换矩阵、欧拉角、四元数
四元数,欧拉角及姿态矩阵的相互转换
矩阵乘法的本质是什么?
理解矩阵乘法
矩阵的加、减、乘、除、求逆运算的实现
为什么matrix(矩阵)不可以相除?


[课程]Linux pwn 探索篇!

收藏
免费 2
支持
分享
最新回复 (43)
雪    币: 155
活跃值: (75)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
还未编缉完么
2017-9-22 06:55
0
雪    币: 3202
活跃值: (1917)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
文章好长,虽然看不懂,但是觉得作者能写这么长的文章再加截图,作者应该很有耐性。
2017-9-22 07:48
0
雪    币: 308
活跃值: (230)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
4
sadanlasi 还未编缉完么
一晚上终于编完了老哥
2017-9-22 08:00
0
雪    币: 308
活跃值: (230)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
5
chixiaojie 文章好长,虽然看不懂,但是觉得作者能写这么长的文章再加截图,作者应该很有耐性。
这些东西以后自己不会用到怕忘记了,就写详细点哈哈。
2017-9-22 08:01
0
雪    币: 768
活跃值: (515)
能力值: ( LV13,RANK:460 )
在线值:
发帖
回帖
粉丝
6
玩3D,全是些矩阵运算,看着就晕,楼主好牛
2017-9-22 08:56
0
雪    币: 66
活跃值: (2570)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
厉害啊      边逆边学
2017-9-22 09:02
0
雪    币: 308
活跃值: (230)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
8
FishSeeWater 玩3D,全是些矩阵运算,看着就晕,楼主好牛
我也是一知半解,靠抄代码过日子的
2017-9-22 09:29
0
雪    币: 1534
活跃值: (312)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
楼主好厉害,学习了
2017-9-22 12:40
0
雪    币: 5676
活跃值: (1303)
能力值: ( LV17,RANK:1185 )
在线值:
发帖
回帖
粉丝
10
666
2017-9-22 15:18
0
雪    币: 6818
活跃值: (153)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
不错!
2017-9-22 19:51
0
雪    币: 308
活跃值: (230)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
12
终于修复完不少错别字,  定下标题了... 
其实中间遇到的问题还可以讲很多东西,全写完太长了,就写了关键的地方。
2017-9-22 20:17
0
雪    币: 940
活跃值: (1053)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
13
很厉害,    重在坚持啊
2017-9-22 20:23
0
雪    币: 8865
活跃值: (2379)
能力值: ( LV12,RANK:760 )
在线值:
发帖
回帖
粉丝
14
一直用Model  Ripper,没研究过文件的路过了
2017-9-23 04:13
0
雪    币: 308
活跃值: (230)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
15






cvcvxk



一直用Model Ripper,没研究过文件的路过了


哟,修仙的V大 
其实我也一直知道有HOOK  DX接口来提取游戏模型的工具,  但提取动作的好像没有找到,可惜这次的目标恰恰就是动作信息。。
突然一想HOOK能截取到模型,应该也能针对模型的骨骼点做HOOK截取动作吧,要是过几天有人告诉我真有截动作的那我就悲剧了。
2017-9-23 05:14
0
雪    币: 8865
活跃值: (2379)
能力值: ( LV12,RANK:760 )
在线值:
发帖
回帖
粉丝
16






noNumber









cvcvxk



一直用Model Ripper,没研究过文件的路过了


哟,修仙的V大&nbsp; 其实我也一直知道有H ...

好像真的能。
我一般都是用老外的工具:
http://cgig.ru/en/
http://www.gildor.org/


2017-9-23 06:19
0
雪    币: 308
活跃值: (230)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
17






cvcvxk









noNumber









cvcvxk



一直用Model Ripper,没研究过文件 ...

看到你的消息吓死我了...  虽然这么说不好..  好像不能截到动画

Ninja  Ripper我试用了,只能截到模型和图片,      主页有一篇文章提取到的游戏动画是游戏压缩包内解出来的,差点吓尿...
第二个网站好像是虚幻引擎专用,不过也没看到动画相关....
还是谢谢了。
2017-9-23 09:38
0
雪    币: 2673
活跃值: (2947)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
18
群主详细讲解下怎么过保护把。。要不然过不了保护进行不下去!!!。。
2017-9-23 09:45
0
雪    币: 308
活跃值: (230)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
19
gtict 群主详细讲解下怎么过保护把。。要不然过不了保护进行不下去!!!。。
我这款游戏冷门游戏,TP保护版本比较低,网上搜得到的相关方法并且有效的。
2017-9-23 09:48
0
雪    币: 6729
活跃值: (3902)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
感谢分享,  学习了,佩服楼主的耐心
2017-9-23 10:26
0
雪    币: 1
活跃值: (16)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
顶,  要消化完不容易啊。
2017-9-23 13:04
0
雪    币: 8865
活跃值: (2379)
能力值: ( LV12,RANK:760 )
在线值:
发帖
回帖
粉丝
22






noNumber









cvcvxk









noNumber









cv ...

我是能,不是说ninja可以,第二个那个UE引擎专怼,我超喜欢。ninjia出来的model有bones的基本型,没有动作而已,自己拿着bones做动作就行了(
我司武术指导用动作采集器生成bones动作)

不过截动作出来,需要先确定模型,然后把动作过程复制出来,然后还原成可以看的文件。
2017-9-23 17:54
0
雪    币: 308
活跃值: (230)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
23
cvcvxk noNumber cvcvxk no ...
我主要是害怕,自己像愣头青一样逆逆逆,  结果早就有现成的工具可以把动作提取出来就悲剧了。
2017-9-23 18:45
0
雪    币: 1
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
能力很高,技术很强!老大给个机会让我给你提鞋
2017-9-23 22:20
0
雪    币: 12626
活跃值: (3122)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
3d  矩阵  od  ida都懂,但我就逆向不出来,
2017-9-24 07:53
0
游客
登录 | 注册 方可回帖
返回
//