利用Anno 1404
撰写者:Thomas Dubier - 16/12/2025 - 分类:漏洞利用 - 下载
Anno 1404是一款由Related Designs开发、育碧发行的策略游戏。这是一款侧重于城市管理和建设的即时战略游戏。其2010年发布的资料片《Anno 1404:威尼斯》包含了在线和局域网多人游戏模式。在我们的研究过程中,发现了若干漏洞。结合利用这些漏洞,可以在多人游戏模式中执行任意代码。
目录
- 引言
- 网络协议
- RDA格式
- GR2格式
- 漏洞
- 利用
- 演示
- 附录
引言
《Anno 1404:威尼斯》是一款可在Steam、Ubisoft Connect或GOG.com等多个平台获得的策略游戏。我们的研究基于GOG上可用的、不含DRM的版本 v2.01.5010。在此版本中,仅局域网多人游戏模式可用。
多人游戏模式允许保存游戏以便后续继续。主机会为自身和每个玩家创建一个 .sww 格式的保存文件。当客户端连接到托管已保存游戏的主机时,该文件会自动传输。这在漏洞研究方面提供了有趣的前景:文件是如何传输的?对允许传输的文件是否有任何限制?由于保存格式是私有的,这也可能成为一个有趣的攻击面。此外,该进程是32位的,并且没有启用任何缓解措施,如下方System Informer截图所示。
在启用3D加速的VirtualBox虚拟机中启动游戏时,存在一些图形兼容性问题。这可以通过在文件 %APPDATA%\Ubisoft\Anno1404Addon\Config\Engine.ini 中强制指定DirectX版本来修复。
1
2
3
4
|
<UseDDSTextures>1</UseDDSTextures>
<DirectXVersion>9</DirectXVersion> <!-- force usage of DirectX 9 -->
<EnableTextureMemoryManagement>0</EnableTextureMemoryManagement>
<EnableModelMemoryManagement>0</EnableModelMemoryManagement>
|
网络协议
游戏主要使用C++开发。RTTI(运行时类型信息)和DLL导出的函数名称方便了逆向工程工作。在网络方面,游戏拥有自己的基于UDP的协议,实现在DLL NetComEngine3.dll 中。首先,我们列出了服务器和客户端之间传输的不同消息。类型为RMC的消息尤其引起了我们的注意,因为它们能触及一个巨大的攻击面。根据下面方法中的日志字符串,程序通过一个RPC(远程过程调用)系统暴露对象。
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
43
44
|
char __stdcall RMC_CallMessage(ByteStream *input, char a3, ByteStream *source, WString *a5)
{
ByteStream *v4; // esi
unsigned int targetObject; // ebp
const wchar_t *v6; // eax
const wchar_t *targetName; // [esp-Ch] [ebp-24h]
const wchar_t *methodName; // [esp-8h] [ebp-20h]
unsigned int v10; // [esp+8h] [ebp-10h] BYREF
ByteStream *v11; // [esp+Ch] [ebp-Ch] BYREF
int Flags; // [esp+10h] [ebp-8h] BYREF
int methodID; // [esp+14h] [ebp-4h] BYREF
v4 = input;
Flags = 0;
v11 = 0;
v10 = 0;
ByteStream::ReadElements(input, &input, 2, 1);
ByteStream::ReadElements(v4, &Flags, 4, 1);
ByteStream::ReadInteger(v4, (unsigned int *)&v11);
ByteStream::ReadInteger(v4, &v10);
ByteStream::ReadElements(v4, &methodID, 2, 1);
if ( (_BYTE)source )
{
targetObject = v10;
source = v11;
methodName = ClassToMethodName(&v10, methodID);
targetName = TargetName(&v10);
v6 = TargetName(&v11);
WString::Format(
a5,
(wchar_t *)L"RMC_CALL message RMC_ID: %d, Flags: %d, Source: %x (%s), TargetObject: %x (%s), Method: %s",
(unsigned __int16)input,
Flags,
source,
v6,
targetObject,
targetName,
methodName);
}
if ( a3 )
return sub_100633D0((int)v4, (unsigned __int16 *)&input, &v11, &Flags, &v10, (__int16 *)&methodID);
else
return 1;
}
|
根据函数 RMC_CallMessage 中显示的日志消息,RMC消息由以下字段组成:ID、Flags、Source、TargetObject 和 Method。暴力枚举所有可能的类ID和方法ID可以全面了解通过此类消息可触及的攻击面。为此,编写了一个Frida脚本 explore-surface.js,其输出如下:
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
|
> frida -l explore-surface.js Addon.exe
800000 = RootDO
c00000 = Station
- 10 = SignalAsFaulty
1000000 = Session
- 8 = RetrieveURLs
- 9 = SynchronizeTermination
1400000 = IDGenerator
- 4 = RequestIDRangeFromMaster
1800000 = PromotionReferee
- 5 = ConfirmElection
- 6 = DeclinePromotion
- 7 = ElectNewMaster
3000000 = DefaultCell
4800000 = SessionClock
- 11 = AdjustTime
- 12 = SyncRequest
- 13 = SyncResponse
7400000 = Player
- 16 = ForceKickPlayer
- 17 = Kick
- 18 = OnCancelSendFile
- 19 = OnReceivedFileData
- 20 = OnSendFileData
- 21 = OnSendFileInit
7800000 = Chat
- 14 = onNewChatLine
7c00000 = GameSettings
- 15 = ExecuteOnHost
8000000 = SyncProtocol
- 22 = ClientToServerPing
- 23 = ClientToServerSync
- 24 = ConfirmHost
- 25 = IdentifyHost
- 26 = LeftGame
- 27 = RequestMsgResend
- 28 = ServerToClientPing
- 29 = ServerToClientSync
|
该脚本显示每个对象的有效ID和名称,以及每个有效方法的ID和名称。我们发现了与文件传输相关的方法 OnSendFileData、OnSendFileInit、OnReceivedFileData、OnCancelSendFile。在服务器端,方法 rd::netcom3::CNetComEngine3::sendFile 用于向客户端发送文件。一个初始数据包 OnSendFileInit 被发送到客户端,其中包含保存文件的名称(默认为 Sauvegarde.sww)。我们可以使用Frida在保存文件名中添加多个 ../ 来测试客户端的行为。该脚本可在附录中找到。
如上图所示,保存文件被写入 C:\User\user,而不是 C:\Users\user\Documents\ANNO 1404 Venise\Savegames\MPShare。客户端似乎没有对文件名进行验证。这种首次发现的路径遍历漏洞,允许以应用程序的权限在系统上几乎任何位置放置文件。游戏安装目录的ACL(访问控制列表)是宽松的,允许用户程序在其中放置任何文件。
在Windows上,动态链接库首先从应用程序目录加载,然后从系统目录加载。我们可以在游戏目录中放置一个DLL,该DLL将在下次启动游戏时被加载。这可以在游戏重启后实现任意代码执行。我们的目标是无需重启游戏即可执行代码。主要思路是用一个损坏的资源文件替换正常的资源文件。
RDA格式
Anno系列游戏使用RDA格式存储各种游戏资源(3D模型、声音、纹理、地图等)。这些存档可以在 addon 和 maindata 目录中找到。诸如RDAExplorer之类的工具可以浏览、提取和修改存档内容。
RDA格式没有官方文档,但GitHub上有一份基于逆向工程的文档1。
简而言之,一个RDA文件被分割成大小可变的链式数据块。每个块包含一定数量的文件及其压缩后的元数据。文件的元数据指示其在存档中的位置和大小。数据使用zlib库实现的DEFLATE算法进行压缩。下图总结了RDA存档的结构。
某些块可能会根据元数据的 flag 字段进行加密。所使用的加密基于XOR和一个用常量初始化的伪随机数生成器。以下是解密函数的伪代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
char __cdecl xor_decrypt(wchar_t *buf, unsigned int size)
{
signed int index; // esi
srand(0xA2C2Au);
index = 0;
if ( size >> 1 )
{
do
buf[index++] ^= rand();
while ( index < (int)(size >> 1) );
}
return 1;
}
|
使用Process Monitor,我们观察到对存档的大量读访问。因此,元数据可能在游戏启动时加载到内存中。随后,文件内容只在需要时才加载到内存。我们观察到在多人游戏会话中会加载许多 .gr2 文件。
GR2格式
.gr2 文件是以Granny Studio开发的专有格式存储的3D模型。这种格式曾在当时的许多视频游戏中使用,例如MMORPG《仙境传说》。围绕该游戏的社区部分记录了GR2格式2。Anno 1404使用一个名为 granny2.dll 的独立库来处理此类文件。简而言之,该格式允许在不同部分(Section)中存储模型的几何信息(Mesh)、骨架、纹理和使用的材质。下图展示了 .gr2 文件的结构:
函数 GrannyReadEntireFile 用于将Granny 3D文件加载到内存中。程序首先读取文件头(Header)和节区表(Section Table)。每个节区被解压缩后加载到内存中(通过 malloc 为每个节区保留空间)。然后,程序为每个节区应用重定位表(Relocation Table)。节区内很可能包含指向其他节区中对象的引用。由于节区没有映射到固定地址,库需要更新这些引用。重定位表中的条目由三个元素组成:
- Section Offset:将要更新的引用所在的位置(相对于关联的重定位表的节区起始位置)。
- Section Number:标识包含被指向对象的节区。
- Offset:被引用对象相对于其所属节区起始位置的偏移量。
漏洞
在研究应用重定位的机制时,发现了一个漏洞。重定位表中的条目没有经过验证,具体来说:
SectionNumber 成员未经验证,这可能导致从 SectionArray 数组进行越界读取(SectionArray 包含每个节区的基地址)。[1]
SectionOffset 成员未经验证,这可能导致向 destination 数组进行越界写入。[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
|
int *__cdecl GrannyGRNFixUp_0(DWORD RelocationCount, Relocation *PointerFixupArray, int *SectionArray, char *destination)
{
int *result; // eax
DWORD v6; // ebp
Relocation *v7; // ecx
int v8; // edx
result = (int *)RelocationCount;
if ( RelocationCount )
{
v6 = RelocationCount;
do
{
v7 = PointerFixupArray;
v8 = SectionArray[PointerFixupArray->SectionNumber]; // [1] Out-of-bound read
result = (int *)&destination[PointerFixupArray->SectionOffset]; // [2] Compute write address
++PointerFixupArray;
*result = v8; // [2] Out-of-bound write
if ( v8 )
*result = v8 + v7->Offset;
--v6;
}
while ( v6 );
}
return result;
}
|
如果知道节区(destination)的内存地址,可以设想构建一个内存任意写入原语。然而,由于ASLR,节区并不总是驻留在同一地址。
利用
有可能达成一种有趣的内存配置,其中节区地址数组(SectionArray)位于第一个节区数据内容之前。在这种情况下,节区指针数组和节区内容之间的偏移量是已知的。
为了实现两个相邻的分配,需要了解一些Windows 10堆的工作原理。《Windows 10 Segment Heap Internals》白皮书3很好地解释了底层机制。有两种类型的分配器:NT堆(在我们的案例中使用)和分段堆(Segment Heap)。NT堆分配器分为两个组件:
- LFH(低碎片堆):用于小尺寸分配。
- backend:用于较大尺寸的分配。
决定使用哪个分配器的阈值由 ntdll.dll 中函数 RtlpAllocateHeapInternal 内定义的常量 RtlpLargestLfhBlock 决定。该值等于 0x4000。自Windows 8起,LFH受到分配随机化的影响,我们无法确保一个数据块被分配在另一个数据块旁边,这使得利用变得不可靠。而backend分配器具有确定性行为。因此,设计了一个包含大量节区的文件,使得节区指针数组被分配在一个大于0x4000字节的块中。为了填补堆中可能存在的空洞,我们创建了多个.gr2文件。这提高了漏洞利用的可靠性。下图表示文件加载到内存后的布局。
结构体 GrannyFile 的大小取决于节区数量 n。当 n = 2720 时,我们得到所需的大小:
1
|
0x20 + 0x20 + n * 1 + n * 1 + n * 4 = 0x4000
|
第一个节区的重定位表被构造成可以修改指针 SectionContentArray[1]。库 granny.dll 中的分配器依赖于Windows分配器,但添加了一个0x1F字节的头。Windows分配器又添加了一个0x10字节的头,并且分配的大小根据系统类型(32位或64位)对齐到8或16字节的倍数。因此,一个 -0x3FF0 (0x4000 - 0x20 - 0x20 + 0x30) 的偏移量允许我们写入 SectionContentArray 数组。
现在第二个节区的地址已知,我们可以通过应用于第二个节区的重定位表在内存中写入任意值。granny.dll 库不受ASLR保护,并且DEP未启用。因此,我们可以替换库中的 alloc/free 回调以执行代码。
演示
这些漏洞已成功在Windows 10 (ver 10.0.19045.2965) 上被利用。
[视频文件]
附录
用于测试路径遍历漏洞的Frida脚本
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
|
const ByteStreamWriteStringPtr = ptr('0x1003A250');
const ByteStreamWriteStringFn = new NativeFunction(ByteStreamWriteStringPtr,'pointer',['pointer','pointer'])
const mem = Memory.alloc(1024);
mem.writeUtf16String('..\\..\\..\\..\\Sauvegarde.sww');
Interceptor.attach(ByteStreamWriteStringFn, {
onEnter(args) {
if(args[1].readPointer().readUtf16String().includes('Sauvegarde.sww'))
{
console.log(hexdump(args[1].readPointer(), {
offset: 0,
length: 256,
header: true,
ansi: true
}));
args[1].writePointer(mem);
console.log(hexdump(args[1].readPointer(), {
offset: 0,
length: 256,
header: true,
ansi: true
}));
}
}
});
|
- https://github.com/lysanntranvouez/RDAExplorer/wiki/RDA-File-Format
- https://github.com/rdw-archive/RagnarokFileFormats/blob/master/GR2.MD
- https://blackhat.com/docs/us-16/materials/us-16-Yason-Windows-10-Segmen…