文章

逆向分析

逆向分析

逆向分析

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);
      

      image-20240117233833680

      • 用对应的轴拖动, 会刚好变成对应轴的颜色.

        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工具:

javelinlin/GPA_CSV2MESH_TOOL_pure_version: 将 GPA 中抓取的模型导出CSV再通过U3D工具导出 FBX 或是 UnityMesh (*.asset) (github.com)这个工具.

04 FAQ

03.1 截帧时提示App没有读写权限

开发者选项里面找到”强制允许将应用写入外部存储设备”, 开启即可.

参考网页

手游逆向分析之Snapdragon Profiler - 知乎 (zhihu.com)

渲染逆向工程:打造一台调试任意Android游戏的设备 - 知乎 (zhihu.com)

ADB 命令大全 - 知乎 (zhihu.com)

0基础 Unity Shader 逆向 使用竞品android平台黑盒 unity shader - 知乎 (zhihu.com)

Intel GPA截帧模型恢复UV顶点色(houdini方法) - 知乎 (zhihu.com)

获取截帧的模型资源 - 知乎 (zhihu.com)

SideFX Houdini FX 19.5.773/19.5.493/19.0.589 Win/Mac X-Force注册机破解版 – 龋齿一号GFXCamp

游戏图形逆向分析 - 知乎 (zhihu.com)

GPA截帧逆向还原Unity Shader - 知乎 (zhihu.com)

本文由作者按照 CC BY 4.0 进行授权