逆向分析
逆向分析
00 前言
GPA截取出来的HLSL代码, 要注意, Unity的内置矩阵(类似unity_ObjectToWorld等)编译出来的代码, 实际上无法直接用于shader. 因为大概率hlslcc_mtxunity_ObjectToWorld与unity_ObjectToWorld互为转置矩阵.
原因是Unity在编译时将矩阵unity_ObjectToWorld通过转置换成hlslcc_mtxunity_ObjectToWorld
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// o.positionWS = mul(unity_ObjectToWorld,v.vertex).xyz; // gles3x编译出来是以下未注释代码, 但如果将下面未注释的代码复制粘贴到着色器中, 并将 // hlslcc_mtx4x4unity_ObjectToWorld 替换为 unity_ObjectToWorld, 实际上视觉效果等同于 // o.positionWS = mul(v.vertex,unity_ObjectToWorld).xyz; o.positionWS = v.vertex.yyy * hlslcc_mtx4x4unity_ObjectToWorld[1].xyz; o.positionWS = hlslcc_mtx4x4unity_ObjectToWorld[0].xyz * v.vertex.xxx + o.positionWS; o.positionWS = hlslcc_mtx4x4unity_ObjectToWorld[2].xyz * v.vertex.zzz + o.positionWS; o.positionWS = hlslcc_mtx4x4unity_ObjectToWorld[3].xyz * v.vertex.www + o.positionWS; // 同样 // o.positionWS = mul(v.vertex,unity_ObjectToWorld).xyz; // gles3x编译出来是以下未注释代码, 但如果将下面未注释的代码复制粘贴到着色器中, 并将 // hlslcc_mtx4x4unity_ObjectToWorld 替换为 unity_ObjectToWorld, 实际上视觉效果等同于 // o.positionWS = mul(unity_ObjectToWorld,v.vertex).xyz; o.positionWS.x = dot(v.vertex, hlslcc_mtx4x4unity_ObjectToWorld[0]); o.positionWS.y = dot(v.vertex, hlslcc_mtx4x4unity_ObjectToWorld[1]); o.positionWS.z = dot(v.vertex, hlslcc_mtx4x4unity_ObjectToWorld[2]);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
//Unity内部有一个优化正交和透视矩阵的函数在UnityInput.hlsl中 float4x4 OptimizeProjectionMatrix(float4x4 M) { // Matrix format (x = non-constant value). // Orthographic Perspective Combined(OR) // | x 0 0 x | | x 0 x 0 | | x 0 x x | // | 0 x 0 x | | 0 x x 0 | | 0 x x x | // | x x x x | | x x x x | | x x x x | <- oblique projection row // | 0 0 0 1 | | 0 0 x 0 | | 0 0 x x | // Notice that some values are always 0. // We can avoid loading and doing math with constants. M._21_41 = 0; M._12_42 = 0; return M; }
1
col = half4(unity_ObjectToWorld[0].w,unity_ObjectToWorld[1].w,unity_ObjectToWorld[2].w,1.0);
用对应的轴拖动, 会刚好变成对应轴的颜色.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
//矩阵的数组取值是按照行进行取值的. //unity是按照列存储矩阵, 即第一列是x的缩放旋转变化, 第二列是y的缩放旋转变化, 第三列是z的缩放旋转变化, 最后一列是 //xyz的位移 |1,0,0,x| |00,01,02,03| |0,1,0,y| |10,11,12,12| |0,0,1,z| |20,21,22,23| |0,0,0,1| |30,31,32,33| //这样来存储unity_ObjectToWorld矩阵的 //但是, 矩阵乘法如果要写成类似 o.positionWS = v.vertex.yyy * hlslcc_mtx4x4unity_ObjectToWorld[1].xyz; o.positionWS = hlslcc_mtx4x4unity_ObjectToWorld[0].xyz * v.vertex.xxx + o.positionWS; o.positionWS = hlslcc_mtx4x4unity_ObjectToWorld[2].xyz * v.vertex.zzz + o.positionWS; o.positionWS = hlslcc_mtx4x4unity_ObjectToWorld[3].xyz * v.vertex.www + o.positionWS; //的形式 //那么hlslcc_mtx4x4unity_ObjectToWorld这个矩阵, 就必须按照行来存储. 而正常的矩阵乘法, 是按照列相乘的方式进行 //的.比如 |-1, 0|*|2|=2*|-1|+2*|0|=|-2| | 0, 1| |2| | 0| |1| | 2| //换句话说, 反编译得到的, 或者unity编译后的形似hlslcc_mtx4x4unity_ObjectToWorld这个矩阵, 是按照行来存储的. //所以不能直接调用Unity的unity_ObjectToWorld这个列存储矩阵来代替hlslcc_mtx4x4unity_ObjectToWorld
直接定义矩阵可以看到编译出的代码是进行的dot运算
1 2 3 4 5 6 7 8 9 10 11 12
half4 col = tex2D(_MainTex, i.uv); OTW = (float4x4)0; OTW[0] = float4(1, 0, 0, -0.1); OTW[1] = float4(0, 1, 0, -0.7); OTW[2] = float4(0, 0, 1, -0.5); OTW[3] = float4(1, 1, 1, 1); half4 output = mul(OTW, col);//这一行等同于下面四行 output.x = dot(float4(1, 0, 0, -0.1), col); output.y = dot(float4(0, 1, 0, -0.7), col); output.z = dot(float4(0, 0, 1, -0.5), col); output.w = dot(float4(1, 1, 1, 1), col);
而使用unity内置矩阵则可以看到
1 2 3 4 5 6 7
half4 col = tex2D(_MainTex, i.uv); half4 output = mul(unity_ObjectToWorld, col); output = col.yyyy * hlslcc_mtx4x4unity_ObjectToWorld[1]; output = hlslcc_mtx4x4unity_ObjectToWorld[0] * col.xxxx + output; output = hlslcc_mtx4x4unity_ObjectToWorld[2] * col.zzzz + output; output = hlslcc_mtx4x4unity_ObjectToWorld[3] * col.wwww + output;
- mul - Win32 apps | Microsoft Learn
目前大部分的截帧软件, 即便是手机取得了Root权限, 并且设置全局为Debuggable, 仍旧无法截帧.
01 处理方法
内置矩阵是列排列, 我们自定义的矩阵也使用列排列(即用列来存储每个向量的变化). 而编译后的内置矩阵会变为行排列. 正常正向写shader无需在意, 但逆向时, 在进行内置矩阵的运算时, 在将hlslcc_mtx4x4unity_ObjectToWorld替换为unity_ObjectToWorld的同时, 也要将运算进行修改(交换mul中的位置, 或者使用unity_ObjectToWorld的转置矩阵来替换hlslcc_mtx4x4unity_ObjectToWorld).
- 注: 反编译后为_hlslcc_mtxUnity_ObjectToWorld, unity直接编译则为hlslcc_mtx4x4unity_ObjectToWorld
1
2
#define _hlslcc_mtxUnity_ObjectToWorld transpose(unity_ObjectToWorld)
#define hlslcc_mtxUnity_ObjectToWorld transpose(unity_ObjectToWorld)
02 常用函数及敏感数值对应
lerp——(w*(b-a)+a)
1
2
3
4
lerp(a,b,w)
{
return a+w*(b-a);
}
PerceptualRoughness——(1.7,0.7,6)
1
2
3
4
5
6
7
//mipCount一般为6
//perceptualRoughness就是直接的贴图采样结果
real PerceptualRoughnessToMipmapLevel(real perceptualRoughness, uint mipMapCount)
{
perceptualRoughness = perceptualRoughness*(1.7-0.7*perceptualRoughness);
return perceptualRoughness*mipCount;
}
03 CSV转Mesh
注意, GPA截帧出来的模型, 其三角面的构建方向与Unity相反. 在Unity中使用以下函数进行修正
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// 翻转mesh的三角片元构建方式
/// </summary>
/// <param name="mesh"></param>
private void FlipTriangles(Mesh mesh)
{
// triangles数量对应subMesh的index数量
var trianglesHash = mesh.triangles;
int offset = 0;
for (int i = 0; i < mesh.subMeshCount; i++)
{
var trianglesSubHash = new ArraySegment<int>(trianglesHash, offset, mesh.GetSubMesh(i).indexCount).ToArray();
offset += mesh.GetSubMesh(i).indexCount;
for (int j = 0; j < mesh.GetSubMesh(i).indexCount; j += 3)
{
(trianglesSubHash[j], trianglesSubHash[j + 2]) = (trianglesSubHash[j + 2], trianglesSubHash[j]);
}
mesh.SetTriangles(trianglesSubHash, i);
}
}
然后用Maya脚本将导出的UV, 光照UV, 顶点色, 进行还原, 但要注意的是, Maya导出FBX并不支持超过2位的UV数据(参考), 如果对象的UV数据是合并后的4位UV数据, 则还是建议使用下面的Git仓库中的工具进行还原.
CSV2MESH工具:
04 FAQ
03.1 截帧时提示App没有读写权限
开发者选项里面找到”强制允许将应用写入外部存储设备”, 开启即可.
参考网页
手游逆向分析之Snapdragon Profiler - 知乎 (zhihu.com)
渲染逆向工程:打造一台调试任意Android游戏的设备 - 知乎 (zhihu.com)
0基础 Unity Shader 逆向 使用竞品android平台黑盒 unity shader - 知乎 (zhihu.com)
Intel GPA截帧模型恢复UV顶点色(houdini方法) - 知乎 (zhihu.com)
SideFX Houdini FX 19.5.773/19.5.493/19.0.589 Win/Mac X-Force注册机破解版 – 龋齿一号GFXCamp
