2022 腾讯游戏安全大赛 mobile 决赛 wp

解密关键程序

Android端 Onclick监听对象中会调用rc4算法解密 assets 目录下的sock1文件,密钥是 "gamesec"

image-20250313101700147

这里给出解密脚本:

from Crypto.Cipher import ARC4 as rc4cipher
def enrc4(cip:bytes,key:bytes):
    from Crypto.Cipher import ARC4 as rc4cipher
    enc = rc4cipher.new(key)
    res = enc.encrypt(cip)
    return res


key = b"gamesec"

fp = open("./sock1","rb")
cip= fp.read()
dec = enrc4(cip,key)

fp1 = open("./dec/sock1.dec","wb")
fp1.write(dec)
fp.close()
fp1.close()

魔改upx脱壳

得到的 sock1.dec是被加壳了的,仔细观察可以发现这是魔改的upx壳,对比初赛时候的加壳程序,我们可以进行修复:

修复标志位:

image-20250312151824063

image-20250312151848361

总之就是把UE4替换成UPX,补上 \x7fELF,根据倒数第8~12个字节补上 EC F9 04 00,即脱壳前的文件大小

程序组成、反调试、混淆相关

程序组成

程序在运行期间加载了 assets目录下的 sock001等shellcode

image-20250313102713843

反调试的处理

用strace 跟了一下,大致就是通过检测 /proc/pid/maps中有没有frida

image-20250313102236175

关键逻辑应该在这里:

void __noreturn sub_426060()
{
  void *v0; // r1
  void *v1; // r0
  void (__fastcall *sleep)(int, _DWORD, void *, void *); // [sp+8h] [bp-20h]

  v0 = &unk_F81A338E;
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        v1 = v0;
        if ( (int)v0 <= (int)&unk_FE03C2D2 )
          break;
        if ( (int)v0 <= (int)&unk_297AC41F )
        {
          if ( (int)v0 > (int)&unk_333E5AE )
          {
            if ( v0 == &unk_333E5AF )
            {
              v0 = (void *)-1014228368;
            }
            else if ( v0 == &unk_5D8EDE4 )
            {
              v0 = (void *)-1761592891;
            }
          }
          else
          {
            v0 = &unk_3F49E12A;
            if ( v1 != &unk_FE03C2D3 )
            {
              v0 = v1;
              if ( v1 == &unk_2ED5952 )
                v0 = &unk_333E5AF;
            }
          }
        }
        else if ( (int)v0 <= 823225152 )
        {
          if ( v0 == &unk_297AC420 )
          {
            sub_425E94();
            v0 = &unk_3F49E12A;
          }
          else if ( v0 == &unk_2C662D1E )
          {
            v0 = (void *)-2111021749;
          }
        }
        else
        {
          v0 = &unk_FE03C2D3;
          if ( v1 != &unk_31116B41 )
          {
            v0 = &unk_2C662D1E;
            if ( v1 != &unk_34F5E0A0 )
            {
              v0 = v1;
              if ( v1 == &unk_3F49E12A )
              {
                sub_425E94();
                v0 = &unk_5D8EDE4;
                sleep = (void (__fastcall *)(int, _DWORD, void *, void *))fun_table_->sleep;
              }
            }
          }
        }
      }
      if ( (int)v0 > (int)&unk_A26FD559 )
        break;
      if ( (int)v0 > (int)&unk_8B915160 )
      {
        v0 = &unk_A26FD55A;
        if ( v1 != (void *)-1953410719 )
        {
          v0 = v1;
          if ( v1 == (void *)-1761592891 )
          {
            sleep(3, sleep, &unk_2C662D1E, &unk_3F49E12A);
            v0 = &unk_D89BA943;
          }
        }
      }
      else
      {
        v0 = &unk_333E5AF;
        if ( v1 != (void *)-2125002182 )
        {
          v0 = v1;
          if ( v1 == (void *)-2111021749 )
            v0 = (void *)-2125002182;
        }
      }
    }
    if ( (int)v0 > (int)&unk_D89BA942 )
    {
      v0 = &unk_A26FD55A;
      if ( v1 != (void *)-660887229 )
      {
        v0 = v1;
        if ( v1 == (void *)-132500594 )
          v0 = &unk_2C662D1E;
      }
    }
    else if ( v0 == &unk_A26FD55A )
    {
      v0 = &unk_31116B41;
    }
    else if ( v0 == (void *)-1014228368 )
    {
      v0 = &unk_FE03C2D3;
    }
  }
}

实际上,直接挂gdbserver + ida调试即可。

 ./gdbserver32 127.0.0.1:1234 ./sock1 1000 1000

混淆的处理

魔改的控制流平坦化混淆,不过混淆强度不高,直接eng看即可

外挂的实现

大致如下:

x1 = [GWorld+0x20]  # 0x491E6F0
x2 = [x1+0x70]      # Actors

y1 = read [GName],120   # 0x48711B4

# what => ULocalPlayer?
z1 = read [what+0x20]
z2 = read [z1+0x30C] + 0x320



v12 = 0
while 1:
    v63 = read [x2 + v12], 4        # actor 
    x4 = read [v63 + 0x10],4        # FName.ComparisonIndex 

    v17 = x4 / 0x4000;              # ChunkIndex 
    v18 = *(_DWORD *)&_y1[4 * (x4 / 0x4000)];   
    v19 = x4 % 0x4000;              # WithinChunkIndex 

    if !v18:
        v18 = read [GNAME + 4 * v17] , 4    # TNameEntryArray 
        [y1 + 4*v17] = v18
    
    v21 = read [v18 + 4*v19],4          # FNameEntry 
    v69 = read[v21 + 0x8],4             # string_addr
    out_buff = v69



     do
      {
        if ( !*((_BYTE *)out_buff + v20) )
          break;
        string_addr_table[v20] = *((_BYTE *)out_buff + v20);
        ++v20;
      }
      while ( v20 < 0x29 );
      string_addr_table[v20 + 1] = 0;
    v21(v23, string_addr_table);
      ((void (__fastcall *)(int, _BYTE *))mb_strcpy)(v22, string_addr_table);
      if ( (unsigned int)((int (__fastcall *)(int))unk_F76BDE44)(v22) < 5 )
        break;
    

    




    if unk_F767E848(v22,"ThirdPersonCharacter4") == 0:
        v69 = read [v63 + 0x120],4
        v69 = read[v69 + 0x100] , 12
    


    if ( ((int (__fastcall *)(int, const char *))unk_F76BD848)(v22, "ThirdPersonCharacter2")
        && ((int (__fastcall *)(int, const char *))unk_F76BD848)(v22, "ThirdPersonCharacter3")
        && ((int (__fastcall *)(int, const char *))unk_F76BD848)(v22, "ThirdPersonCharacter") )
      {
        break;
      }

    v58 = v23 = read [z2] , 28      # 
                                    #   Class: MinimalViewInfo
                                    #   Vector Location;//[Offset: 0x0, Size: 0xc] , 存储相机位置的坐标(例如:X、Y、Z),确定相机在世界坐标系中的位置
                                    #   Rotator Rotation;//[Offset: 0xc, Size: 0xc] , 表示相机的旋转,通常包含绕X、Y、Z轴的旋转角度,决定了相机的朝向。
                                    #   float FOV;//[Offset: 0x18, Size: 0x4] , 视场角决定了相机能看到的场景范围,通常与屏幕宽高比(Aspect Ratio)相关
    v69 = read [v63 + 0x120],4
    v24 = v63 + 0x120;
    if v69 = read[v69 + 0x100],12  
        || (v63 = v24,
            v26 = v54,
            sub_F7C91ED0(
              v54,
              v25,
              *(float *)v23,
              *(float *)(v23 + 4),
              *(float *)(v23 + 8),
              *(_DWORD *)(v23 + 12),
              *(_DWORD *)(v23 + 16),
              *(_DWORD *)(v23 + 20),
              0,
              0,
              v55,
              SHIDWORD(v55)),
            v27 = *v26,
            *v26 <= 0.0)
        || (v28 = v26[1], v28 >= v9) )
        goto LABEL_25;

    ... 
    ...
    v69 = read [v63] , 4
    v69 = read [v69 + 0x100],12
    v32 = v69
    v35[1] = (int)v32;
      *((float *)v35 + 2) = v27;
      *((float *)v35 + 3) = v28;
    goto writefile
    

    v12 += 1
  • 0x491E6F0处的偏移对应了 GWorld,随后通过 [[GWORD+0x20] + 0x70]拿到了所有的Actor对象

  • 0x48711B4处的偏移对应了 GName

  • v69 = read [v63] , 4
    v69 = read [v69 + 0x100],12
    这里主要是获取存储在AActor类的RootComponent成员的RelativeLocation的坐标信息,这样可以获取敌人的坐标信息
    
  • z1 = read [what+0x20]
    z2 = read [z1+0x30C] + 0x320
    v58 = v23 = read [z2] , 28
    这里是获取了MinimalViewInfo类中的这三个成员:
    #   Class: MinimalViewInfo
    #   Vector Location;//[Offset: 0x0, Size: 0xc] , 存储相机位置的坐标(例如:X、Y、Z),确定相机在世界坐标系中的位置
    #   Rotator Rotation;//[Offset: 0xc, Size: 0xc] , 表示相机的旋转,通常包含绕X、Y、Z轴的旋转角度,决定了相机的朝向。
    #   float FOV;//[Offset: 0x18, Size: 0x4] , 视场角决定了相机能看到的场景范围,通常与屏幕宽高比(Aspect Ratio)相关
    
  • sub_F7C91ED0 函数实现了敌方的世界坐标到我们的视图坐标的转化

随后,调用 write 函数,把相关人物与坐标的信息写入了 "/sdcard/1A.txt" 文件中,并通过libPutri.so文件进行绘制

image-20250313110209876

image-20250313125037949

image-20250313124419872

ue4dumper

dump sdk:

./ue4dumper --package com.YourCompany.ThirdPerson --sdkw  --gworld 0x491E6F0 --gname 0x48711B4 --output /data/local/tmp/files  

外挂实现参考:

https://renyili.org/post/game_cheat2/#331-透视

1、为什么我gg搜不到

2、透视的实现细节,尝试自己实现。

3、shellcode + ida的更好的处理方式

4、字符串混淆的更好处理方式

5、去混淆

posted @ 2025-03-16 14:20  TLSN  阅读(82)  评论(0)    收藏  举报