Table of Contents


Arena of Valor Hotfix Analysis

Arena of Valor Hotfix Analysis

Reverse engineering the iFix hotfix system in Arena of Valor.

Arena of Valor Hotfix Analysis

This document provides a comprehensive reverse-engineering analysis of the Tencent’s InjectFix as implemented in Arena of Valor version 1.60.1.9.

Disclaimer & Ethical Statement

This research is conducted and documented solely for educational and security research purposes.

The author does not encourage, support, or condone the use of this information for:

  • Creating malicious cheats or hacks.
  • Disrupting game services or violating the Terms of Service of Arena of Valor.
  • Infringing upon the intellectual property rights of Tencent, Garena, or any associated parties.

Notice: If you are an authorized representative of the relevant parties and wish for this content to be removed or redacted, please contact the author immediately. This content will be removed upon valid request.

Part 1: Decryption of Hotfix Files

The first step in the iFix pipeline is decrypting the .bytes hotfix files.

1.1. Decryption Algorithm

The system uses the AES algorithm in CBC Mode. The decryption is handled by the .NET framework’s System.Security.Cryptography.CryptoStream class, which provides a stream-based interface for cryptographic transformations.

The setup and initialization of this decryption stream occurs in the constructor of the CommonLibs.FileModule.ResDReader.KnowLengthBytesStream class.

  • Function: CommonLibs.FileModule.ResDReader.KnowLengthBytesStream..ctor
  • Address: 0x5BF8D24

IDA Pseudo-code:

// Constructor for KnowLengthBytesStream
public KnowLengthBytesStream(Stream inStream, string resName, ..., bool isEncrypt)
{
    // ...
    if (isEncrypt) 
    {
        // 1. Get the AES provider instance. This function also sets the IV.
        m_aesProvider = ResDReader.get_m_aesProvider();

        // 2. Derive the decryption key based on the resource name.
        byte[] mixedKey = ResDReader.GetMixedKey(resName);

        // 3. Set the Key for the AES provider.
        m_aesProvider.set_Key(mixedKey);

        // 4. Create the AES decryptor transform.
        ICryptoTransform decryptor = m_aesProvider.CreateDecryptor();

        // 5. Wrap the original (encrypted) stream with a CryptoStream.
        //    Data read from this stream will be decrypted on the fly.
        this.m_cryptoStream = new CryptoStream(inStream, decryptor, CryptoStreamMode.Read);
    }
    // ...
}

1.2. Key and IV Derivation

The Key (16-bytes)

The key is dynamically calculated based on the filename. This process involves two main functions:

Step 1: Hashing the Filename

  • Function: CommonLibs.FileModule.ResDReader.GetStrUpperHash
  • Address: 0x5BF7F68
  • Logic: Hashes the uppercase resource name using the formula: hash = (hash * 31) + character.

IDA Pseudo-code:

public static uint GetStrUpperHash(string inAssetName)
{
    uint hash = 0;
    foreach (char c in inAssetName.ToUpper())
    {
        hash = (hash * 31) + c;
    }
    return hash;
}

Step 2: Mixing with a Base Key

  • Function: CommonLibs.FileModule.ResDReader.GetMixedKey
  • Address: 0x5BF805C
  • Logic: Takes the hash from the previous step and XORs it byte-by-byte with a fixed Base Key: 99 64 b1 b0 6b 03 8d 7f b7 7d b6 a7 54 90 8b 73

IDA Pseudo-code:

public static byte[] GetMixedKey(string resName)
{
    uint hash = GetStrUpperHash(resName);
    byte[] keyBytes = (byte[])mRawKeys.Clone(); // mRawKeys holds the base key

    for (int i = 0; i < keyBytes.Length; i++)
    {
        // XOR each byte of the key with a byte from the hash
        keyBytes[i] ^= (byte)(hash >> ((i & 3) * 8));
    }
    return keyBytes;
}

The IV (16-bytes)

The IV is a static value. After using a debugger, it was confirmed that the IV is 16 bytes of zero.

  • Function: CommonLibs.FileModule.ResDReader.get_m_aesProvider
  • Address: 0x5BF82D4
  • Logic: This function creates the AesCryptoServiceProvider instance if it doesn’t exist and sets its IV property.

IDA Pseudo-code:

private static AesCryptoServiceProvider get_m_aesProvider()
{
    if (m_aesProvider == null)
    {
        m_aesProvider = new AesCryptoServiceProvider();
        m_aesProvider.set_Mode(CipherMode.CBC);
        m_aesProvider.set_IV(mIV); // Sets the static IV, which is 16 zero bytes
    }
    return m_aesProvider;
}

1.3. Script Python

This script implements the decryption logic. Some files have 3 bytes magic header 22 4A 67 before the encrypted data

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os

MAGIC = bytes.fromhex("22 4A 67")

def _key(name: str) -> bytes:
    h = 0
    for ch in name:
        c = ord(ch)
        if 97 <= c <= 122:
            c -= 32
        h = ((h * 31) + c) & 0xFFFFFFFF
    k = bytearray(bytes.fromhex("99 64 b1 b0 6b 03 8d 7f b7 7d b6 a7 54 90 8b 73"))
    k0, k1, k2, k3 = (h & 0xFF, (h >> 8) & 0xFF, (h >> 16) & 0xFF, (h >> 24) & 0xFF)
    for i in range(len(k)):
        k[i] ^= (k0, k1, k2, k3)[i & 3]
    return bytes(k)

def decrypt(name: str) -> bytes:
    key = _key(name)
    if not os.path.exists(f"{name}.bytes"):
        print(f"File {name}.bytes not found!")
        return b""
        
    with open(f"{name}.bytes", "rb") as f:
        data = f.read()
    
    off = 3 if data[:3] == MAGIC else 0
    ct = data[8:]
    cipher = AES.new(key, AES.MODE_CBC, b"\x00"*16)

    try:
        decrypted_data = cipher.decrypt(ct)
        pt = unpad(decrypted_data, AES.block_size)
    except (ValueError, KeyError):
        raise ValueError("Invalid padding or wrong key/iv")
        
    with open(f"{name}.dec", "wb") as f:
        f.write(pt)
    return pt

def encrypt(name: str) -> bytes:
    key = _key(name)
    if not os.path.exists(f"{name}.dec"):
        print(f"File {name}.dec not found!")
        return b""

    with open(f"{name}.dec", "rb") as f:
        pt = f.read()
    
    original_len = len(pt)
    
    pt_padded = pad(pt, AES.block_size)
    
    cipher = AES.new(key, AES.MODE_CBC, b"\x00"*16)
    ct = cipher.encrypt(pt_padded)
    
    hdr = (0).to_bytes(4, "little") + original_len.to_bytes(4, "little")
    
    with open(f"{name}.bytes", "wb") as f:
        f.write(hdr + ct)
    
    with open(f"{name}.bytes", "r+b") as f:
        f.seek(0)
        f.write(MAGIC)

    return ct

Part 2: Decrypted File Format

After decryption, the resulting .dec file is parsed by the IFix.Core.PatchManager$$Load function.

2.1. The Patch File Loader

  • Function: IFix.Core.PatchManager$$Load
  • Address: 0x3256270
  • Purpose: This function reads the binary data from the decrypted stream, parses it into a structured format, and prepares it for execution by the iFix VM.

2.2. File Header & Magic Number

The file begins with an 8-byte magic number that identifies it as a valid iFix patch.

  • Expected Magic Number: 0x1FD45F460E2CDBD0

IDA Pseudo-code (from IFix.Core.PatchManager$$Load):

// v7 is a System_IO_BinaryReader_o*
// Reads a ulong (8 bytes)
v10 = v7->ReadUInt64(); 

if ( v10 != 0x1FD45F460E2CDBD0LL ) // Compares with the Magic Number
{
    // ... throw exception
}

2.3. Overall File Structure & Data Types

The file is parsed sequentially, containing the following sections:

  1. Magic Number
  2. Bridge Name
  3. Extern Types: A list of external type definitions.
  4. Methods: The core bytecode and metadata for patched methods.
  5. Extern Methods: A list of external method signatures.
  6. Intern Strings: A pool of strings.
  7. …and other metadata sections.

2.4. Key Data Structures

Instruction Struct

This 8-byte struct represents a single operation in the iFix bytecode.

public struct Instruction
{
    public Code Code;    // 4-byte opcode enum
    public int Operand;  // 4-byte operand
}

Part 3: Virtual Machine Internals

The iFix bytecode is interpreted by a custom Virtual Machine. This execution engine is derived from the architecture of Tencent’s InjectFix, designed to dynamically parse and run IL instructions at runtime without requiring a full application update.

3.1. The VM Execution Loop

The main execution loop is in the IFix.Core.VirtualMachine.Execute method.

  • Function: IFix.Core.VirtualMachine.Execute
  • Address: 0x3245FF8
  • Role: It iterates through an array of Instruction structs, decodes each opcode, and executes the corresponding logic on a virtual evaluation stack.

Execution Loop Concept:

Value* Execute(Instruction* pc, ...)
{
    Instruction* current_pc = pc;
    while (true)
    {
        Instruction instruction = *current_pc;
        switch (instruction.Code)
        {
            // cases for each opcode...
        }
    }
}

3.2. Example Opcode Handling

StackSpace

This instruction is processed once at the start of Execute to set up the stack frame.

  • Purpose: Defines the maximum stack depth and number of local variables for a method.
  • Operand: Contains max_stack (lower 16 bits) and locals_cnt (upper 16 bits).
  • IDA Pseudo-code:
    // ... at the very beginning of the Execute function
    if ( pc->fields.Code != 146 ) // pc is the first instruction
    {
        // ... throw InvalidProgramException
    }
    virtualMachine = v15;
    Operand = pc->fields.Operand; // pc->fields.Operand is the operand of StackSpace
    v18 = argumentBase - evaluationStackBase;
    if ( (int)(v18 + argsCount + (Operand >> 16) + (unsigned __int16)Operand) >= 10241 ) // Check for stack overflow
    {
        // ... throw StackOverflowException
    }
    // ... further initialization using Operand's values
    
  • Explanation: Before any other instructions are executed, the Execute function validates that the first instruction is StackSpace. It then extracts the maximum stack size and local variable count from its operand. This information is crucial for setting up the method’s stack frame and performing initial stack overflow checks.

Ldarg: Load Argument

Pushes a method argument onto the evaluation stack.

  • Opcode: case 164:
  • IDA Pseudo-code:
    // Ldarg instruction
    // v1187 is the 'argumentBase'
    // v20->fields.Operand contains the argument index
    // v19 is the stack pointer ('evaluationStack')
    IFix_Core_VirtualMachine__copy(
        virtualMachine,
        evaluationStackBasea,
        v19++, // Destination on stack (and increment stack pointer)
        &v1187[v20->fields.Operand], // Source argument from argument array
        managedStack, 
        ...);
    ++v20; // Move to next instruction
    continue;
    
  • Explanation: This code copies a value from the argumentBase array (at the index specified by the operand) to the top of the evaluation stack. The stack pointer v19 is then incremented.

Add

Pops two values, adds them, and pushes the result.

  • Opcode: case 80:
  • IDA Pseudo-code:
    // Add instruction
    v537 = v19 - 2; // Pointer to the first value on stack
    if ( v19 == (IFix_Core_Value_o *)&dword_18 )
      sub_94DC8A0("L_574");
    --v19; // Decrement stack pointer
    switch ( v537->fields.Type )
    {
      case 0: // Integer
        if ( !v19 ) sub_94DC8A0("L_588");
        v537->fields.Value1 += v19->fields.Value1; // Add integer values
        ++v20;
        break;
      case 1: // Long
        if ( !v19 ) sub_94DC8A0("L_582");
        *(_QWORD *)&v537->fields.Value1 += *(_QWORD *)&v19->fields.Value1; // Add long values
        ++v20;
        break;
      case 2: // Float
         if ( !v19 ) sub_94DC8A0("L_595");
        *(float *)&v537->fields.Value1 = *(float *)&v537->fields.Value1 + *(float *)&v19->fields.Value1; // Add float values
        ++v20;
        break;
      // ... and so on for other types
    }
    continue;
    
  • Explanation: The code first decrements the stack pointer (v19). It then checks the Type of the value at the new top of the stack (v537). Based on the type (integer, long, float, etc.), it performs the appropriate addition with the value that was on top, storing the result back at v537.

3.3. Script Python

You can find at iFix-parse. Please give a star if you find it useful.

Part 4: Creating the Hotfix Bytecode

With the file format and VM logic understood, I could now construct a custom hotfix. My goal was to modify CameraSystem.GetZoomRate to return 3x the original value.

4.1. The Patching Strategy

I needed to construct a binary file (.dec) containing:

  1. Extern Types: CameraSystem (to access the property) and System.Single (float).
  2. Method Body: A sequence of instructions to implement return this.get_ZoomRateFromAge() * 3.0f;.
  3. Fix Info: Mapping my new method to GetZoomRate.

4.2. Opcode Logic

I constructed the following assembly sequence:

  1. StackSpace (146, 2): Init stack size of 2.
  2. Ldarg.0 (164, 0): Load this.
  3. CallVirt (122, token): Call get_ZoomRateFromAge.
  4. Ldc_I4 (143, 3): Push integer 3.
  5. Conv.R4 (58, 0): Convert 3 to float.
  6. Mul (3, 0): Multiply the result.
  7. Ret (173, 1): Return.

4.3. Script Python

I wrote this Python script to generate the unencrypted binary file (HeroSkin_1.dec) directly, bypassing the need for the official compiler.

import struct

def w_str(f, s):
    data = s.encode('utf-8')
    l = len(data)
    while l >= 0x80:
        f.write(struct.pack('B', (l & 0x7F) | 0x80))
        l >>= 7
    f.write(struct.pack('B', l))
    f.write(data)

with open("HeroSkin_1.dec", 'wb') as f:
    # 1. Header Magic
    f.write(struct.pack('<Q', 0x1FD45F460E2CDBD0))

    # 2. Bridge Name
    w_str(f, "IFix.ILFixInterfaceBridge, Project_d, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")

    # 3. Extern Types
    types = [
        "CameraSystem, Project_d, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
        "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
    ]
    f.write(struct.pack('<i', len(types)))
    for t in types: w_str(f, t)

    # 4. Methods (Payload)
    # Logic: return 3 * get_ZoomRateFromAge()
    ops = [
        (146, 2),     # StackSpace (locals=0, maxstack=2)
        (164, 0),     # ldarg.0
        (122, 65536), # callvirt CameraSystem::get_ZoomRateFromAge
        (143, 3),     # ldc.i4 3
        (58, 0),      # conv.r4
        (3, 0),       # mul
        (173, 1)      # ret
    ]
    
    f.write(struct.pack('<i', 1)) # Num methods
    f.write(struct.pack('<i', len(ops)))
    for code, op in ops:
        f.write(struct.pack('<ii', code, op))
    f.write(struct.pack('<i', 0)) # No exception handlers

    # 5. Extern Methods References
    f.write(struct.pack('<i', 1)) # Count
    f.write(struct.pack('<?i', False, 0)) # is_generic=False, type_id=0 (CameraSystem)
    w_str(f, "get_ZoomRateFromAge")
    f.write(struct.pack('<i', 0)) # param_ids count

    f.write(struct.pack('<4i', 0, 0, 0, 0))

    # 6. Managers info
    w_str(f, "IFix.WrappersManagerImpl, Project_d, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")
    w_str(f, ", Project_d, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")

    # 7. Fix Infos (Mapping)
    f.write(struct.pack('<i', 1)) # Count
    # Target: CameraSystem.GetZoomRate -> Patch ID 0
    f.write(struct.pack('<?i', False, 0))
    w_str(f, "GetZoomRate")
    f.write(struct.pack('<i', 0)) # param_ids count
    f.write(struct.pack('<i', 0)) # patch_id

    # 8. New Classes
    f.write(struct.pack('<i', 0))

print("Done")

4.4. Finalizing the Hotfix

After running the generator script, I obtained HeroSkin_1.dec. Then use the encrypt code from Part 1.3 to produce HeroSkin_1.bytes and copy it into the game’s resource folder.

image