HANA5 游戏逆向

前言

某著名百合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(尝试读取)1~ 14,我点了第二页又继续加载15~ 28

在这里插入图片描述

我又重新保存了一个,发现这个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游戏资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值