ISITDTU-CTF 2021 Final

Flag Knight

Reversing

Đây là bài thuộc mục reverse, nó là 1 game viết bằng Unity.

Với game viết bằng Unity, ta có thể dùng dnSpy, decompile file Assembly-CSharp.dll để đọc code của game. Nhưng thư mục mà đề cho không chứa file này, mà chỉ có folder tên là il2cpp_data, đó là dấu hiệu cho thấy game đã được compile bằng IL2CPP backend.

The IL2CPP backend converts MSIL (Microsoft Intermediate Language) code (for example, C# code in scripts) into C++ code, then uses the C++ code to create a native binary file (for example, .exe, .apk, or .xap) for your chosen platform.

Với game sử dụng IL2CPP, code C# đã bị chuyển thành C++. Ta có thể sử dụng IL2cppDumper để lấy thông tin về tên hàm, tên biến, v.v … và gắn nó vào IDA để dễ dàng cho việc reverse. Tải IL2cppdumper bản mới nhất ở đây.

IL2cppDumper cần 2 file để hoạt động, một là file GameAssembly.dll và hai là file global-metadata.dat. Ta chọn hai file này trong thư mục game thì nó báo lỗi:

Lỗi Metadata file supplied is not valid metadata file ở file Metadata.cs. Giờ ta cùng mở source code của IL2cppDumper ra đọc xem sao.

Trong source code, nó check 4 byte đầu của file global-metadata.dat xem có phải là AF 1B B1 FA không. Nếu nhìn lại file của đề cho thì nó sẽ như này:

Vậy là file metadata của đề cho đã bị sửa đổi theo một cách nào đó. Để chắc chắn, mình đã tải thử Unity xuống, compile thử một project với IL2cpp rồi so sánh hai file metadata với nhau. Version Unity mà mình tải là 2021.2.2f1, giống y chang như của tác giả. Mình biết điều này là tại vì:

vm@vm:~/dist$ strings UnityPlayer.dll  | grep 2021
2021.2.2f1
2021.2.2f1_5e2b1e92c7f8
2021.2.2f1 (5e2b1e92c7f8)

Giờ mình thử so sánh:

Bên trái là file của mình tự tạo ra, bên phải là của tác giả. Có thể thấy file của mình bắt đầu bằng AF 1B B1 FA, nếu nhìn kĩ thì ta thấy file của tác giả cũng có AF 1B B1 FA, nhưng nó bắt đầu tại offset 0x80, điều này làm ta nghĩ tới việc tác giả chỉ đổi thứ tự các field của header. Struct này có tên là Il2CppGlobalMetadataHeader, nằm ở file “C:\Program Files\Unity\Hub\Editor\2021.2.2f1\Editor\Data\il2cpp\libil2cpp\il2cpp-metadata.h”

typedef struct Il2CppGlobalMetadataHeader
{
    int32_t sanity;
    int32_t version;
    int32_t stringLiteralOffset; // string data for managed code
    int32_t stringLiteralSize;
    int32_t stringLiteralDataOffset;
    int32_t stringLiteralDataSize;
    int32_t stringOffset; // string data for metadata
    int32_t stringSize;
    int32_t eventsOffset; // Il2CppEventDefinition
    int32_t eventsSize;
    int32_t propertiesOffset; // Il2CppPropertyDefinition
    int32_t propertiesSize;
    int32_t methodsOffset; // Il2CppMethodDefinition
    int32_t methodsSize;
    int32_t parameterDefaultValuesOffset; // Il2CppParameterDefaultValue
    int32_t parameterDefaultValuesSize;
    int32_t fieldDefaultValuesOffset; // Il2CppFieldDefaultValue
    int32_t fieldDefaultValuesSize;
    int32_t fieldAndParameterDefaultValueDataOffset; // uint8_t
    int32_t fieldAndParameterDefaultValueDataSize;
    int32_t fieldMarshaledSizesOffset; // Il2CppFieldMarshaledSize
    int32_t fieldMarshaledSizesSize;
    int32_t parametersOffset; // Il2CppParameterDefinition
    int32_t parametersSize;
    int32_t fieldsOffset; // Il2CppFieldDefinition
    int32_t fieldsSize;
    int32_t genericParametersOffset; // Il2CppGenericParameter
    int32_t genericParametersSize;
    int32_t genericParameterConstraintsOffset; // TypeIndex
    int32_t genericParameterConstraintsSize;
    int32_t genericContainersOffset; // Il2CppGenericContainer
    int32_t genericContainersSize;
    int32_t nestedTypesOffset; // TypeDefinitionIndex
    int32_t nestedTypesSize;
    int32_t interfacesOffset; // TypeIndex
    int32_t interfacesSize;
    int32_t vtableMethodsOffset; // EncodedMethodIndex
    int32_t vtableMethodsSize;
    int32_t interfaceOffsetsOffset; // Il2CppInterfaceOffsetPair
    int32_t interfaceOffsetsSize;
    int32_t typeDefinitionsOffset; // Il2CppTypeDefinition
    int32_t typeDefinitionsSize;
    int32_t imagesOffset; // Il2CppImageDefinition
    int32_t imagesSize;
    int32_t assembliesOffset; // Il2CppAssemblyDefinition
    int32_t assembliesSize;
    int32_t fieldRefsOffset; // Il2CppFieldRef
    int32_t fieldRefsSize;
    int32_t referencedAssembliesOffset; // int32_t
    int32_t referencedAssembliesSize;
    int32_t attributeDataOffset;
    int32_t attributeDataSize;
    int32_t attributeDataRangeOffset;
    int32_t attributeDataRangeSize;
    int32_t unresolvedVirtualCallParameterTypesOffset; // TypeIndex
    int32_t unresolvedVirtualCallParameterTypesSize;
    int32_t unresolvedVirtualCallParameterRangesOffset; // Il2CppMetadataRange
    int32_t unresolvedVirtualCallParameterRangesSize;
    int32_t windowsRuntimeTypeNamesOffset; // Il2CppWindowsRuntimeTypeNamePair
    int32_t windowsRuntimeTypeNamesSize;
    int32_t windowsRuntimeStringsOffset; // const char*
    int32_t windowsRuntimeStringsSize;
    int32_t exportedTypeDefinitionsOffset; // TypeDefinitionIndex
    int32_t exportedTypeDefinitionsSize;
} Il2CppGlobalMetadataHeader;

Nếu để ý thì ta sẽ thấy struct trên có 64 field, nếu tác giả đổi thứ tự thì sẽ khó để ta có thể bruteforce lại, vì có 64! trường hợp. Vậy nên ta sẽ dựa vào một số tính chất của header để giảm số lần bruteforce.

Đầu tiên sanity, trường này bắt buộc phải bằng 0xFAB11BAF. Tiếp theo là version, với bản Unity của tác giả thì version là 29, nên 4 byte của nó sẽ là 1D 00 00 00. Còn các trường tiếp theo, cứ mỗi 2 trường, để ý rằng nó sẽ có tên là XXXOffsetXXXSize, nên ta có thể hiểu hai trường này dùng để trỏ đến một section nào đó và size của nó. Giờ quay lại nhìn vào file metadata mà mình tạo lúc nãy:

Phần màu đỏ là stringLiteralOffset (=0x1000), còn xanh lá cây là stringLiteralSize (=0x6CB0) và xanh dương là stringLiteralDataOffset (=0x6DB0). Mà ta lại để ý 0x100+0x6CB0 = 0x6DB0, nên ta có thể dùng đây là một điều kiện để bruteforce hiệu quả hơn. Tương tự với các field tiếp theo. Nhờ vào điều kiện này mà mình đã recover được metadata header.

from pwn import *

nh = p32(0xfab11baf) + p32(0x1d)
nh += p32(0x100) + p32(0xa7a0)
nh += p32(0xa8a0) + p32(0x20b8c)
nh += p32(0x2b42c) + p32(0x8b9d4)
nh += p32(0xb6e00) + p32(0x330)
nh += p32(0xb7130) + p32(0x175e8)
nh += p32(0xce718) + p32(0xcf900)
nh += p32(0x19e018) + p32(0x12cc)
nh += p32(0x19f2e4) + p32(0x13440)
nh += p32(0x1b2724) + p32(0xdad4)
nh += p32(0x1c01f8) + p32(0x30a8)
nh += p32(0x1c32a0) + p32(0x5aff0)
nh += p32(0x21e290) + p32(0x32d6c)
nh += p32(0x250ffc) + p32(0x2e10)
nh += p32(0x253e0c) + p32(0x410)
nh += p32(0x25421c) + p32(0x26b0)
nh += p32(0x2568cc) + p32(0xeec)
nh += p32(0x2577b8) + p32(0x1470)
nh += p32(0x258c28) + p32(0x2cd0c)
nh += p32(0x285934) + p32(0x8cc8)
nh += p32(0x28e5fc) + p32(0x5a730)
nh += p32(0x2e8d2c) + p32(0x708)
nh += p32(0x2e9434) + p32(0xb40)
nh += p32(0x2e9f74) + p32(0x330)
nh += p32(0x2ea2a4) + p32(0x3b8)
nh += p32(0x2ea65c) + p32(0x4b960)
nh += p32(0x335fbc) + p32(0xc218)
nh += p32(0x3421d4) + p32(0x2b0c)
nh += p32(0x344ce0) + p32(0x1b70)
nh += p32(0x346850) + p32(0)
nh += p32(0x346850) + p32(0)
nh += p32(0x346850) + p32(0x1974)

Đoạn code trên sẽ tạo ra 64 field mới theo đúng thứ tự ban đầu. Thay header của tác giả bằng header mới tạo ra và chạy lại IL2cppDumper:

Vậy là ta đã dump được hết thông tin của game. Ta mở file GameAssembly.dll bằng IDA, import hết những thông tin này vào bằng script ida_with_struct_py3.py (file này nằm trong thư mục chứa IL2cppDumper), và ta sẽ có đầy đủ tên hàm.

Giờ ta có thể reverse game rồi.

Đầu tiên để ý rằng khi ta di chuyển nhân vật thì camera không di chuyển theo. Tức là khi nhân vật đi quá màn hình thì ta sẽ không thấy nhân vật nữa. Đến đây ta có thể nghĩ tới việc tạo một bản hack để di chuyển camera theo ý muốn, vì có thể tác giả giấu flag ở chỗ nào đó mà hiện tại camera không thể thấy.

Hacking

Coding

Để di chuyển được camera, chúng ta cần biết toạ độ nó nằm ở đâu trong bộ nhớ, và thay đổi nó. Trong Unity có một camera chính gọi là “main camera”, chúng ta có thể lấy “main camera” bằng Camera.main. Nhưng từ từ dã, đây là code C#, làm gì có source đâu mà gọi? Đó là lí do chúng ta phải viết code dưới dạng C, compile thành một file dll rồi inject vào game. Code để gọi và lấy camera như sau:

typedef unsigned long long u64;
typedef u64(*getMainCamera_t)(u64);

{
    u64 base = (u64)GetModuleHandleA("gameassembly.dll");
    getMainCamera = (getMainCamera_t)(base + 0x7CAB30);

    u64 mainCamera = getMainCamera(0); // <=== main camera
}

Nhưng trong đoạn code trên sao mình biết hàm lấy “main camera” nằm ở offset 0x7CAB30 ? Đó là vì:

Nhờ IDA ta có thể biết tên hàm và offset cần thiết để gọi hàm. Để đổi vị trí của camera, trong code Unity ta sẽ gọi:

{
    Camera.main.transform.position = new_position;
}

Như vậy trong dạng code C ta sẽ viết:

{
    u64 mainCamera = getMainCamera(0);
    u64 cameraTrans = getTransform(mainCamera, 0);
    u64 mainCameraTrans = getTransform(mainCamera, 0);
    vector3 pos = getPosition(cameraTrans, 0);
    const float SPEED = 0.5f;
    if (GetAsyncKeyState('J'))
        pos.x -= SPEED;
    if (GetAsyncKeyState('L'))
        pos.x += SPEED;
    if (GetAsyncKeyState('I'))
        pos.y += SPEED;
    if (GetAsyncKeyState('K'))
        pos.y -= SPEED;
    if (GetAsyncKeyState('U'))
        pos.z += SPEED;
    if (GetAsyncKeyState('O'))
        pos.z -= SPEED;
    setPosition(cameraTrans, &pos, 0);
}

Với đoạn code trên, ta sẽ có thể di chuyển camera bằng 4 nút J, K, L và I.

Ta không quan tâm tới toạ độ Z vì đây là game 2D.

Where to call?

Tuy nhiên ta chỉ mới biết code chứ chưa biết inject như nào. Một cách đơn giản là tạo 1 thread với 1 vòng lặp vô hạn, mỗi vòng lặp ta lại gọi đoạn code trên một lần, nhưng nó sẽ tốn nhiều CPU. Để “hack” chuẩn hơn, ta có thể hook hàm Update của một object, vì theo tài liệu ở đây thì nó là hàm được gọi mỗi frame.

Update: Update is called once per frame. It is the main workhorse function for frame updates.

Mình sẽ chọn hàm FlagkNights_SpawnPage__Update để hook. (Full script ở cuối bài :D)

Sau khi di chuyển được camera, mình nghĩ tới việc di chuyển xung quanh xem có flag không, nhưng map quá lớn nên mình không thể tìm được gì.

Hint

Với hint của tác giả: “AccountInfo”, mình liền tìm string này trong IDA, và tìm được hàm FlagkNights_SpawnPageContext__OnGetAccountInfoRes.

void __stdcall FlagkNights_SpawnPageContext__OnGetAccountInfoRes(
        FlagkNights_SpawnPageContext_o *this,
        __generated___protocol_GetAccountInfoRes_o *res,
        const MethodInfo *method)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  if ( !byte_180D82395 )
  {
    sub_1801054B0(&StringLiteral_1594, res);
    byte_180D82395 = 1;
  }
  if ( _generated___game_ability_PropertyData__get_Name((__generated___game_ability_PropertyData_o *)res, 0i64) == 12003648 )
  {
    main = (UnityEngine_Component_o *)UnityEngine_Camera__get_main(0i64);
    if ( !main )
      goto LABEL_13;
    transform = UnityEngine_Component__get_transform(main, 0i64);
    v6 = (UnityEngine_Component_o *)UnityEngine_Camera__get_main(0i64);
    if ( !v6 )
      goto LABEL_13;
    v7 = UnityEngine_Component__get_transform(v6, 0i64);
    if ( !v7 )
      goto LABEL_13;
    z = UnityEngine_Transform__get_position(vec, v7, 0i64)->fields.z;
    if ( !transform )
      goto LABEL_13;
    *(_QWORD *)&vec[0].fields.x = 0x468B0800475BAC00i64;// <--- assign camera's X, Y
    vec[0].fields.z = z;
    UnityEngine_Transform__set_position(transform, vec, 0i64);
    GameObjectWithTag = UnityEngine_GameObject__FindGameObjectWithTag(StringLiteral_1594, 0i64);// Player
    gameObject = GameObjectWithTag;
    if ( !GameObjectWithTag
      || (v10 = UnityEngine_GameObject__get_transform(GameObjectWithTag, 0i64),
          (v11 = UnityEngine_GameObject__get_transform(gameObject, 0i64)) == 0i64)
      || (v13 = UnityEngine_Transform__get_position(vec, v11, 0i64)->fields.z, !v10) )
    {
LABEL_13:
      sub_180105600();
    }
    *(_QWORD *)&vec[0].fields.x = 0x468B019A475BBBB1i64;// assign Player's X, Y
    vec[0].fields.z = v13;
    UnityEngine_Transform__set_position(v10, vec, 0i64);
  }
}

Đoạn code trên kiểm tra xem ID của account có phải là 12003648 không, nếu có thì nó sẽ set toạ độ x, y của camera và của nhân vật thành một số hardcode. Hai con số này lần lượt là 0x468B0800475BAC000x468B019A475BBBB1, tương ứng với (56236.000,17796.000)(56251.691,17792.801). Cuối cùng ta chỉ cần thêm một đoạn sau vào code:

    if (GetAsyncKeyState('P'))
    {
        pos.x = 56236.000f;
        pos.y = 17796.000f;
    }

Bấm P và lấy flag:

Script:

#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <stdio.h>
#include "MinHook.h"
#define def(x) (*(u64*)(x))
typedef unsigned long long u64;

typedef u64(*getMainCamera_t)(u64);
typedef u64(*getTransform_t)(u64, u64);
struct vector3 {
    float x, y, z;
};
typedef vector3 (*getPosition_t)(u64, u64);
typedef u64(*setPosition_t)(u64, vector3*, u64);
typedef u64(*spUpdate_t)(u64, u64);
spUpdate_t orgSpawnPageUpdate = 0;
u64 base = 0;
getMainCamera_t getMainCamera = 0;
getTransform_t getTransform = 0;
getPosition_t getPosition = 0;
setPosition_t setPosition = 0;

u64 mySpawnPageUpdate(u64 sp, u64 zero)
{
    u64 mainCamera = getMainCamera(0);
    u64 cameraTrans = getTransform(mainCamera, 0);
    u64 mainCameraTrans = getTransform(mainCamera, 0);
    vector3 pos = getPosition(cameraTrans, 0);
    const float SPEED = 0.5f;
    if (GetAsyncKeyState('J'))
        pos.x -= SPEED;
    if (GetAsyncKeyState('L'))
        pos.x += SPEED;
    if (GetAsyncKeyState('I'))
        pos.y += SPEED;
    if (GetAsyncKeyState('K'))
        pos.y -= SPEED;
    if (GetAsyncKeyState('U'))
        pos.z += SPEED;
    if (GetAsyncKeyState('O'))
        pos.z -= SPEED;
    if (GetAsyncKeyState('P'))
    {
        pos.x = 56236.000f;
        pos.y = 17796.000f;
    }
    setPosition(cameraTrans, &pos, 0);
    return orgSpawnPageUpdate(sp, zero);
}

void hack()
{
    AllocConsole();
    freopen("CONIN$", "r", stdin);
    freopen("CONOUT$", "w", stdout);
    freopen("CONOUT$", "w", stderr);

     base = (u64)GetModuleHandleA("gameassembly.dll");
     getMainCamera = (getMainCamera_t)(base + 0x7CAB30);
     getTransform = (getTransform_t)(base + 0x7E2B40);
     getPosition = (getPosition_t)(base + 0x7F8860);
     setPosition = (setPosition_t)(base + 0x7F8DC0);

    printf("[+] Base: 0x%llx\n", base);

    MH_Initialize();
    MH_CreateHook((LPVOID)(base + 0x1F3320), (LPVOID*)mySpawnPageUpdate, (LPVOID*)&orgSpawnPageUpdate);
    MH_EnableHook((LPVOID)(base + 0x1F3320));
}

BOOL WINAPI DllMain( HINSTANCE hinstDLL,  DWORD fdwReason, LPVOID lpReserved)
{
    switch(fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            // Initialize once for each new process.
            CreateThread(0, 0, (LPTHREAD_START_ROUTINE)hack, 0, 0, 0);
            // Return FALSE to fail DLL load.
            break;
    }
    return TRUE;  // Successful DLL_PROCESS_ATTACH.
}