HANA5 游戏逆向

news2024/9/24 3:28:59

前言

某著名百合R18游戏
以前尝试逆过一次,半途而废了。今天想起来再逆一下,记录下逆向的过程。

游戏文件结构:
在这里插入图片描述

游戏资源extract

主要目标是弄明白游戏资源:SE、CG这些怎么加载解密的。

还是像万华镜那样下三个API断点:

在这里插入图片描述

以SE为例分析结构:

0~8:签名

0~54:固定结构的头部

剩下读的A0刚好读到这里:在这里插入图片描述

最后的65读到这里:

在这里插入图片描述

0~7:签名

0~53:固定长度头部

54~END1:

END1~END2:文件名,长度应该是来自0C处的值。

再看看MSD结构分析下:

0~7:签名

0~53:固定长度头部

54~END1:

END1~END2:文件名,长度来自0C 0D的WORD值。

08 09处的WORD指示的是END2结束后下一部分开始的位置。

先跟踪下DATA的读取调用,在sub_43C330(ReadHeader)

跟一跟sub_43C1A0看到对三种版本进行了判断:

在这里插入图片描述

HANA5对应的都是FJSYS,所以return 4。

可以看到这里就有0x54这个值(固定头部长度)出现了:

在这里插入图片描述

后面两段内容的读取就在这里:

在这里插入图片描述

调试了下,除了那个*(_DWORD *)(this+44)!=4不明所以外,其它就是原先想的那样,读取了后面两部分的内容。

掠过后面几个文件读取后,在这里发现了异样:

在这里插入图片描述

OP.MPG,但我搜索本地却没有这个文件🤔。内存中的也能这么读?(不行吧)

(嘶,
在这里插入图片描述

但是我看没引入CreateFileMapping啊。。。)

对应在这个函数sub_442BC0(CVideoPlayer::Play)

好吧,这里读取是失败了的。。。(突然想到可以自己放一个OP.MPG,楽)

在这里插入图片描述

对应的这里应该就是这个ogg:

在这里插入图片描述

可以跟ogg文件对比,格式是一样的:

在这里插入图片描述

对应的是BGM文件的01ED处:

在这里插入图片描述

也就是根本没有压缩,加密。。。服了。。。

提取出来看看。

01ED开始3D7C95字节。

确实能够成功播放:

在这里插入图片描述

逆天,小日子真就不加密的。。。

下面就是找一下对应的offset字段和size字段是在header哪里设置的。

现在回来看就很清楚了:

在这里插入图片描述

写个脚本全部提取出来。

import struct
filename = r"BGM"
with open(filename,"rb+") as f:
    data = f.read()

start1 = 0x50
num = 0
size = []
offset = []
idx = data[start1+4]
while (1):
    size.append(struct.unpack('<I', data[start1+0x8:start1+0xC])[0])
    offset.append(struct.unpack('<I', data[start1+0xC:start1+0x10])[0])
    num += 1
    if(idx+8 != data[start1+4+0x10]):
        break
    start1 += 0x10
    idx = data[start1+4]

print(num)

def extract(offset,size,num):
    output = "M" + str(num).rjust(2,"0") + ".ogg"
    with open(output,"wb+") as f:
        f.write(data[offset:offset+size])
    print(f"[+] {num} : Done!")

for i in range(1,num+1):
    extract(offset[i-1],size[i-1],i)

在这里插入图片描述

在这里插入图片描述

MSD的格式也是类似的:

在这里插入图片描述

只是不知道这MSD是啥。。

像是类似配置文件?

DATA也是一样,只是bmp的size是固定的:

在这里插入图片描述

稍微改改脚本就能提取:

import struct
filename = r"DATA"
with open(filename,"rb+") as f:
    data = f.read()

start1 = 0x50
num = 0
size = []
offset = []
idx = data[start1+4]
while (1):
    size.append(struct.unpack('<I', data[start1+0x8:start1+0xC])[0])
    offset.append(struct.unpack('<I', data[start1+0xC:start1+0x10])[0])
    num += 1
    if num == 7:
        break
    start1 += 0x10

print(num)

def extract(offset,size,num):
    output = "M" + str(num).rjust(2,"0") + ".bmp"
    with open(output,"wb+") as f:
        f.write(data[offset:offset+size])
    print(f"[+] {num} : Done!")

for i in range(1,num+1):
    extract(offset[i-1],size[i-1],i)

在这里插入图片描述

接下来就提取IMG,结构也是一样的。

只是注意到每个PNG前都有0x60 size的MGD文件(?)

稍微改下脚本:

import struct
filename = r"IMG"
with open(filename,"rb+") as f:
    data = f.read()

start1 = 0x50
num = 0
size = []
offset = []
idx = data[start1+4]
while (1):
    size.append(struct.unpack('<I', data[start1+0x8:start1+0xC])[0])
    offset.append(struct.unpack('<I', data[start1+0xC:start1+0x10])[0])
    num += 1
    start1 += 0x10
    if start1>=0xC50:
        break

print(num)

def extract(offset,size,num):
    output = "M" + str(num).rjust(2,"0") + ".png"
    with open(output,"wb+") as f:
        f.write(data[offset+0x60:offset+size])
    print(f"[+] {num} : Done!")

for i in range(1,num+1):
    extract(offset[i-1],size[i-1],i)

没问题,成功提取:

在这里插入图片描述

SE跟前面的BGM没啥区别,一样的提取:

import struct
filename = r"SE"
with open(filename,"rb+") as f:
    data = f.read()

start1 = 0x50
num = 0
size = []
offset = []
idx = data[start1+4]
while (1):
    size.append(struct.unpack('<I', data[start1+0x8:start1+0xC])[0])
    offset.append(struct.unpack('<I', data[start1+0xC:start1+0x10])[0])
    num += 1
    start1 += 0x10
    if start1>=0xf0:
        break

print(num)

def extract(offset,size,num):
    output = "M" + str(num).rjust(2,"0") + ".ogg"
    with open(output,"wb+") as f:
        f.write(data[offset:offset+size])
    print(f"[+] {num} : Done!")

for i in range(1,num+1):
    extract(offset[i-1],size[i-1],i)

在这里插入图片描述

至此,游戏资源的提取就弄明白了。(其余的格式不清楚,应该是引擎自己解析罢)

游戏进度存储

接下来我还想知道游戏进度是怎么存储的?

也就是这几个文件的结构:

在这里插入图片描述

GAME%d.SAV

打开游戏,点击start,会断在这里:

在这里插入图片描述

sub_415E70函数里

在这里插入图片描述

这里有个MD5校验?

所以每个SAVE开头的0x20就是md5 hash

在这里插入图片描述

调试看这里,很显然MD5两者是不匹配的。

在这里插入图片描述

是一个循环比对MD5,直至找到相同。

(所以感觉不应该点START,应该点LOAD。。。)

再点一遍LOAD,可以发现逻辑:

在这里插入图片描述

先加载第一页的SAV(尝试读取)114,我点了第二页又继续加载1528

在这里插入图片描述

我又重新保存了一个,发现这个MD5检测的可能是TIME,太久的SAVE就会不能加载。。。

紧接着后面有4个CFile::Read

  CFile::Read_sub_43B610(Buffer, 4u);
  CFile::Read_sub_43B610(lpBuffer, 0x40u);
  CFile::Read_sub_43B610(&nNumberOfBytesToRead, 4u);
  CFile::Read_sub_43B610(v17, 8u);

看SAVE文件像是用位图这种来保存每个场景的选择?

在这里插入图片描述

这里做个对比,LOAD 1过后,点击一次再保存。

发现唯一差异点:

在这里插入图片描述

但切换一个场景(图片变了)后,就有很大的差异

在这里插入图片描述

找找之前extract的bg14a,对应第8张

在这里插入图片描述

果然是

在这里插入图片描述

那存储逻辑到这里大概就明白了,还有很多细节没必要逆引擎的解析过程了。

CONFIG.SAV

最后关注一下这个CONFIG.SAV的逻辑

如果是START的话是这种:

在这里插入图片描述

所以我很感兴趣为什么我每点一个场景,切换都要读取一遍CONFIG.SAV?

这里也采取比较的方式。对于两个场景,比较CONFIG.SAV的区别。

注意到全部是1h大小的差异:

在这里插入图片描述

x32调试看看到底读取了CONFIG.SAV的什么?

跟到这里:

在这里插入图片描述

对应CONFIG.SAV的这里:

在这里插入图片描述

后面就是一堆奇奇怪怪的操作:

在这里插入图片描述

这里的v15 += 18也是往后移动72字节。

这里就当在计算一个不知道是什么鬼的值。。。

对于Sxxx这些段计算。

再后面的这里:

在这里插入图片描述

由黄色那句,知道v13往后移动72B,

可以看到这样一个结构刚好72字节:

在这里插入图片描述

关键是这个函数sub_447D10

里面用了SSE指令集,很难弄清楚在干啥。。。

在这里插入图片描述

。。。

但是问问GPT:

这段代码是一个函数 sub_447D10 的实现,功能是将一个 __m128 类型的数组从源地址 a2 复制到目标地址 a1,并且根据条件采用不同的复制方式。它包括了对内存对齐和性能优化的考虑,尤其是针对 SIMD 操作进行了优化。

该函数实现了一个高效的内存复制功能,支持标准和优化路径,根据内存对齐条件选择适当的复制方法。优化路径利用了 SSE 指令集,进行对齐的数据块复制,并采用流式存储和预取优化,旨在提高性能和减少缓存未命中的影响。

牛逼。。

就是复制。。。
GPT注释的代码:

__m128 *__cdecl sub_447D10(__m128 *a1, __m128 *a2, unsigned int a3)
{
  __m128 *result; // eax
  __m128 *v5; // edi
  unsigned int v6; // ecx
  __m128 v7; // xmm1
  __m128 v8; // xmm2
  __m128 v9; // xmm3
  __m128 v10; // xmm4
  __m128 v11; // xmm5
  __m128 v12; // xmm6
  __m128 v13; // xmm7
  unsigned int v14; // ecx
  __m128 *v15; // esi
  __m128 *v16; // edi
  unsigned int v17; // ecx
  unsigned __int64 v18; // mm1
  unsigned __int64 v19; // mm2
  unsigned __int64 v20; // mm3
  unsigned __int64 v21; // mm4
  unsigned __int64 v22; // mm5
  unsigned __int64 v23; // mm6
  unsigned __int64 v24; // mm7
  unsigned int v25; // ecx

  result = a2;

  // Check if special conditions for optimized copy are met
  if ((byte_8347EC & 4) == 0 || a3 < 0x80 || ((unsigned __int8)a2 & 0xF) != 0 || ((unsigned __int8)a1 & 0xF) != 0)
  {
    if ((byte_8347EC & 1) == 0 || a3 < 0x40 || ((unsigned __int8)a2 & 7) != 0 || ((unsigned __int8)a1 & 7) != 0)
    {
      // Standard memory copy if conditions are not met
      result = (__m128 *)a3;
      qmemcpy(a1, a2, a3);
    }
    else
    {
      // Optimized memory copy for 64-byte aligned and properly aligned memory
      v15 = a2;
      v16 = a1;
      v17 = a3 >> 6;
      do
      {
        v18 = v15->m128_u64[1];
        v19 = v15[1].m128_u64[0];
        v20 = v15[1].m128_u64[1];
        v21 = v15[2].m128_u64[0];
        v22 = v15[2].m128_u64[1];
        v23 = v15[3].m128_u64[0];
        v24 = v15[3].m128_u64[1];
        v16->m128_u64[0] = v15->m128_u64[0];
        v16->m128_u64[1] = v18;
        v16[1].m128_u64[0] = v19;
        v16[1].m128_u64[1] = v20;
        v16[2].m128_u64[0] = v21;
        v16[2].m128_u64[1] = v22;
        v16[3].m128_u64[0] = v23;
        v16[3].m128_u64[1] = v24;
        v15 += 4;
        v16 += 4;
        --v17;
      }
      while (v17);

      // Handle any remaining bytes that don't fit into the 64-byte aligned blocks
      v25 = a3 & 0x3F;
      if ((a3 & 0x3F) != 0)
      {
        do
        {
          v16->m128_i8[0] = v15->m128_i8[0];
          v15 = (__m128 *)((char *)v15 + 1);
          v16 = (__m128 *)((char *)v16 + 1);
          --v25;
        }
        while (v25);
      }
      _m_empty(); // Ensure all SSE operations are completed
    }
  }
  else
  {
    // Optimized memory copy with prefetch and streaming stores
    _ESI = a2;
    v5 = a1;
    v6 = a3 >> 7;
    do
    {
      __asm { prefetchnta byte ptr [esi] }
      v7 = _ESI[1];
      v8 = _ESI[2];
      v9 = _ESI[3];
      v10 = _ESI[4];
      v11 = _ESI[5];
      v12 = _ESI[6];
      v13 = _ESI[7];
      _mm_stream_ps(v5->m128_f32, *_ESI);
      _mm_stream_ps(v5[1].m128_f32, v7);
      _mm_stream_ps(v5[2].m128_f32, v8);
      _mm_stream_ps(v5[3].m128_f32, v9);
      _mm_stream_ps(v5[4].m128_f32, v10);
      _mm_stream_ps(v5[5].m128_f32, v11);
      _mm_stream_ps(v5[6].m128_f32, v12);
      _mm_stream_ps(v5[7].m128_f32, v13);
      _ESI += 8;
      v5 += 8;
      --v6;
    }
    while (v6);

    // Handle any remaining bytes that don't fit into the aligned blocks
    v14 = a3 & 0x7F;
    if ((a3 & 0x7F) != 0)
    {
      do
      {
        v5->m128_i8[0] = _ESI->m128_i8[0];
        _ESI = (__m128 *)((char *)_ESI + 1);
        v5 = (__m128 *)((char *)v5 + 1);
        --v14;
      }
      while (v14);
    }
    _m_empty(); // Ensure all SSE operations are completed
    _mm_sfence(); // Ensure all stores are completed
  }
  return result;
}

那么就当作复制来看。

a2复制到a1

可以看到复制后就是一些位图的形式

在这里插入图片描述

总结来说就是对CONFIG.SAV的文件进行读取,复制,以一种位图的形式保存游戏状态。

到这里,大致也就逆的差不多了,至于游戏文件怎么加载,怎么解析,应该去看游戏引擎的源码,逆向的话就太繁杂了、也没必要。

总结

Nippon真就不加密???
记得以前逆XP3的时候,Nippon也是先加密再压缩。。。然后XP3的加密也仅仅是单字节异或。。😂

总结逆向过程就是:动静结合分析程序逻辑,结合文件结构推测结构体,找出规律后extract游戏资源。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2090345.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

windows下载安装Node.js 并切换镜像地址

一、 windows下载Node.js Node.js官方安装包及源码下载地址为&#xff1a;https://nodejs.org/en/download/。 如果下载过慢可以使用nullhttps://nodejs.cn/download/ 下载对应版本 二、安装 双击安装包 全部点next即可 三、测试是否安装成功 打开命令窗口查看node版本 …

Unity SceneView 相机聚焦到指定位置

SceneView 相机聚焦到指定位置 SceneView.lastActiveSceneView.LookAt(pos);

书生大模型实战营基础(5)——XTuner 微调个人小助手认知任务

目录 1 、微调前置基础 2、准备工作 2.1环境配置 结果 2.2模型准备 目录结构&#xff1a;在目录结构中可以看出&#xff0c;internlm2-chat-1_8b 是一个符号链接 3、快速开始 3.1 微调前的模型对话 获取开发机端口和密码&#xff1a; 3.2 指令跟随微调 3.2.1 准备数…

基于Yolov5_6.1、LPRNet、PySide6开发的车牌识别系统

项目概述 项目背景 随着车辆数量的不断增加&#xff0c;车牌识别系统在交通管理、停车场自动化等领域变得越来越重要。本项目利用先进的深度学习技术和现代图形用户界面框架来实现高效的车牌识别功能。 项目特点 高效识别&#xff1a;采用 YOLOv5_6.1 进行车牌定位&#xff…

Linux--IO模型_多路转接

目录 0.往期文章 1.五种IO模型介绍 概念 调用函数&#xff08;非阻塞IO&#xff09; 2.详解多路转接 之select select函数介绍 设置文件描述符 写一个基于select的TCP服务器 辅助库 基于TCP的Socket封装 服务器代码 测试服务器 小结 3.详解多路转接 之poll poll函…

【云游戏】点量云流赋能大型游戏新体验

点量小刘发现近期国产化大型3A游戏《黑神话&#xff1a;悟空》的发售&#xff0c;可谓是赢得了一波好评。从场景内容来说深厚的文化底蕴支撑和高质量精美的特效及画面制作令人眼前一亮&#xff0c;作为备受瞩目的一款游戏&#xff0c;从技术层面来说&#xff0c;该游戏也离不开…

关于武汉芯景科技有限公司的多协议收发芯片XJ3160开发指南(兼容MAX3160)

一、芯片引脚介绍 1.芯片引脚图 2.引脚描述 二、功能模式 1.RS232模式 2.RS485模式

赋能楼宇智能化升级:EasyCVR视频汇聚平台引领智慧楼宇安防新趋势

在当今科技飞速发展的时代&#xff0c;智慧楼宇监控系统如同一位默默守护的“超级卫士”&#xff0c;保障着我们工作和生活的舒适与安全。那么&#xff0c;这个强大的“卫士”是由哪些子系统构成的呢&#xff1f;且听我慢慢道来。 一、智慧楼宇的组成部分 首先&#xff0c;不…

神州数码半年业绩双增长,AI驱动数云服务及软件业务增长62.7%

发布 | 大力财经 8月30日晚间&#xff0c;神州数码集团&#xff08;000034.SZ&#xff09;发布2024年度中期业绩报告。 报告期内&#xff0c;神州数码营业收入实现625.6亿元&#xff0c;同比增长12.5%&#xff1b;归母净利润实现5.1亿元&#xff0c;同比增长17.5%&#xff1b…

JavaScript 知识:this、apply/call/bind、Promise、HTTP 库 Axios

1、变量、声明、传递 (值、引用) javascript:void(0) 含义 javascript:void(0) 中最关键的是 void 关键字&#xff0c; void 是 JavaScript 中非常重要的关键字&#xff0c;该操作符指定要计算一个表达式但是不返回值。void() 仅仅是代表不返回任何值&#xff0c;但是括号内的表…

字体的一些基本知识(字体族、衬线字体、回退机制)

文章目录 字体族常见的字体族作为网页&#xff0c;这里暂时只讨论衬线体和无衬线体多字体机制&#xff08;fallback&#xff09;回退机制 字体族 衬线体&#xff08;serif&#xff09;无衬线体&#xff08;sans-serif&#xff09;等宽字体&#xff08;monospace&#xff09;手…

Vue3中 defineProps 与 defineEmits 基本使用

defineProps 基本概念 在Vue 3中&#xff0c;defineProps是一个函数&#xff0c;用于定义一个组件的props。它接收一个props对象作为参数&#xff0c;并且会返回一个响应式的props对象。简单来说在vue3中&#xff0c;在进行父组件向子组件的通信&#xff0c;我们可以使用defin…

DeeplxFile:基于Deeplx提供的免费,不限制文件大小的文件翻译工具

DeeplxFile是一款基于Deeplx提供的免费&#xff0c;不限制文件大小的文件翻译工具&#xff0c;目前已完全支持Word&#xff0c;Excel&#xff0c;powerpoint 支持翻译大部分内容&#xff0c;工具支持Windows和macos Windows提供了编译好的exe版本, 直接双击运行即可&#xff0…

并发容器简介

由于同步器的串行化严重降低了并发性&#xff0c;Java之后推出了多种并发容器&#xff0c;使用并发容器来替代同步容器&#xff0c;可以提高绳索性并降低风险 J.U.C包中提供了几个非常有用的并发容器作为线程安全的容器&#xff1a; J.U.C包中提供的并发容器命名一般分为三类&…

EmguCV学习笔记 VB.Net 8.1 漫水填充法 floodFill

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 EmguCV是一个基于OpenCV的开源免费的跨平台计算机视觉库,它向C#和VB.NET开发者提供了OpenCV库的大部分功能。 教程VB.net版本请访问…

RabbitMQ本地Ubuntu系统环境部署与无公网IP远程连接服务端实战演示

文章目录 前言1.安装erlang 语言2.安装rabbitMQ3. 安装内网穿透工具3.1 安装cpolar内网穿透3.2 创建HTTP隧道 4. 公网远程连接5.固定公网TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址 &#x1f4a1; 推荐 前些天发现了一个巨牛的人工智能学习网站&am…

Docker 数据卷管理及优化

目录 1 数据卷实现的目的 2 为什么要用数据卷 3 docker的两种数据卷 3.1 bind mount 数据卷 实践实例&#xff1a; 3.2 docker managed 数据卷 实验实例&#xff1a; 3.3 bind mount 数据卷和docker managed 数据卷的对比 3.3.1 相同点&#xff1a; 3.3.2 不同点&#xff1a; …

Ubuntu安装android studio(压缩包版)

#这里适用于linux版压缩版# 1、官网下载Linux版本压缩包 2、下载的文件 .gz,将这个包双击解压出来 3、直接进入解压包的bin目录下&#xff0c;右击在终端打开 3.1、输入 ./studio.sh ./studio.sh 如图所示&#xff1a; 后续操作见图片 下载的时候&#xff0c;直接cancel&…

HIVE 数据仓库工具之第二部分(数据库相关操作)

HIVE 数据仓库工具之第二部分&#xff08;数据库相关操作&#xff09; 一、Hive 对数据库的操作1.1 创建数据库1.1.1 创建数据库语法1.1.3 示例 1.2 使用数据库1.2.1 使用数据库语法1.2.2 示例 1.3 修改数据库1.3.1 修改数据库的语法1.3.2 示例 1.4 删除数据库1.4.1 删除数据库…

STM32基于HAL库串口printf使用和接收

我们这里使用HAL库直接用cubemx生成代码配置串口 1.打开cubemx&#xff0c;选择MCU型号 2.我这里使用的是STM32F103C8T6&#xff0c;根据自己的型号选择&#xff0c;这里不限制型号 3.选择时钟源 4.系统设置 5时钟配置 5.选择和配置串口 5.配置中断和中断优先级 6.工程设置…