吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 11628|回复: 103
上一主题 下一主题
收起左侧

[原创] 某三字游戏公司启动器的简单分析+过程

  [复制链接]
跳转到指定楼层
楼主
VisionTravel 发表于 2025-3-27 22:49 回帖奖励
本帖最后由 VisionTravel 于 2025-4-1 22:55 编辑

某三字游戏公司启动器的简单分析+过程

简单分析HYP启动器,以方便后续的逆向分析。 20250401:添加简单前端分析。HYP的前端实现的具体代码我不会给出,这里只进行技术讨论。前端实际上也没什么好说的,主要是学习(借鉴)一下实现的方式吧... 我对前端实现没什么兴趣,所以有些部分可能写的比较模糊。


1. 观察启动器和启动器文件寻找分析思路

直接逆向可能会跳到思维惯性的坑里,所以我们可以先来根据启动器和启动器的文件和文件结构来进行推测,方便在逆向的时候更快得出我们想要的代码和实现。

1.1 观察启动器安装目录进行分析

来到启动器的安装位置,直接观察程序文件夹结构和文件进行猜测:

📁 HYP                                 使用CN版演示
    📁 1.4.5.222                       启动器版本文件夹
        📁 bearer                      Qt 网络模块
            📦 qgenericbearer.dll
        📁 ico                         一些游戏图标
            🖼️ bh3_cn.ico
            🖼️ hk4e_cn.ico
            🖼️ hkrpg_cn.ico
            🖼️ nap_cn.ico
        📁 imageformats                Qt 图像处理
            📦 qgif.dll
            📦 qicns.dll
            ...
        📁 platforms                   Qt 平台实现
            📦 qwindows.dll
        📁 resources                   CEF 资源文件夹
            📁 locales
                📄 af.pak
                ...
            📄 chrome_100_percent.pak
            📄 chrome_200_percent.pak
            📄 feapp.pak               启动器的资源文件?
            📄 resources.pak
        📁 styles                      Qt 页面样式
            📦 qwindowsvistastyle.dll
        📦 7z.dll                      7z
        🖥️ 7z.exe                      7z
        ⚙️ app.conf.dat                启动器配置文件
        📦 Astrolabe.dll               未知
        📦 chrome_elf.dll              CEF
        📦 concrt140.dll               MSVC 运行库
        🖥️ crashreport.exe             崩溃日志上报用程序
        📦 d3dcompiler_47.dll          DirectX 11 组件
        📦 HoYoDeviceFpSDK.dll         获取设备指纹SDK
        🖥️ hpatchz.exe                 游戏更新用工具
        📦 HYBase.dll                  基础框架?
        📦 HYCommon.dll                公共工具?
        📦 HYContainer.dll             启动器容器管理?
        📦 HYContainerPlugin.dll       容器管理扩展?
        📦 HYEngine.dll                引擎核心?
        📦 HYGui.dll                   用户页面?
        📦 HYLauncher.dll              启动器核心功能逻辑库?
        📦 HYLauncherPlugin.dll        功能扩展?
        🖥️ HYP.exe                     启动器主程序
        🖥️ HYPHelper.exe               辅助服务程序?
        📦 HYQComm.dll                 未知 网络相关?
        📦 HYSysTrayPlugin.dll         系统图标插件
        📦 HYUiKit.dll                 界面组件?
        🖥️ HYUpdater.exe               启动器自更新程序
        📦 HYUpdaterPlugin.dll         自更新扩展
        📄 icudt71l.dat                ICU
        📄 icudtl.dat                  ICU
        🖥️ launcher.exe                启动器兼容性入口程序
        📦 libcef.dll                  CEF
        📦 libcrypto-1_1-x64.dll       OpenSSL 密码学库
        📦 libEGL.dll                  OpenGL ES 接口库
        📦 libGLESv2.dll               OpenGL ES2.0 兼容层
        📦 libssl-1_1-x64.dll          OpenSSL HTTPS库
        📦 MiHoYoMTRSDK.dll            Mi Trace Reporter SDK?
        📦 msvcp140.dll                MSVC 运行库
        📦 msvcp140_1.dll              MSVC 运行库
        📦 msvcp140_2.dll              MSVC 运行库
        📦 msvcp140_atomic_wait.dll    MSVC 运行库
        📦 msvcp140_codeecvt_ids.dll   MSVC 运行库
        📦 QCefView.dll                CEF相关的库
        📦 Qt5Core.dll                 Qt5
        📦 Qt5Gui.dll                  Qt5
        📦 Qt5Networl.dll              Qt5
        📦 Qt5Widgets.dll              Qt5
        📄 snapshot_blob.bin           CEF
        📦 sophon.dll                  未知
        📦 telemetry.dll               数据统计上报
        📄 v8_context_snapshot.bin     CEF
        📦 vccorlib140.dll             MSVC 运行库
        📦 vcruntime140.dll            MSVC 运行库
        📦 vcruntime140_1.dll          MSVC 运行库
        📦 vk_swiftshader.dll          Vulkan 兼容层
        ⚙️ vk_swiftshader_icd.json     驱动配置文件
        📦 vulkan-1.dll                Vulcan
        📦 xyvodsdk.dll                视频相关SDK?
📁 games                               游戏默认安装目录
    📁 GenShen
        ...
    📁 Honsan
        ...
    📁 Hontie
        ...
    📁 Zero
        ...
⚙️ config.ini                          全局配置文件?
🖥️ launcher.exe                        启动器兼容性入口程序
🖥️ uninstall.exe                       卸载
🖥️ vc_redist.x64.exe                   MSVC 运行库安装程序

根据一些经验,我们可以很猜测HYP使用了Qt5作为基础框架,并使用CEF作为前端网页框架,并且使用.dll实现模块化,使用插件实现功能解耦。
不过存在一些尚未清楚用途和功能的库,这时候我们可以利用搜索引擎直接搜索来推测大概的用途:

📦 sophon.dll    某环智能分析工具
📦 xyvodsdk.dll  某域云视频SDK
📦 QCefView.dll  基于QWidget集成CEF的Web View 组件

其中 Astrolabe.dll 的功能当时还不知道,不过在查看启动器内有什么线索时,我在点击启动器的设置-关于页面内的查看客户端日志选项后发现打开的文件夹路径为C:\Users\VTS\AppData\Roaming\miHoYo\HYP\1_1\modules\astrolabe\Log,所以推测Astrolabe.dll是日志记录工具。
如果不熟悉其他的一些库(如telemetry.dll),最好是先使用浏览器搜索一下,实在不清楚的只能逆向分析来看了。

还存在一个似乎是HYP的资源文件的文件feapp.pak,推测是一个压缩文件,使用hashcat验证后确实是一个PKZIP压缩文件,并且有密码,暴力破解还是太吃时间了,所以看能不能在后续的逆向分析中找到密码。
我们已知QCefView是一个基于QWidget集成CEF的Web View 组件,并且feapp.pak的位置在CEF资源文件夹下,所以直接去看QCefView的官方文档有什么线索:添加本地Zip文件到URL的映射
果然还是有线索的,从QCefView的官方文档得知这个文件应该是启动器所需的WebApp资源,应该是前端实现相关的资源文件,不过暂时不知道如何解压查看,所以暂时先跳过。

得出结论,HYP采用Qt5作为基础框架,前端通过QCefView加载并运行本地打包的WebApp资源,使用.dll实现模块化开发,使用插件实现功能解耦,并使用sophon.dll作为智能分析工具,使用xyvodsdk.dll作为云视频SDK,使用Astrolabe.dll作为日志记录工具。


1.2 根据启动器的配置和数据存储猜测一些便于逆向分析的思路

前面我在启动器设置-页面中的查看客户端日志得知了启动器存放配置和数据的路径,路径为C:\Users\VTS\AppData\Roaming\miHoYo\HYP\1_1,所以尝试在这个路径下的文件夹中寻找一些便于逆向分析的思路和方式。

📁 cache
📁 crash
📁 data
📁 fedata
📁 fepak
📁 logs
📁 modules

查看各个文件夹后,筛选出应该有关键信息的文件夹:

📁 data
📁 fedata
📁 modules
1.2.1 data文件夹
📁 data
    📄 appstore.dat
    📄 appstore.dat.crc
    📄 gamedata.dat
    📄 gamedata.dat.crc
    📄 notifications.dat
    📄 notifications.dat.crc
    📄 usersettings.dat
    📄 usersettings.dat.crc

只看文件命名,这些文件都是启动器的配置文件+每个配置文件的校验文件,而且可以知道这里没有什么我们需要的信息,所以我们可以跳过这个文件夹。

1.2.2 fedata文件夹
📁 fedata
    📁 blob_storage
    📁 Cache
    📁 Code Cache
    📁 databases
    📁 GPUCache
    📁 IndexedDB
    📁 Local Storage
    📁 Network
    📁 Sevice Worker
    📁 Session Storage
    📁 WebStorage
    📄 DevToolsActivePort
    📄 LocalPrefs.json
    📄 Visited Links

这结构就非常明显就是浏览器的数据存储文件夹,也就是启动器的WebApp使用的浏览器数据存储文件夹,并且看到了DevToolsActivePort这个文件,且我们已知HYP使用了QCefView作为前端网页框架,所以这个文件应该是QCefView的调试端口文件,也就代表有办法直接调试前端的WebApp了。

1.2.3 moudles文件夹
📁 modules
    📁 astrolabe
    📁 sophon
    📁 telemetry

观察这些文件夹下的内容,可以印证之前的推测,不过具体是干什么用的还是需要逆向分析。

到这里我们已经猜测出了一些便于逆向分析的思路,但是这些思路都是基于启动器的文件结构和文件名等精细猜测,所以下一步便是尝试逆向分析这些文件,看看有没有什么线索。

2. 简单逆向分析

2.1 确认启动器WebApp使用的浏览器的调试端口并启用调试

直接打开,确认端口为9222,且使用的是QCefView作为前端网页框架,那就可以使用DnSpy直接调试启动器主程序并添加参数-remote-debugging-port=9222
启动程序后,在浏览器中打开localhost:9222,就可以看到启动器的调试页面了。

" alt="" target="_blank" rel="noopener noreferrer nofollow" />

这个时候实际上已经可以不用关注之前发现有密码的feapp.dat资源文件,直接去DevTools中查看即可。
具体的调试就看各路大神发挥了,后续我也会写一篇关于前端实现的分析。

2.2 分析app.conf.dat

解密app.conf.dat的方法已在我的第一个帖子获取HYP(XX游启动器)的基本信息中详细解释思路和解密代码,这里就不再赘述了。

可以看到app.conf.dat的可读字段结构:(隐藏的字段可以自己去解密看)

{
  "_meta_ver": "1.4.5.222",
  "App": {
    "Region": "cn",
    "Language": "zh-cn",
    "Company": "miHoMo",
    "Product": "HYP",
    "ProductDisplayName": "\u7c73\u54c8\u6e38\u542f\u52a8\u5668",
    "Standalone": false,
    "Channel": "1",
    "SubChannel": "1",
    "LauncherId": "jGHBHlcOq1",
    "CPS": "hyp_mihoyo",
    "UAPC": "",
    "PrimaryGame": "hyp_cn",
    "ClientEnv": "production",
    "ClientPreview": false,
    "EnableBetaLogin": false,
    "WebApiBaseUrl": "https://hyp-api.mihomo.com/hyp/hyp-connect",
    "ThirdPartyLogin": null,
    "StartMenuDirName": "\u7c73\u54c8\u6e38\u542f\u52a8\u5668"
  },
  "FE": {
    "BridgeName": "HoYoPlayClient",
    "PackageKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  "Launcher": {
    "LogDebugMode": false,
    "PlatApp": "ddxf5qt290cg",
    "Win7PatchUrl": "https://launcher-webstatic.mihomo.com/launcher-public/2023/06/08/fac986b82c31f75c0820803748a74af4_4452073143311314458.zip",
    "Win7PatchMd5": "fac986b82c31f75c0820803748a74af4",
    "Win7PatchEnable": true,
    "LoginMode": 3,
    "EnableMultiAccount": false
  },
  "ABTest": {
    "Url": "https://data-abtest-api.mihomo.com/data_abtest_api/config/experiment/list"
  },
  "Box": {
    "Url": "https://sdk-static.mihomo.com/combo/box/api/config/plat-launcher/plat-launcher"
  },
  "H5Log": {
    "Url": "https://h5log-api-dualstack.mihomo.com/common/h5log/log/batch?topic=h5log-plat-launcher"
  },
  "DataUpload": {
    "Url": "https://sdk-log-upload.mihomo.com/sdk/dataUpload"
  },
  "APM": {
    "AppId": "plat_Windows_xxxxxxxxxx",
    "AppKey": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  "Updater": {
    "AppId": "xxxxxxxxxx",
    "AppKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "ReportUrl": "https://api-takumi.mihomo.com/ptolemaios_api/api/reportStrategyData"
  },
  "Epic": {
    "EnablePay": false,
    "ProductId": "",
    "SandboxId": "",
    "DeploymentId": "",
    "CredentialsId": "",
    "CredentialsSecret": ""
  }
}

app.conf.dat主要还是配置启动器的各类参数:(这里只写出一部分字段)

基础配置(App):

字段 作用
Region 运行区域
ProductDisplayName 启动器显示的名称
EnableBetaLogin 是否启用登录测试
PrimaryGame 运行区域
LauncherId 启动器ID
WebApiBaseUrl 后端API地址

前端配置(FE):

字段 作用
BridgeName QCefView桥接对象名称
PackageKey 前端资源包密钥

有了PackageKey,就可以通过解密feapp.dat来获取前端资源包。

2.3 分析feapp.dat

有密码了直接解压就好了,看了一下确实是前端的实现和一些资源文件,前端实现会单独写一篇,这里就不再赘述了。

2.4 分析 Qt5 、CEF 和 QCefView 使用的版本

2.4.1 Qt5版本

Qt5的版本很容易知道,直接拖到Binary Ninja中搜索字符串5.就行了,得知Qt5的版本为Qt 5.15.17 (x86_64-little_endian-llp64 shared (dynamic) release build; by MSVC 2022),这个版本时Qt的商用版。

2.4.2 CEF版本

CEF的版本相对困难,不过已经知道使用了开源的QCefView库,那么可以直接去看源码,不过我发现QCefView的协议是LGPL-2.1,所以某三字游戏公司应该是没有修改关于QCefView的源码,所以可以直接编译一份,然后将编译后生成的库文件中搜索编译时使用的CEF版本的字符串,应该能找到CEF的版本会在什么地方。
不过编译完后,发现文件结构和HYP的不一样,估计是之前版本呢的QCefView,不过不影响。编译后生成的文件有一个程序叫CefViewWing.exe,不过没有在HYP的程序安装目录发现这个程序,同时QCefView的协议是LGPL-2.1,所以继续查看QCefView的源码。
在QCefView的解决方案中找到了CefViewWing这个项目,右键打开文件夹所在位置,向上两级找到了CefViiewCore的源码,CefViewCore的协议是MIT,所以HYP大概是修改了CefViewWing.exe的实现,或者直接修改了CefViewCore的实现,不过仍不知道CefViewWing.exe具体是哪个程序,所以想到了一个简单的办法:运行QCefViewTest.exe,看看除了QCefviewTest.exe之外CefViewWing.exe有没有被运行,如果运行了,那么在运行HYP时,除了主程序外的程序就是要找的""CefViewWing.exe"

为什么要找这个程序呢?因为这个程序是连接Web端和Qt端的关键桥梁,且是CEF浏览器的宿主。

测试后发现,HYPHelper.exe实现了"CefViewWing.exe"的功能。直接使用Binary Ninja打开自己本地编译的CefViewWing.exe,并搜索CEF的版本号,找到D:\\QCefView-main\\.build\\windows.x86_64\\_deps\\cefviewcore-src\\dep\\cef_binary_126.2.18+g3647d39+chromium-126.0.6478.183_windows64\\libcef_dll/ctocpp/ctocpp_ref_counted.h,提取关键字符串ctocpp_ref_counted并在HYPHelper.exe的字符串内搜索,确认了CEF的版本为cef_binary_102.0.10+gf249b2e+chromium-102.0.5005.115_windows64

2.4.3 QCefView版本

具体的版本很难确定,不过可以通过对比最新的QCefView的编译后的QCefView.dll中函数的区别大概判断一下:

最新提交的QCefView的JS调用C++的函数名为invoke,不过在HYP所使用的QCefView中的函数名为invokeMethod,查看历史提交后,确定HYP所使用的QCefView的版本一定在2024年12月之前。继续翻看历史提交,得知在2023年5月后QCefView添加了devtools方法,而HYP所使用的QCefView是有devtools方法的,所以HYP所使用的QCefView版本一定在2023年5月之后。这个启动器公布似乎是在2024年七月左右,算上开发时间,可能是2024年年初或者2023年年末的某个版本。

3. 前端分析

3.1 可能的打包方式、项目配置和使用的库

3.1.1 打包方式

我们先看看打包后的main.js文件

( () => {
    var e, a = {
        00000: (e, a, t) => {
            "use strict";
            var n = t(00000)
              , i = t.n(n)
              , l = t(00000)
              , o = t(00000)
              , s = t(00000);
            const r = "production"
              , _ = "cn"
              , c = "zh-cn"
              , u = [{
                label: "简体中文",
                value: "zh-cn"
            }]
...... 

//# sourceMappingURL=main_1_4_5_cn_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.js.map

观察构建后的main.js文件,很明显是由是 Webpack 打包的,模块化加载函数 t(00000) 对应 Webpack 的__webpack_require__,且数字模块ID(如11451、19810)是 Webpack 默认的模块标识方式。在js末尾也存在//# sourceURL=xxx注释。

3.1.2 使用的库

vendor.js.LICENSE.txt写出了一部分第三方库,可以得出使用了:

    1. vue相关库
      • Vue
      • Vue Roter
      • Vue Core
      • Vue Shared
      • Vue Runtime-DOM
    1. 常用工具库
      • lodash
      • asn1.js
      • JavaScript Cookie
    1. 其他工具库
      • Element Plus Icons
      • buffer
      • is-buffer
      • CryptoJS(通过 Cédric Mesnil 和 Jan Hruby 判断)

不过这个协议文件没有写全所使用的第三方库,我们先把容易找的找出来,直接搜索©https://字符串,找到了core-jsElement PlusPinia,根据经验也找到了Axios(因为存在网络请求相关实现)和babel

剩下的就是通过源码来判断了。不过 Webpack 打包后的变量名函数名模块ID都是随机生成的,而且HYP可能使用了 Tree Shaking 来优化代码并导致完整结构缺失,并通过optimization.splitChunks合并模块到单个作用域, 且 Webapck 压缩和混淆导致可读性极低,看了vendors.js后我认为有些库是按需导入,而按需导入会让代码以模块形式分散,找到可能没被找到的库超级麻烦,我也就懒得看了,后续如果要还原功能实现再说了...

3.1.3 基本项目配置

查看两个js的协议文件,可以发现在vendors.js.LICENSE中写了用到的第三方库的协议,而main.js.LICENSE中则没有。我们基本可以确定HYP的 Webpack 使用了配置项 optimization.splitChunks 来分离外部库和这个项目业务代码。并且重新指定了主入口,使用 output.filename 来指定打包后的文件命名。

3.1.4 创建一个Vue3项目以便于推测HYP的打包与构建配置

使用 Vue-cli 创建一个 Vue3 项目(我估计HYP不是使用Vue-cli创建的,所以这里先用Vue-cli创建一个Vue3项目,然后修改配置,然后尝试还原HYP的配置,这里不给出详细步骤了,试出来的):

npm install -g @vue/cli
vue init webpack hypreverse
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 3.x
? Use history mode for router? (Requires proper server setup for index fallback in production) No
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files

添加已知的第三方库:

npm i lodash asn1js js-cookie element-plus pinia axios core-js crypto-js buffer is-buffer @element-plus/icons-vue

简单在main.js导入第三方库:

import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);

import { createPinia } from "pinia";
const pinia = createPinia();
app.use(pinia);

import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
app.use(ElementPlus);

import * as ElementPlusIconsVue from "@element-plus/icons-vue";
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}

import router from "./router";
app.use(router).mount("#root");

修改vue.config.js,依照HYP构建后的文件结构和文件内容并参考webpack官方文档编写,以尝试还原项目配置
HYP的构建文件结构(WebApp文件夹):

📁 WebAppFolder
    📁 images
        📁...
    🖼️ xxxxxxxxxxxxxxxxxxxx.png
    🖼️ xxxxxxxxxxxxxxxxxxxx.webp
    📄 index.html
    📄 main_1_4_5_cn_xxxxxxxxxxxxxxxxxxxx.js
    📄 main_1_4_5_cn_xxxxxxxxxxxxxxxxxxxx.js.LICENSE.txt
    📄 main_1_4_5_xxxxxxxxxxxxxxxxxxxx.css
    📄 vendors_1_4_5_xxxxxxxxxxxxxxxxxxxx.css
    📄 vendors_1_4_5_cn_xxxxxxxxxxxxxxxxxxxx.js
    📄 vendors_1_4_5_cn_xxxxxxxxxxxxxxxxxxxx.js.LICENSE.txt

vue.config.js

const { defineConfig } = require("@vue/cli-service");
const packageJson = require("./package.json");

// HYP的main_0_0_0_'cn'是区分启动器类型的字段,这里使用环境变量来代表
const version = packageJson.version.replace(/[^a-zA-Z0-9]/g, "_");
const env = process.env.VUE_APP_ENV || process.env.NODE_ENV;
const envSuffix = env ? `_${env.replace(/[^a-zA-Z0-9]/g, "_")}` : "";

module.exports = defineConfig({
  publicPath: "./",
  filenameHashing: true,

  // hyp的vendors.js
  configureWebpack: {
    output: {
      chunkLoadingGlobal: "webpackChunk_vts_hyp_reverse_fe_app",
      globalObject: "globalThis",
    },
  },

  // Webpack链式配置
  chainWebpack: (config) => {
    config.entryPoints.delete("app"); // 删除默认的app入口
    config.entry("main").clear().add("./src/main.js"); // 添加自定义入口main

    config.optimization
      .splitChunks({
        cacheGroups: {
          vendors: {
            test: /[\\/]node_modules[\\/]/,
            name: "vendor",
            chunks: "all",
            priority: 20,
            enforce: true,
          },
        },
      })
      .when(config.optimization.get("splitChunks"), (optimization) => {
        optimization.delete("default");
      });

    // 配置输出文件名格式
    config.output
      .filename(`[name]_${version}${envSuffix}_[contenthash:20].js`)
      .chunkFilename(`[name]_${version}${envSuffix}_[contenthash:20].js`);

    // 禁用runtimeChunk,保持入口文件独立
    config.optimization.runtimeChunk(false);

    // 配置CSS提取规则
    config.plugin("extract-css").tap(() => [
      {
        filename: `[name]_${version}_[contenthash:20].css`,
        chunkFilename: `[name]_${version}_[contenthash:20].css`,
      },
    ]);

    // 协议提取
    config.optimization.minimizer("terser").tap((args) => {
      args[0].extractComments = true;
      return args;
    });
  },
});

运行npm run build,查看./dist

📁dist
    📄 index.html
    🖼️ favicon.ico
    📄 app_0_1_0_production_xxxxxxxxxxxxxxxxxxxx.js
    📄 app_1_4_5_xxxxxxxxxxxxxxxxxxxx.css
    📄 vendors_0_1_0_production_xxxxxxxxxxxxxxxxxxxx.css
    📄 vendors_0_1_0_production_xxxxxxxxxxxxxxxxxxxx.js
    📄 vendors_0_1_0_production_xxxxxxxxxxxxxxxxxxxx.js.LICENSE.txt
    没有app_0_1_0_production_xxxxxxxxxxxxxxxxxxxx.js.LICENSE.txt是因为业务代码内没有相关协议存在

查看项目结构和内容后,可以确定基本还原了HYP的项目配置,不过我没怎么学过前端,希望有大佬帮忙看看,欢迎留言。

3.1.4.1 项目配置总结:

猜测的项目配置,肯定有遗漏...

  • 基础框架&第三方库&依赖:

    • vue3
    • vue vouter
    • pinia
    • axios
    • element plus
    • Lodash
    • js-cookie
    • crypto-js
    • buffer
    • is-buffer
    • @element-plus/icons-vue
    • asn1js
    • core-js
  • 打包与构建配置

    • 修改输出的文件名,为文件名添加版本号、启动器类型的标识
    • 分割代码,将第三方库打包到vendor
    • 禁用runtimeChunk,保持入口文件独立
    • 提取第三方库等依赖的协议到单独文件
    • 分离CSS,将CSS提取到单独的文件
    • 自定义webpack的chunk加载全局变量名
    • 设置全局对象为globalThis(为了兼容CEF和Node环境?)

3.2 主界面分析

" alt="主页面" target="_blank" rel="noopener noreferrer nofollow" />

在devtools中获取类名,直接去源码搜索即可得出想要的实现
比如我们想知道三字游戏公司是如何实现游戏栏内游戏图标可拖动的,我们可以直接搜索xxx-side-bar

(e, t) => {
    const n = Z_
        , i = Y_
        , o = cn
        , s = F_
        , r = H_;
    return (0,
    Ya.wg)(),
    (0,
    Ya.iD)(Ya.HY, null, [(0,
    Ya._)("div", {
        class: "xxx-side-bar",
        ref_key: "sideBarRef",
        ref: m
    }, [(0,
    Ya._)("div", {
        class: "xxx-side-bar__top",
        onMousedown: t[0] || (t[0] = (...e) => (0,
        T.SU)(y) && (0,
        T.SU)(y)(...e))
    }, [(0,
    Ya._)("div", {
        class: (0,
        un.normalizeClass)(["logo", {
            "sub-logo": (0,
            T.SU)(false)
        }])
    }, null, 2)], 32), (0,
    Ya.wy)((0,
    Ya._)("div", {
        ref_key: "sideBarBottomRef",
        ref: p,
        class: "xxx-side-bar__bottom"
    }, [(0,
    Ya.Wm)(i, {
        class: "game-list",
        items: (0,
        T.SU)(h),
        "container-el": (0,
        T.SU)(m),
        "padding-bottom": 99,
        "drag-padding-top": 88,
        "drag-padding-bottom": 93,
        "item-size": 36,
        "gap-size": 20,
        "item-key": "gameBiz",
        onDrag: f
    }, {
        default: (0,
        Ya.w5)(( ({item: e, isDragging: t, isUnderDrag: i}) => [(0,
        Ya.Wm)(n, (0,
        Ya.dG)({
            "under-drag": i,
            "disable-hover": t,
            "disable-click": t
        }, e, {
            onClick: t => (e => {
                a.replace({
                    name: "launcher",
                    params: {
                        gameBiz: e
                    },
                    query: {
                        source: Sa.SIDEBAR
                    }
                })
            }
            )(e.gameBiz)
        }), null, 16, ["under-drag", "disable-hover", "disable-click", "onClick"])])),
        _: 1
    }, 8, ["items", "container-el"]), q_, (0,
    Ya.Wm)(s, {
        visible: (0,
        T.SU)(c)
    }, {
        default: (0,
        Ya.w5)(( () => [(0,
        Ya.Wm)(o, {
            content: (0,
            T.SU)(V)("side_bar_all_games"),
            placement: "right",
            enterable: !1,
            "popper-options": {
                modifiers: [{
                    name: "offset",
                    options: {
                        offset: [0, 20]
                    }
                }]
            }
        }, {
            default: (0,
            Ya.w5)(( () => [(0,
            Ya._)("div", {
                class: (0,
                un.normalizeClass)(["add-game-button", {
                    selected: (0,
                    T.SU)(_)
                }]),
                onClick: S
            }, null, 2)])),
            _: 1
        }, 8, ["content"])])),
        _: 1
    }, 8, ["visible"])], 512), [[l.vShow, (0,
    T.SU)(E)]])], 512), (0,
    T.SU)(v) ? ((0,
    Ya.wg)(),
    (0,
    Ya.j4)(r, {
        key: 0,
        onClosed: I
    })) : (0,
    Ya.kq)("v-if", !0)], 64)
}

简单尝试还原:

<template>
  <div
    class="xxx-side-bar"
    ref="sideBarRef"
    v-show="isSideBarVisible"
  >
    <div
      class="xxx-side-bar__top"
      @mousedown="handleDragStart"
    >
      <div :class="['logo', { 'sub-logo': !isMainLogo }]" />
    </div>

    <div
      ref="sideBarBottomRef"
      class="xxx-side-bar__bottom"
    >
      <VirtualList
        class="game-list"
        :items="games"
        :container-el="sideBarRef"
        :padding-bottom="99"
        :drag-padding-top="88"
        :drag-padding-bottom="93"
        :item-size="36"
        :gap-size="20"
        item-key="gameBiz"
        @drag="handleDragGame"
      >
        <template #default="{ item: game, isDragging, isUnderDrag }">
          <GameItem
            :class="{ 
              'under-drag': isUnderDrag,
              'disable-hover': isDragging,
              'disable-click': isDragging 
            }"
            v-bind="game"
            @click="switchGame(game.gameBiz)"
          />
        </template>
      </VirtualList>

      <DividerComponent />

      <TooltipWrapper :visible="showTooltip">
        <Tooltip
          :content="$t('side_bar_all_games')"
          placement="right"
          :enterable="false"
          :popper-options="{
            modifiers: [{ 
              name: 'offset', 
              options: { offset: [0, 20] } 
            }]
          }"
        >
          <div
            class="add-game-button"
            :class="{ selected: isAddButtonActive }"
            @click="openGameSelector"
          />
        </Tooltip>
      </TooltipWrapper>
    </div>

    <GameModal
      v-if="showGameModal"
      @close="closeModal"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import VirtualList from '@/?/VirtualList.vue'
import GameItem from '@/?/GameItem.vue'
import TooltipWrapper from '@/?/TooltipWrapper.vue'
import Tooltip from '@/?/Tooltip.vue'
import GameModal from '@/?/GameModal.vue'
import DividerComponent from '@/?/DividerComponent.vue'

const router = useRouter()

const sideBarRef = ref(null)
const sideBarBottomRef = ref(null)
const isMainLogo = ref(true)
const games = ref([])
const showTooltip = ref(false)
const isAddButtonActive = ref(false)
const showGameModal = ref(false)
const isSideBarVisible = ref(true)

const handleDragStart = (e) => {
  // 处理侧边栏拖拽开始
}

const switchGame = (gameId) => {
  router.replace({
    name: 'launcher',
    params: { gameBiz: gameId },
    query: { source: 'SIDEBAR' }
  })
}

const handleDragGame = (event) => {
  // 处理游戏项拖拽
}

const openGameSelector = () => {
  isAddButtonActive.value = !isAddButtonActive.value
  showTooltip.value = true
}

const closeModal = () => {
  showGameModal.value = false
}
</script>

抛砖引玉,我的目的不是完整还原HYP的实现,而是想学习大厂的启动器前端实现使用了什么技术,所以我这里不继续还原下去,仅给出一个参考例子。


3.3 资源获取分析

在devtools转到应用一栏,查看CEF本地存储,发现图片等资源不是以本地存储的方式存储的,而是以url存储的,所以肯定是从什么地方获取的,所以可以通过查看devtools的network面板,查看请求的url,然后通过url获取资源。

直接在业务代码中搜索对应的getGameContent之类的字符串,找到了HYP封装请求的客户端(我只给出一些还原完的代码片段,完整实现请自行逆向查看):

......
const headers = {
      "x-rpc-device_id": encodeURIComponent(clientInfo?.deviceId || ""),
      "x-rpc-device_fp": encodeURIComponent(clientInfo?.deviceFp || ""),
      "x-rpc-client_version": encodeURIComponent(
        clientInfo?.launcherVersion || ""
      ),
      "x-rpc-preview": !!clientInfo?.clientPreview,
      ...options.headers,
    };
const response = await axiosInstance.request({
      url: fullPath,
      method: requestConfig.method,
      withCredentials: true,
      data: requestConfig.data,
      signal: shouldRetry
        ? abortControllers[requestConfig.requestFunctionName].signal
        : undefined,
      headers,
      params: options.params,
      timeout: options.timeout,
    });
......
getGameContent: {
    devUrl: devBaseURL,
    prodUrl: prodBaseURL,
    path: "/api/getGameContent",
    method: axios.Methods.GET,
    requestHeaders: {},
    requestBodyType: axios.BodyTypes.query,
    responseBodyType: axios.ResponseTypes.json,
    dataKey,
    paramNames: [],
    queryNames: ["game_id", "language", "launcher_id"],
    requestDataOptional: false,
    requestDataJsonSchema: {},
    responseDataJsonSchema: {},
    requestFunctionName: "getGameContent",
    queryStringArrayFormat: axios.QueryFormats.brackets,
    extraInfo: {},
  },

所有的资源请求都来自这个http客户端,且存在充实、缓存等功能
请求域名/xxx/xxx-connect/api/类型?启动器ID&游戏ID&语言,GET,不限制请求头,请求体为空,返回json格式数据


3.4 三字游戏公司自制库

查看未格式化的vendors.js时,看到了类似// EXTERNAL MODULE: external "xxxxxx-h5log"的注释,找到了几个库:

  • hy-logger    日志输出相关库
  • hy-login     登录相关库
  • hy-analysis  统计相关库
  • hy-i18n      国际化相关库
  • hy-?         似乎是一些h5活动所需要的库

3.5 其他分析

某三字游戏公司使用和修改了很多element-plus的样式,以适配设计需求

4. 总结

  • 技术框架

    • 后端框架: Qt5.15.17商业版
    • 前端框架: CEF 102.0.10+gf249b2e+chromium-102.0.5005.115
    • 前端集成: QCefView 未知版本
    • 编译方式: Jenkins + MSVC
    • 开发平台: GitLab
    • 前端框架: Vue3
    • 前端构建工具: webpack5

使用Qt+QCefView实现启动器,避免了使用QtWebEngine(QtWebEngine bug真的多),方便开发和调试;使用Vue3组合式API,方便开发,使用webpack5构建前端代码,并自定义输出结构,优化性能。

  • 安全机制(没什么用)

    • 加密配置文件app.conf.dat
    • 加密WebApp静态资源feapp.dat
    • 配置文件(appstore.dat等)带有CRC校验
    • 一些自制库添加混淆
  • 逆向的突破口

    • 添加启动参数-remote-debugging-port=9222HYP.exe直接使用CEF调试端口调试前端实现
    • 没有混淆关键的库
    • 前端实现基本没有混淆

5. 后记

写这个帖子的时候地震了给我吓一跳...

  • 现在HYP还没有实现启动器内登录,不过在app.conf.dat下看到了EnableBetaLogin字段,这个字段是false,所以HYP目前可能在测试版本已经有登录了,而且还存在EnableMultiAccount字段,所以后续会有多账号登录吧...
  • 后面可能会先研究HYP的前端实现,后端就简单很多了,直接读伪代码就可以了,如果有需要我也会写一个详细的帖子来分析后端实现。
  • 这个帖子我写的可能有点乱或者有些地方没有解释清楚,如果有问题,欢迎留言,我会尽量及时回复。
  • 这三字游戏公司把js用到的一些库的协议放在了加密的feapp.dat中......

202504012240更新:

主要也就是项目配置那一块有点能写的东西,后面实际上就看每个人想学习什么了...
下一个帖子会分析一些HYP的算法,比如x-rpc-device_id的获取之类的,这个帖子比较水...
这个前端现阶段没有什么可动态调试的,只是个启动器...
马上期中考了,没什么时间写帖子和回复,可能下一篇帖子不会出来的太快... 不过有问题直接问就行,不涉及求破、询问是哪个公司的启动器这种的我一般会回复

免费评分

参与人数 32威望 +2 吾爱币 +125 热心值 +30 收起 理由
Q1935991986 + 1 我很赞同!
笙若 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
yemaozi521 + 1 + 1 用心讨论,共获提升!
fvv123456 + 1 + 1 我很赞同!
allspark + 1 + 1 用心讨论,共获提升!
Ignorantlu + 1 + 1 谢谢@Thanks!
Issacclark1 + 1 谢谢@Thanks!
qingne0130 + 1 + 1 我很赞同!
lsh7d1 + 1 + 1 用心讨论,共获提升!
zydh + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
RIKKIA + 1 + 1 热心回复!
TADan + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
wdj500 + 1 + 1 热心回复!
yixi + 1 + 1 谢谢@Thanks!
52pojieyangyi + 1 + 1 热心回复!
ThisIsMC0 + 1 + 1 用心讨论,共获提升!
withoutpower + 1 用心讨论,共获提升!
shenbing + 1 + 1 我很赞同!
弑者 + 1 + 1 热心回复!
120305 + 1 + 1 谢谢@Thanks!
lwGoodChinese + 1 用心讨论,共获提升!
lin5789 + 1 我很赞同!
BrutusScipio + 1 + 1 用心讨论,共获提升!
jk998 + 1 + 1 我很赞同!
130x + 1 热心回复!
zzage + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Tomyi + 1 我很赞同!
xiaokeaihia + 1 热心回复!
AnObsidianDrake + 1 + 1 我很赞同!
whitehack + 1 + 1 谢谢@Thanks!
ioyr5995 + 1 + 1 热心回复!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
 楼主| VisionTravel 发表于 2025-3-29 00:24 |楼主
BrutusScipio 发表于 2025-3-28 23:45
技术栈不是U3D吗?启动器怎么用qt,web前端万用electron

1、Electron对前端开发者确实更友好,但是在调用系统的原生API复杂度很高,跨平台实现时需要处理不同操作系统的差异,且这个启动器会有频繁的系统级操作,这个时候Electron的缺点就体现出来了。安全也是个问题。在开发这种启动器时Electron的缺点能找出很多,我不一一列举。
2、使用Qt可能是注重性能和跨平台支持,并且结合使用QCefView后可以使用Web前端技术来开发应用的UI,同时保持使用Native的方式编写核心业务/功能逻辑。(来自QCefView的官方文档)
3、至于技术栈得看是哪方面的团队和职位啊...这个是启动器不是游戏本体...
沙发
超逸绝尘 发表于 2025-3-28 01:27
3#
jiemax 发表于 2025-3-28 07:58
厉害,感谢分享                              
4#
han163426 发表于 2025-3-28 08:07
我嘞个崩坏3
5#
taidan 发表于 2025-3-28 08:55
太牛了 万分感谢 学习到了
6#
bokewangyu 发表于 2025-3-28 08:56
万分感谢 学习到了
7#
myownsword 发表于 2025-3-28 08:57
萌新学习一下 现在还看不太懂
8#
zhiuan 发表于 2025-3-28 09:09
十分感谢,学到了
9#
eniaclyl 发表于 2025-3-28 09:09
感谢分享
10#
jxkdg 发表于 2025-3-28 09:38
西山居|mihoyo??
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-4-15 08:27

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表