2022 腾讯游戏安全大赛 mobile 决赛 wp
解密关键程序
Android端 Onclick监听对象中会调用rc4算法解密 assets 目录下的sock1文件,密钥是 "gamesec"
这里给出解密脚本:
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壳,对比初赛时候的加壳程序,我们可以进行修复:
修复标志位:
总之就是把UE4替换成UPX,补上 \x7fELF,根据倒数第8~12个字节补上 EC F9 04 00,即脱壳前的文件大小
程序组成、反调试、混淆相关
程序组成
程序在运行期间加载了 assets目录下的 sock001等shellcode
反调试的处理
用strace 跟了一下,大致就是通过检测 /proc/pid/maps中有没有frida
关键逻辑应该在这里:
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文件进行绘制
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、去混淆