腾讯游戏安全技术竞赛-2021 Android Final 参赛纪录

TP 2021 Android Final

修复so

和预赛一样,决赛的 .so 文件的 .text,.rodata 段被加密。由于 dd 指令 dump 内存太慢,决赛我选择 gameguardian(GameGuardian APK Download) Dump内存,dump的内存地址从 /proc/xxxx/maps 读取。

1
2
3
4
5
6
7
8
9
10
11
12
u0_a285      15423   777 1363396 110672 ep_poll             0 S com.personal.flappybird

1|walleye:/ # cat /proc/15423/maps | grep 2cpp
c02fb000-c06db000 r--s 00000000 103:1d 2917159 /data/media/0/Android/data/com.personal.flappybird/files/il2cpp/Metadata/global-metadata.dat
c16cb000-c1dd1000 r-xp 00000000 103:1d 2310173 /data/app/com.personal.flappybird-Ro_Mv8KtyMyikogWcojKtw==/lib/arm/libil2cpp.so
c1dd2000-c1e03000 r--p 00706000 103:1d 2310173 /data/app/com.personal.flappybird-Ro_Mv8KtyMyikogWcojKtw==/lib/arm/libil2cpp.so
c1e03000-c1e22000 rw-p 00737000 103:1d 2310173 /data/app/com.personal.flappybird-Ro_Mv8KtyMyikogWcojKtw==/lib/arm/libil2cpp.so
c1e53000-c1e54000 rw-p 00755000 103:1d 2310173 /data/app/com.personal.flappybird-Ro_Mv8KtyMyikogWcojKtw==/lib/arm/libil2cpp.so
c1e54000-c1e5a000 r-xp 00755000 103:1d 2310173 /data/app/com.personal.flappybird-Ro_Mv8KtyMyikogWcojKtw==/lib/arm/libil2cpp.so
c1e5a000-c1e5c000 rw-p 0075a000 103:1d 2310173 /data/app/com.personal.flappybird-Ro_Mv8KtyMyikogWcojKtw==/lib/arm/libil2cpp.so
d3dda000-d3e2d000 r--s 00000000 103:1d 2917157 /data/media/0/Android/data/com.personal.flappybird/files/il2cpp/Resources/mscorlib.dll-resources.dat

1
2
3
4
5
6
7
walleye:/ # cat /proc/15423/maps | grep unity
c335a000-c3e14000 r-xp 00000000 103:1d 2310168 /data/app/com.personal.flappybird-Ro_Mv8KtyMyikogWcojKtw==/lib/arm/libunity.so
c3e15000-c3e2f000 r--p 00aba000 103:1d 2310168 /data/app/com.personal.flappybird-Ro_Mv8KtyMyikogWcojKtw==/lib/arm/libunity.so
c3e2f000-c3e41000 rw-p 00ad4000 103:1d 2310168 /data/app/com.personal.flappybird-Ro_Mv8KtyMyikogWcojKtw==/lib/arm/libunity.so
c3ef5000-c3ef7000 rw-p 00ae5000 103:1d 2310168 /data/app/com.personal.flappybird-Ro_Mv8KtyMyikogWcojKtw==/lib/arm/libunity.so
c3ef7000-c3efc000 r-xp 00ae6000 103:1d 2310168 /data/app/com.personal.flappybird-Ro_Mv8KtyMyikogWcojKtw==/lib/arm/libunity.so
c3efc000-c3efe000 rw-p 00aea000 103:1d 2310168 /data/app/com.personal.flappybird-Ro_Mv8KtyMyikogWcojKtw==/lib/arm/libunity.so

dump后,使用 010 editor 将 dump下的数据覆盖 两个 .so 的 .text 和 .rodata。

观察资源文件,发现 global-metadata.dat 被加密。分析il2cpp.so,发现加载global-metadata.dat的函数被hook到sec2021.so。

image-20210410163156167

上图是还原后的加载函数。

dump global-metadata.dat

为了获取 global-metadata.dat 文件,我们可以选择从全局变量 s_GlobalMetadata 找到内存中的 global-metadata.datdump下来。

image-20210410163242967

记下 s_GlobalMetadata 在il2cpp.so的偏移,通过 /proc/xxxx/maps 获取il2cpp.so的基址,加上偏移后,用GG修改器dump下来。

dump后,发现dat文件与正常的dat文件差距较大。分析 il2cpp::vm::MetadataCache::Initialize函数,发现 dat 文件的字符串被加密了,写个CPP解密一下。

image-20210410163748836

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
char g_pDecoutput[20000000];
extern unsigned char hexData[];//密文在其他CPP中,太长不放这里了。

int __fastcall GetStringFromIndex(int idx)
{
char *pOutBase=g_pDecoutput; // r2
int pRet; // r12
int *v3; // lr
char *data=(char*)hexData; // r1
printf("%d:",idx);
if ( !*(char *)(g_pDecoutput + idx) )
{
if ( 222636 > idx ) // 222636
{

do
{
if ( !data[idx] )
break;
pOutBase[idx] = idx & 0x7F ^ (data[idx] - 0x7F) ^ 0x5C;
printf("%c",pOutBase[idx]);
++idx;
}
while ( idx < 222636 ); // 222636
}
}
puts("");
return idx++;
}

解密后,可以看到字符串,下面是末尾的一部分字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
222120:medalSparkle
222133:flappy
222140:ready
222146:newBool
222154:gameScore
222164:<GameOver>c__Iterator0
222187:<i>__1
222194:$locvar0
222203:$locvar1
222212:<StartGame>c__Iterator1
222236:MainMenu
222245:ChangeScene
222257:RateGame
222266:<StartGame>c__Iterator0
222290:ObstacleBehaviour
222308:moveSpeed
222318:ObstacleSpawner
222334:OnTriggerEnter2D
222351:obstaclePrefabs
222367:tempTime
222376:PlayerController
222393:OnCollisionEnter2D
222412:KillPlayer
222423:thrust
222430:minTiltSmooth
222444:maxTiltSmooth
222458:hoverDistance
222472:hoverSpeed
222483:tiltSmooth
222494:playerRigid
222506:downRotation
222519:upRotation
222530:Scrolling
222540:scrollSpeed
222552:isMainMenu
222563:quadRenderer
222576:SoundManager
222589:tempName
222598:PlayTheAudio
222611:swoosh
222618:flap
222623:playAudio

尝试使用il2cppdumper解析,但报错了。

1
2
3
4
5
6
7
8
9
Il2CppDumper.exe D:\CTF\MATCH\TP2021\Final\Android\FlappyBird\lib\armeabi-v7a\libil2cpp.so D:\CTF\MATCH\TP2021\Final\Android\FlappyBird\assets\bin\Data\Managed\Metadata\global-metadata.dat .\
Initializing metadata...
System.ArgumentException: An item with the same key has already been added. Key: 119
at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
at System.Linq.Enumerable.ToDictionary[TSource,TKey](TSource[] source, Func`2 keySelector, IEqualityComparer`1 comparer)
at Il2CppDumper.Metadata..ctor(Stream stream) in C:\projects\il2cppdumper\Il2CppDumper\Il2Cpp\Metadata.cs:line 78
at Il2CppDumper.Program.Init(String il2cppPath, String metadataPath, Metadata& metadata, Il2Cpp& il2Cpp) in C:\projects\il2cppdumper\Il2CppDumper\Program.cs:line 126
at Il2CppDumper.Program.Main(String[] args) in C:\projects\il2cppdumper\Il2CppDumper\Program.cs:line 100
Press any key to exit...

模拟sec2021.so

sec2021 的功能与初赛大致相似,都导出了2个数组:

image-20210410162805774

分析p_array中的函数

image-20210410162908862

第0个函数大概与保护相关,在fread,fwrite等函数都有调用。

h_open_meatdata 函数实现加载 metadata。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void __fastcall dec_Metadata(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, __int64 a9, __int64 a10, int a11)
{
int v11; // r4
int v12; // r5
char v13; // r8
int v14; // r9
int v15; // r0
int sem; // r0
int v17; // r0

a8 = 0xEECF7326; // memcpy

((void (__fastcall *)(int *, int, int, int))((char *)off_4C704 + v14))(&a8, v11, -1, v12);// 31f70 memcpy
a9 = 0LL;
a10 = 0LL;
LOBYTE(a11) = v13;
v15 = ((int (*)(void))((char *)off_4C708 + v14))();// 33b08 init sem
sem = ((int (__fastcall *)(int, __int64 *))((char *)0x340d8 + v14))(v15, &a9);// 340d8 wait sem
a8 = 0xEECF7326;
((void (__fastcall *)(int *, int, int, int))((char *)0x003202c + v14))(&a8, v11, g_metadata_size, sem);// 0x003202c decrypt
a8 = 0xEECF7326;
((void (__fastcall *)(int *, int))((char *)off_4C714 + v14))(&a8, 3539);// 0032138
v17 = ((int (__fastcall *)(int *))((char *)off_4C718 + v14))(&dword_4EDA1);// 0284fc
hooked_il2cpp_func[0] = (int (*)(void))(v17 + 0x553034);
hooked_il2cpp_func[1] = (int (*)(void))(v17 + 0x58DDE4);
hooked_il2cpp_func[2] = (int (*)(void))(v17 + 0x55311C);
__asm { POPEQ {R4-R11,PC}; Pop registers }
}

和初赛一样,我选择模拟 sec2021.so。将模拟的 sec2021.so、解密后的 unity.so,il2cpp.so、解密后的 global-metadata.dat覆盖原文件打包后,发现程序crash。

1
2
3
4
5
6
7
8
# 部分crash log
09-12 10:19:23.802 10761 15384 E CRASH : #00 pc 0058dde4 /data/app/com.personal.flappybird-JK0jL3qOSXAgeknqh2UNyA==/lib/arm/libil2cpp.so
09-12 10:19:23.802 10761 15384 E CRASH : #01 pc 002a6480 /data/app/com.personal.flappybird-JK0jL3qOSXAgeknqh2UNyA==/lib/arm/libunity.so
09-12 10:19:23.802 10761 15384 E CRASH : #02 pc 002a63a4 /data/app/com.personal.flappybird-JK0jL3qOSXAgeknqh2UNyA==/lib/arm/libunity.so
09-12 10:19:23.802 10761 15384 E CRASH : #03 pc 002a16b4 /data/app/com.personal.flappybird-JK0jL3qOSXAgeknqh2UNyA==/lib/arm/libunity.so
09-12 10:19:23.802 10761 15384 E CRASH : #04 pc 004cd144 /data/app/com.personal.flappybird-JK0jL3qOSXAgeknqh2UNyA==/lib/arm/libunity.so
09-12 10:19:23.802 10761 15384 E CRASH : #05 pc 004cce40 /data/app/com.personal.flappybird-JK0jL3qOSXAgeknqh2UNyA==/lib/arm/libunity.so
0

分析crash部分,发现是 il2cpp_method_get_name 发生了异常。

image-20210410164902757

image-20210410164943255

这个导出函数显得很奇怪,相当于是 R0=*(0+12)。结合 p_array 中有 hook_dlsym,怀疑是sec2021.so 中的 dlsym 将这个函数解析到真正的地址,而 il2cpp.so 中的原导出地址是假的。

分析 hook_dlsym,发现很多 BX 指令,干扰了IDA的分析。将BXPatch成NOP,勉强分析一下。

image-20210410165345417

第一个函数是根据index获取字符串,有很多BX BLX之类的混淆指令,patch成Nop后,勉强能看,但循环、条件判断之类的都无法看出了,还好通过查找SBox的交叉引用,看到其他函数的未经混淆的解密函数,写CPP解密一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int __fastcall GetString_31A60(int *a1, int ID)
{
int v2; // r9
unsigned __int8 size; // r8
char *pBuf; // r6
int v5; // r0
int *v6; // r2
_DWORD *v7; // r1
char cur_xor; // [sp+4h] [bp-3Ch]
_DWORD v10[3]; // [sp+8h] [bp-38h] BYREF
char buf4[4]; // [sp+14h] [bp-2Ch] BYREF
int ID_1; // [sp+18h] [bp-28h] BYREF
int a1a; // [sp+1Ch] [bp-24h] BYREF

v2 = *a1;
ID_1 = ID;
Set0(buf4);
sub_30D78((int)v10, (int)&dword_4DFC4, (int)&ID_1);
sub_30DA0((int)buf4, v10);
Set0to0_0((int)v10, (int)&dword_4DFC4);
sub_30DD4(buf4, v10);
cur_xor = SBox[ID_1];
size = SBox[ID_1 + 1] ^ cur_xor;
pBuf = (char *)malloc(size + 1);
v10[2] = pBuf;
a1a = v2;
clear_mem((int)&a1a, (int)pBuf, -1);
a1a = v2;
copy((int)&a1a, (int)pBuf, -1, &SBox[ID_1 + 2], size);
a1a = v2;
do_xor((int)&a1a, (int)pBuf, size, cur_xor);
a1a = v2;
v5 = getCheckSum(&a1a, pBuf);
v6 = (_DWORD *)&byte_4;
v7 = (_DWORD *)(unsigned __int8)SBox[size + 2 + ID_1];
if ( (_DWORD *)v5 == v7 )
v6 = &dword_14;
sub_31054(0, v7, v6);
sub_31038(&dword_4DFC4, 0);
return v10[2];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int getxor(int id){
int checksum=0xff;
// ret=[]
int _0=sbox[id];
int _1 = sbox[id+1];
int cur_size=_0 ^ _1;
unsigned char key[9];
key[0]=_0;
// puts("Keys:");
for (int i = 1; i < 9; ++i) {
// printf("%02hhx,",key[i-1]);
key[i]= ((unsigned __int8)(key[i-1] + 17) >> 5) | (8 * (key[i-1] + 17));
//key[i]= ((key[i-1]+17) >> 5) | ((key[i-1]+17)<<3);

}
puts("");
// puts("data:");
for (int i = 0; i < cur_size; ++i) {
printf("%c",key[i%9]^sbox[id+2+i]);
}
puts("");
return 0;
}
int main() {
getxor(264);
getxor(297);
getxor(327);
getxor(363);
getxor(390);
}

第二个是strcmp,再结合汇编分析,

image-20210410165504371

不难想到,hook_dlsym 将这几个函数解析到真正的地址。

1
2
3
4
5
il2cpp_method_get_name
il2cpp_class_get_name
il2cpp_class_get_methods (0x553034)
il2cpp_image_get_class (0x58DDE4)
il2cpp_class_get_method_from_name (0x55311C)

其中,前两个函数是在sec2021.so中实现,且只有2个指令。

image-20210410165746203

没有必要模拟,直接在 il2cpp.so中的对应位置 patch 成正确的指令即可。

image-20210410165951753

image-20210410170011213

image-20210410170106424

image-20210410170058255

分析后三条指令:

1
2
3
4
5
6
7
8
9
10
11
12
int h_il2cpp_class_get_methods()
{
return hooked_il2cpp_func[0]();
}
int h_il2cpp_image_get_class()
{
return hooked_il2cpp_func[1]();
}
int h_il2cpp_class_get_method_from_name()
{
return hooked_il2cpp_func[2]();
}

hooked_il2cpp_func 的引用,发现在加载 metadata 函数被赋值。

不难猜到,v17 和后面的数字分别是il2cpp的基址和真正函数的偏移。

image-20210410170233846

将后三个函数的导出地址的第一条指令,patch成 B 0xXXXXXX,即可让这三个函数跳转到真正函数地址。

实现无敌破解

global-metadata.dat 的格式与常规的差很多,无法使用工具解析。

在GitHub上搜关键字 gameScoreText flappy,可以找到这个游戏的源代码。

image-20210410171701790

找到判断障碍物的逻辑

image-20210410172219593

下载后,编译成apk,使用 il2cppdumper 分析,可以定位到关键逻辑函数,方便对比分析。

image-20210410171847600

image-20210410171832036

其中,UnityEngine_Component__CompareTag函数存在特征字符串”UnityEngine.GameObject::CompareTag(System.String)”

image-20210410171955252

在题目的so中找到特征字符串、查找引用,定位到题目对应函数。

image-20210410172125290

image-20210410172148041

为了实现无敌,将 BNE patch 成 B 即可。

image-20210410172351889