@source: https://learnopengl.com/Getting-started/Camera

摄像机#

opengl 中通过把场景往相反方向移动来模拟出摄像机

glm::lookAt#

$$ x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} $$$$ \text{LookAt}= \begin{bmatrix} R_x & R_y & R_z & 0\\ U_x & U_y & U_z & 0\\ D_x & D_y & D_z & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} 1 & 0 & 0 & -P_x\\ 0 & 1 & 0 & -P_y\\ 0 & 0 & 1 & -P_z\\ 0 & 0 & 0 & 1 \end{bmatrix} $$
glm::mat4 view = glm::lookAt(
    cameraPos,    // Camera position in world space
    cameraTarget, // Target point the camera is looking at
    cameraUp      // “Up” direction for the camera
);

lookAt 构造 view 矩阵,需要三组向量,其中有这样的关系

glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
右向量(公式中的 R)由上向量(U)和方向向量(D)正交得到

解释了 lookAt 的三个参数

控制 cameraTarget 在 (0, 0, 0),将 cameraPos 绕上轴(y轴;OpenGL 中 z 轴正方向从屏幕指出)旋转

float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0)); 

cameraDirection, cameraFront 在文档里有点混淆(可能是相反的关系?),暂时不做深究

自由移动#

为 GLFW 的键盘输入定义一个 processInput,添加检查的按键,按下按键 cameraPosition 更新,向前/向后移动把位置向量加上/减去方向向量即可,左右移动则先用叉乘创建右向量然后标准化沿着移动就可以

视角移动#

文档中提到的 cameraFront 被鼠标输入改变,两种欧拉角(其实总共有三种):

  • 俯仰角 (Pitch) 控制抬头低头
  • 偏航角 (Yaw) 控制左右转头

根据三角函数把两者转换成新的 front

总结#

关键值

  • view -> 反推玩家观察姿态
    • position
    • front/direction -> 玩家当前视角方向
    • up/right
  • yaw/pitch

为了避免将来我自己看得一头雾水,把我想到的一些记录一下:

  1. 基本认为 front 和 direction 相反
  2. 由 right = normalize(cross(up, direction)) 得,right = normalize(cross(front, up))

渲染图元#

一个物体被画出来,涉及:

  1. CPU 准备顶点数据,写入 VBO
    1. 位置 position
    2. 法线 normal
    3. 纹理坐标 textcoord
    4. 顶点颜色
  2. CPU 准备索引数据,写入 EBO
  3. VAO 记录顶点属性布局
  4. CPU 激活 shader program
    1. vertex shader:处理顶点,做坐标变换
      • 典型计算:gl_Position = projection * view * model * vec4(aPos, 1.0);
        • 模型矩阵 model,把模型局部系变换到世界系
        • 观察矩阵 view,把世界系变换到相机系
        • 投影矩阵 projection 做投影,准备后续光栅化流程
    2. fragement shader:处理像素,决定最终颜色
  5. CPU 绑定纹理
  6. CPU 计算 model/view/projection
  7. CPU 通过 uniform 把矩阵上传到 GPU
    • 变换矩阵 model / view / projection
    • 光照参数
    • 材质参数
    • 这一步可以截获到 3D 结构
  8. CPU 调用 glDrawElements
  9. GPU 按当前 VAO/EBO/program/texture/uniform 执行绘制

不难想到 glDrawElements 是绝佳的 hook 点,走到调用点时所有关键状态都已经设置好了

这个接口从数组中获取数据渲染图元。

游戏安全侧#

  • 客户端游戏的图形层数据并不安全,坐标、血量、命中框、视角、骨骼矩阵,只要进入本地进程理论上都可以被攻击
  • 渲染 API 是典型的攻击面,因为它处在『业务逻辑』和『最终显示』之间
  • 一种方案是渲染层数据提取
    • 不修改游戏逻辑
    • 不涉及网络协议
    • 只观察渲染调用现场

2021 腾讯游戏安全技术竞赛初赛 PC 端#

int __cdecl sub_4064D0()
{
  glfwInit_wrapper();
  glfwWindowHint_wrapper(139266, 3);
  glfwWindowHint_wrapper(139267, 3);
  glfwWindowHint_wrapper(139272, 204801);
  Window_wrapper = glfwCreateWindow_wrapper(800, 600, "XDDDDDDDDD", 0, 0);
  v1 = Window_wrapper;
  if ( !Window_wrapper )
  {
    glfwTerminate_wrapper();
    return -1;
  }
  glfwMakeContextCurrent_wrapper(Window_wrapper);
  glfwSetFramebufferSizeCallback_wrapper(v1, framebuffer_size_callback);
  glfwSetCursorPosCallback_wrapper(v1, mouse_cursor_callback);
  glfwSetInputMode_wrapper(v1, 208897, 212995);
  if ( !load_opengl_functions() )
    return -1;
  glEnable_ptr(2929);
  build_shader_program(v31, v32);
  v59[0] = xmmword_455340;
  v59[1] = xmmword_456AA0;
  v59[2] = xmmword_455470;
  v59[3] = xmmword_4554D0;
  v59[4] = xmmword_455560;
  v59[5] = xmmword_455320;
  v59[6] = xmmword_456AE0;
  v59[7] = xmmword_456A90;
  v59[8] = xmmword_455450;
  v59[9] = xmmword_455300;
  v59[10] = xmmword_455540;
  v59[11] = xmmword_4554A0;
  v59[12] = xmmword_455500;
  v59[13] = xmmword_456A70;
  v59[14] = xmmword_4552A0;
  v59[15] = xmmword_455550;
  v59[16] = xmmword_456A60;
  v59[17] = xmmword_456AC0;
  v59[18] = xmmword_456A80;
  v59[19] = xmmword_455530;
  v59[20] = xmmword_4552D0;
  v59[21] = xmmword_4554B0;
  v59[22] = xmmword_455470;
  v59[23] = xmmword_455460;
  v59[24] = xmmword_455560;
  v59[25] = xmmword_455330;
  v59[26] = xmmword_456AB0;
  v59[27] = xmmword_456A40;
  v59[28] = xmmword_455450;
  v59[29] = xmmword_4552F0;
  v59[30] = xmmword_455340;
  v59[31] = xmmword_456AB0;
  v59[32] = xmmword_456A50;
  v59[33] = xmmword_455460;
  v59[34] = xmmword_455300;
  v59[35] = xmmword_4552D0;
  v59[36] = xmmword_456AD0;
  v59[37] = xmmword_4554F0;
  v59[38] = xmmword_4554C0;
  v59[39] = xmmword_455560;
  v59[40] = xmmword_455540;
  v59[41] = xmmword_455480;
  v59[42] = xmmword_4554E0;
  v59[43] = xmmword_456A30;
  v59[44] = xmmword_455520;
  glGenVertexArrays_ptr(1, &v58);
  glGenBuffers_ptr(1, &v54);
  glBindVertexArray_ptr(v58);
  glBindBuffer_ptr(34962, v54);
  glBufferData_ptr(34962, 720, v59, 35044);
  glVertexAttribPointer_ptr(0, 3, 5126, 0, 20, 0);
  glEnableVertexAttribArray_ptr(0);
  glVertexAttribPointer_ptr(1, 2, 5126, 0, 20, 12);
  glEnableVertexAttribArray_ptr(1);
  glGenTextures_ptr(1, &v56);
  glBindTexture_ptr(3553, v56);
  glTexParameteri_ptr(3553, 10242, 10497);
  glTexParameteri_ptr(3553, 10243, 10497);
  glTexParameteri_ptr(3553, 10241, 9729);
  glTexParameteri_ptr(3553, 10240, 9729);
  dword_465FC4 = 1;
  v3 = (void *)decode_image_from_memory(&v40, &v39, &v57);
  v4 = v3;
  if ( v3 )
  {
    glTexImage2D_ptr(3553, 0, 6407, LODWORD(v40), v39, 0, 6407, 5121, v3);
    glGenerateMipmap_ptr(3553);
  }
  j___free_base(v4);
  glGenTextures_ptr(1, &v55);
  glBindTexture_ptr(3553, v55);
  glTexParameteri_ptr(3553, 10242, 10497);
  glTexParameteri_ptr(3553, 10243, 10497);
  glTexParameteri_ptr(3553, 10241, 9729);
  glTexParameteri_ptr(3553, 10240, 9729);
  v5 = (void *)decode_image_from_memory(&v40, &v39, &v57);
  v6 = v5;
  if ( v5 )
  {
    glTexImage2D_ptr(3553, 0, 6407, LODWORD(v40), v39, 0, 6408, 5121, v5);
    glGenerateMipmap_ptr(3553);
  }
  j___free_base(v6);
  v7 = v38;
  glUseProgram_ptr(v38);
  v43 = 15;
  v42 = 0;
  LOBYTE(Block[0]) = 0;
  std_string_assign_n("texture1", 8u);
  v60 = 0;
  v8 = Block;
  if ( v43 >= 0x10 )
    v8 = (void **)Block[0];
  UniformLocation_ptr = glGetUniformLocation_ptr(v38, v8, 0);
  glUniform1i_ptr(UniformLocation_ptr);
  v60 = -1;
  std_string_destroy(Block);
  v43 = 15;
  v42 = 0;
  LOBYTE(Block[0]) = 0;
  std_string_assign_n("texture2", 8u);
  v60 = 1;
  v10 = Block;
  if ( v43 >= 0x10 )
    v10 = (void **)Block[0];
  v11 = glGetUniformLocation_ptr(v38, v10, 1);
  glUniform1i_ptr(v11);
  v60 = -1;
  std_string_destroy(Block);
  if ( !glfwWindowShouldClose_wrapper(v1) )
  {
    v12 = *(double *)_libm_sse2_tan_precise().m128_u64;
    v38 = 0;
    v40 = 1.0 / v12;
    v57 = 1.0 / (float)(v12 * 1.3333334);
    do
    {
      flt_465FBC = glfwGetTime_wrapper();
      if ( glfwGetKey_wrapper(v1, 256) == 1 )
        glfwSetWindowShouldClose_wrapper(v1, 1);
      glClearColor_ptr(1045220557, 1050253722, 1050253722, 1065353216);
      glClear_ptr(16640);
      glActiveTexture_ptr(33984);
      glBindTexture_ptr(3553, v56);
      glActiveTexture_ptr(33985);
      glBindTexture_ptr(3553, v55);
      glUseProgram_ptr(v7);
      mat4_identity(&v38);
      v44 = v57;
      v45 = v40;
      v47 = -1082130432;
      v46 = -1082113638;
      v48 = -1102249656;
      v43 = 15;
      v42 = 0;
      LOBYTE(Block[0]) = 0;
      std_string_assign_n("projection", 0xAu);
      v60 = 2;
      v13 = Block;
      if ( v43 >= 0x10 )
        v13 = (void **)Block[0];
      v14 = glGetUniformLocation_ptr(v7, v13, 1);
      glUniformMatrix4fv_ptr(v14);
      v60 = -1;
      if ( v43 >= 0x10 )
      {
        v15 = Block[0];
        if ( v43 + 1 >= 0x1000 )
        {
          if ( ((int)Block[0] & 0x1F) != 0
            || (v16 = (void *)*((_DWORD *)Block[0] - 1), v16 >= Block[0])
            || (unsigned int)((char *)Block[0] - (char *)v16) < 4
            || (unsigned int)((char *)Block[0] - (char *)v16) > 0x23 )
          {
LABEL_52:
            _invalid_parameter_noinfo_noreturn();
          }
          v15 = (void *)*((_DWORD *)Block[0] - 1);
        }
        j_j___free_base(v15);
      }
      v34[0] = *(float *)&dword_464CC4 + *(float *)&qword_464CAC;
      v34[1] = *(float *)&dword_464CC8 + *((float *)&qword_464CAC + 1);
      v34[2] = *(float *)&dword_464CCC + *(float *)&dword_464CB4;
      build_view_matrix(v34);
      v43 = 15;
      v42 = 0;
      LOBYTE(Block[0]) = 0;
      std_string_assign_n("view", 4u);
      v60 = 3;
      v17 = Block;
      if ( v43 >= 0x10 )
        v17 = (void **)Block[0];
      v18 = glGetUniformLocation_ptr(v7, v17, 1);
      glUniformMatrix4fv_ptr(v18);
      v60 = -1;
      if ( v43 >= 0x10 )
      {
        v19 = Block[0];
        if ( v43 + 1 >= 0x1000 )
        {
          if ( ((int)Block[0] & 0x1F) != 0 )
            goto LABEL_52;
          v20 = (void *)*((_DWORD *)Block[0] - 1);
          if ( v20 >= Block[0]
            || (unsigned int)((char *)Block[0] - (char *)v20) < 4
            || (unsigned int)((char *)Block[0] - (char *)v20) > 0x23 )
          {
            goto LABEL_52;
          }
          v19 = (void *)*((_DWORD *)Block[0] - 1);
        }
        j_j___free_base(v19);
      }
      v43 = 15;
      v42 = 0;
      LOBYTE(Block[0]) = 0;
      glBindVertexArray_ptr(v58);
      v21 = 0;
      v22 = dword_46601C;
      if ( (dword_466020 - (int)dword_46601C) >> 2 )
      {
        v33[0] = 1065353216;
        v33[1] = 1050253722;
        v33[2] = 1056964608;
        do
        {
          v23 = v22[v21 + 1];
          v24 = v22[v21 + 2];
          v53[0] = v22[v21];
          v53[1] = v23;
          v53[2] = v24;
          mat4_zero_init(&v49);
          v25 = (__int128 *)build_model_translation_matrix(v53);
          v49 = *v25;
          v50 = v25[1];
          v51 = v25[2];
          v52 = v25[3];
          v26 = (__int128 *)build_model_scale_matrix(v33);
          v37 = 15;
          v36 = 0;
          LOBYTE(v35) = 0;
          v49 = *v26;
          v50 = v26[1];
          v51 = v26[2];
          v52 = v26[3];
          std_string_assign_n("model", 5u);
          v60 = 4;
          v27 = &v35;
          if ( v37 >= 0x10 )
            v27 = v35;
          v28 = glGetUniformLocation_ptr(v7, v27, 1);
          glUniformMatrix4fv_ptr(v28);
          v60 = -1;
          if ( v37 >= 0x10 )
          {
            v29 = v35;
            if ( v37 + 1 >= 0x1000 )
            {
              if ( ((unsigned __int8)v35 & 0x1F) != 0 )
                goto LABEL_52;
              v30 = *((_DWORD *)v35 - 1);
              if ( v30 >= (unsigned int)v35 || (char *)v35 - v30 < (char *)4 || (char *)v35 - v30 > (char *)0x23 )
                goto LABEL_52;
              v29 = (void *)*((_DWORD *)v35 - 1);
            }
            j_j___free_base(v29);
          }
          v37 = 15;
          v36 = 0;
          LOBYTE(v35) = 0;
          glDrawArrays_ptr(4u, 0, 0x24u, 0);
          v21 += 3;
          v22 = dword_46601C;
        }
        while ( v21 < (dword_466020 - (int)dword_46601C) >> 2 );
      }
      glfwSwapBuffers_wrapper(v1);
      glfwPollEvents_wrapper();
    }
    while ( !glfwWindowShouldClose_wrapper(v1) );
  }
  glDeleteVertexArrays_ptr(1, &v58);
  glDeleteBuffers_ptr(1, &v54);
  glfwTerminate_wrapper();
  return 0;
}

渲染逻辑:

加载 shader
创建 VBO
加载两个纹理
设置 uniform sampler

texture1 -> 0
texture2 -> 1

每帧循环:
	设置 projection 矩阵
	设置 view 矩阵

for each object
	设置 model 矩阵
	glDrawElements(GL_TRIANGLES, 36)

下面注入的 dll 劫持了摄像机,用键盘输入改 cameraPosition,并实现

  • W/S:沿 cameraFront 前后移动
  • A/D:沿右向量 cross(cameraFront, cameraUp) 左右平移
  • R/T:通过修改 fov,传入 projection 实现视野缩放

实现自由视角解锁

没有 hook glDrawElements,而是 hook glUniformMatrix4fv 的参数准备点

// @source: https://www.xuenixiang.com/thread-3160-1-1.html
// DllMain
#include "stdafx.h"
 
// 复用原程序中的 glfwGetKey_wrapper
#define RVA_glfwGetKey 0x19920
 
// 四个点,贴了附近的代码
// .text:0040653D                 call    glfwCreateWindow_wrapper
// .text:00406542                 mov     ebx, eax
#define RVA_Get_p_windows_HookPoint 0x06542
// .text:00406BD8                 sub     esp, 10h
// .text:00406BDB                 mov     [esp+474h+var_468], 3F800000h
// .text:00406BE3                 mov     [esp+474h+var_46C], 3E99999Ah
// .text:00406BEB                 mov     [esp+474h+var_470], 3E99999Ah
// .text:00406BF3                 mov     [esp+474h+var_474], 3E4CCCCDh
// .text:00406BFA                 call    glClearColor_ptr
// do
// {
//       if ( glfwGetKey_wrapper(v1, 256) == 1 )
//         glfwSetWindowShouldClose_wrapper(v1, 1);
//       glClearColor_ptr(1045220557, 1050253722, 1050253722, 1065353216);
//   ...
// }
#define RVA_Key_HookPoint 0x06BDB // 这里是接近渲染主循环每帧开头的地方,放消息钩子很合适
// std_string_assign_n("projection", 0xAu);
// v60 = 2;
// v13 = Block;	// 覆盖这里
// if ( v43 >= 0x10 )
//   v13 = (void **)Block[0];
// v14 = glGetUniformLocation_ptr(v7, v13, 1);
// glUniformMatrix4fv_ptr(v14);
#define RVA_Setprojection_HookPoint 0x06CDB
// 跟上面的差不多
#define RVA_SetView_HookPoint 0x06DEE
 
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
 
float fov = 50.0f;
 
typedef DWORD(__cdecl* pfnglfwGetKey)(DWORD p_windows, DWORD KeyValue);
pfnglfwGetKey m_glfwGetKey;
 
DWORD p_windows;
HMODULE hModule_EXE;
PVOID p_windows_HookPoint = NULL;
PVOID p_GetKey_HookPoint = NULL;
 
PVOID p_Setprojection = NULL;
PVOID p_Setview = NULL;
 
glm::mat4 projection;
glm::mat4 view;
 
// 拿到窗口句柄:目标不是找某个静态值而是主动复用原程序的输入系统,拿到窗口句柄是为了后面能够调用 glfwGetKey
VOID NAKED Get_p_windows()
{
    STACK_FRAME_BEGIN
        __asm {
        mov p_windows, eax
    }
    STACK_FRAME_END
        __asm {
        push p_windows_HookPoint
        ret
    }
}
 
// 把渲染循环 hook 下来后插入对键盘的处理
VOID NAKED prcessInPut()
{
    STACK_FRAME_BEGIN
        if (m_glfwGetKey(p_windows, GLFW_KEY_W) == GLFW_PRESS)
        {
            cameraPos += cameraFront;
        }
    if (m_glfwGetKey(p_windows, GLFW_KEY_S) == GLFW_PRESS)
    {
        cameraPos -= cameraFront;
    }
    if (m_glfwGetKey(p_windows, GLFW_KEY_A) == GLFW_PRESS)
    {
        cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp));
    }
    if (m_glfwGetKey(p_windows, GLFW_KEY_D) == GLFW_PRESS)
    {
        cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp));
    }
  // R、V 拉视野
    if (m_glfwGetKey(p_windows, GLFW_KEY_R) == GLFW_PRESS)
    {
        fov -= 1.0;
    }
    if (m_glfwGetKey(p_windows, GLFW_KEY_T) == GLFW_PRESS)
    {
        fov += 1.0f;
 
    }
  // 用改过的 fov 计算 projection,实现视野拉近/拉远
    view = glm::lookAt(cameraPos, cameraFront + cameraPos, cameraUp);
    projection = glm::perspective(glm::radians(fov), (float)800.0 / (float)600.0, 0.1f, 200.0f);
 
    STACK_FRAME_END
        __asm {
        push p_GetKey_HookPoint
        ret
    }
}
 
// hook uniform 的传参,修改 projection
VOID NAKED Setprojection()
{
    __asm {
        mov ecx, offset projection
        push p_Setprojection
        ret
    }
}
 
// 同上
VOID NAKED Setview()
{
    __asm
    {
        mov ecx, offset view
        push p_Setview
        ret
    }
}
 
BOOL DetourHook()
{
    if (DetourTransactionBegin() == NO_ERROR)
    {
        DetourAttach(&p_windows_HookPoint, Get_p_windows);
        DetourAttach(&p_GetKey_HookPoint, prcessInPut);
        DetourAttach(&p_Setprojection, Setprojection);
        DetourAttach(&p_Setview, Setview);
 
        if (DetourTransactionCommit() == NO_ERROR)
        {
            return TRUE;
        }
    }
    return FALSE;
}
 
BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        hModule_EXE = GetModuleHandle(NULL);
        m_glfwGetKey = (pfnglfwGetKey)((DWORD)hModule_EXE + RVA_glfwGetKey);
 
        p_windows_HookPoint = (PVOID)((DWORD)hModule_EXE + RVA_Get_p_windows_HookPoint);
        p_GetKey_HookPoint = (PVOID)((DWORD)hModule_EXE + RVA_Key_HookPoint);
        p_Setprojection = (PVOID)((DWORD)hModule_EXE + RVA_Setprojection_HookPoint);
        p_Setview = (PVOID)((DWORD)hModule_EXE + RVA_SetView_HookPoint);
 
        DetourHook();
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

小补充#

  1. glBindTexture(GL_TEXTURE_2D, texture1) 绑定第一张纹理。 纹理 ID、program ID 也常被外挂拿来辅助区分对象类别。
  2. glDrawArrays(GL_TRIANGLES, 0, 36) 实际发出绘制命令。 攻击思路也相通,都是在 draw call 边界读上下文。