Decompiling - nbreeze/closers-rev GitHub Wiki

.CMF Archives

Most of the time, you'll be wanting to snoop into the game's content. If you use QuickBMS, I have attached an updated .bms script that should handle most .CMF files. Credit goes to Ekey on zenhax for the initial version of the script.

Decryption

Each .CMF contains an array of file entries, beginning with a int32 specifying the amount of entries and then the entry data. Both are encrypted with an XOR + high/low byte swap algorithm, so before traversing, it has to be decrypted first. Here is a C++ implementation of the encryption/decryption algorithm:

void xor_swap_encrypt( void * buffer, unsigned int bufferSize, unsigned int keys[3] ) {
    unsigned int * ptr = (unsigned int *)buffer;
    unsigned int numElements = bufferSize >> 2;
    for (unsigned i = 0; i < numElements; i++) {
        unsigned value = ptr[i];

        // Swap the most/least significant bytes.
        value = ((value >> 24) & 0xff) | (value << 24) | (value & 0xFFFF00);

        // XOR
        value ^= keys[i % 3];
        ptr[i] = value;
    }
}

The encryption scheme makes use of three keys. For .CMF files specifically, these keys are 0xac9372de, 0x8469af01, and 0xdc39628f. First, decrypt the entry count first, and only the entry count. Then, advance and start the decryption process again, this time decrypting all of the entries. Here is a simple way of doing this process in C++:

// Assume we have some pointer "buffer" at the entry count value already.
unsigned int keys[] = { 0xac9372de, 0x8469af01, 0xdc39628f };
unsigned int * ptr = (unsigned int *)buffer;

// Decrypt entry count ONLY.
xor_swap_encrypt( ptr, 4, keys );
unsigned int entry_count = *ptr;

// Advance.
ptr++;

// totalEntrySize is equal to entry_count * 528.
unsigned int totalEntrySize = entry_count * sizeof(CMFFileEntry); 

// Restart decryption.
xor_swap_encrypt( ptr, totalEntrySize, keys );

For more technical detail of the file format itself, see the .CMF page.

.LUA, .XET, and .TET

.LUA, .TET, and .XET are files composed of compiled Lua bytecode under version 5.1.5.

To decompile, use a Lua decompiler such as luadec to convert bytecode to readable format.

:warning: In .CMF files, these filetypes (along with .NTF files) are encrypted. Decrypt first using AES-256 before decompiling. Find both the key and the IV by unpacking the game executable and do some digging.

:warning: In the case of luadec, you have to compile luadec with Lua 5.1.5, not 5.1. If you don't do this, decompilation will fail.

Exceptions

CLIENT_CODE_*.LUA - compressed only; use INFLATE.

DAT_WEB/DOWN_FILE_LIST.LUA - encrypted with xor_swap_encrypt and SEED.

Appendix

A.1

# Closers Online (CBT/OBT) (CMF format) 0.4
# Written by Ekey (h4x0r), updated by nbreeze
# 
# script for QuickBMS http://quickbms.aluigi.org

# World + Korean Client
# https://closers.enmasse.com/
# https://closers.nexon.com
# NK ENGINE COMBINE FILE ver. 1.0
#idstring "\x4E\x00\x4B\x00\x20\x00\x45\x00\x4E\x00\x47\x00\x49\x00\x4E\x00\x45\x00\x20\x00\x43\x00\x4F\x00\x4D\x00\x42\x00\x49\x00\x4E\x00\x45\x00\x20\x00\x46\x00\x49\x00\x4C\x00\x45\x00\x20\x00\x76\x00\x65\x00\x72\x00\x2E\x00\x20\x00\x31\x00\x2E\x00\x30\x00\x00\x00"

# ENGINE COMBINE FILE ver. 2
#idstring "\x43\x00\x4F\x00\x4D\x00\x42\x00\x49\x00\x4E\x00\x45\x00\x20\x00\x46\x00\x49\x00\x4C\x00\x45\x00\x20\x00\x76\x00\x65\x00\x72\x00\x2E\x00\x32\x00"

# Indonesian Client (closed)
# https://closers.megaxus.com
# COMBINE FILE ver. 1.0 - Also can be nulled for SCRIPT_PACK.CMF archive
#idstring "\x43\x00\x4F\x00\x4D\x00\x42\x00\x49\x00\x4E\x00\x45\x00\x20\x00\x46\x00\x49\x00\x4C\x00\x45\x00\x20\x00\x76\x00\x65\x00\x72\x00\x2E\x00\x20\x00\x31\x00\x2E\x00\x30\x00\x00\x00"

# Keys for decrypting entry table info
set ENTRY_KEY1 long "0xAC9372DE"
set ENTRY_KEY2 long "0x8469AF01"
set ENTRY_KEY3 long "0xDC39628F"

# Function for decrypt > pBuffer, dwSize, dwKey1, dwKey2, dwKey3
set MEMORY_FILE2 binary "\x55\x8b\xec\xb8\xab\xaa\xaa\x2a\xf7\x6d\x0c\xd1\xfa\x8b\xc2\xc1\xe8\x1f\x03\xc2\x89\x45\x0c\x85\xc0\x7e\x7b\x53\x8b\x5d\x08\x56\x57\x83\xc3\x08\x8b\x53\xf8\x8b\x73\xfc\x8b\x3b\x8b\xca\xc1\xf9\x18\x81\xe1\xff\x00\x00\x00\x8b\xc2\xc1\xe0\x18\x0b\xc8\x81\xe2\x00\xff\xff\x00\x0b\xca\x33\x4d\x10\x8b\xc6\x89\x4b\xf8\x8b\xce\xc1\xf9\x18\x81\xe1\xff\x00\x00\x00\xc1\xe0\x18\x0b\xc8\x81\xe6\x00\xff\xff\x00\x0b\xce\x33\x4d\x14\x8b\xc7\x89\x4b\xfc\x8b\xcf\xc1\xf9\x18\x81\xe1\xff\x00\x00\x00\xc1\xe0\x18\x0b\xc8\x81\xe7\x00\xff\xff\x00\x0b\xcf\x33\x4d\x18\xff\x4d\x0c\x89\x0b\x8d\x5b\x0c\x75\x91\x5f\x5e\x5b\x5d\xc3"

# Function for wipe unused data > pBuffer, dwSize
set MEMORY_FILE3 binary "\x55\x8b\xec\xb8\xe1\x83\x0f\x3e\xf7\x6d\x0c\x56\x8b\x75\x08\xc1\xfa\x07\x57\x8b\xfa\xc1\xef\x1f\x03\xfa\x85\xff\x7e\x2d\x8b\xff\x32\xc9\x33\xc0\x66\x83\x3c\x46\x00\x74\x07\x80\xf9\x01\x75\x0a\xeb\x02\xb1\x01\x33\xd2\x66\x89\x14\x46\x40\x3d\xff\x00\x00\x00\x7c\xe2\x81\xc6\x10\x02\x00\x00\x4f\x75\xd5\x5f\x5e\x5d\xc3"

goto 0x64
get FILES long
savepos TABLE_OFFSET

xmath FILES "((FILES << 24) & 0xFF000000) | ((FILES >> 0) & 0x0000FF00) | ((FILES << 0) & 0x00FF0000) | ((FILES >> 24) & 0x000000FF)"
math FILES ^= ENTRY_KEY1

set TABLE_SIZE long FILES
math TABLE_SIZE *= 528

set DATASIZE long 104
math DATASIZE += TABLE_SIZE

log MEMORY_FILE TABLE_OFFSET TABLE_SIZE
calldll MEMORY_FILE2 0 cdecl RET MEMORY_FILE TABLE_SIZE ENTRY_KEY1 ENTRY_KEY2 ENTRY_KEY3
calldll MEMORY_FILE3 0 cdecl RET MEMORY_FILE TABLE_SIZE

for i = 0 < FILES
    getdstring UNAME 512 MEMORY_FILE
    set NAME UNICODE UNAME
    get SIZE long MEMORY_FILE
    get ZSIZE long MEMORY_FILE
    get OFFSET long MEMORY_FILE
    math OFFSET += DATASIZE
    get FLAG long MEMORY_FILE
	
    set EXT extension NAME
	
    if EXT == "FX"
        set FLAG long 2
    elseif EXT == "LUA"
        set FLAG long 2
    elseif EXT == "TET"
        set FLAG long 2
    elseif EXT == "XET"
        set FLAG long 2
    elseif EXT == "NTF"
        set FLAG long 2
    endif
	   
    if FLAG == 0
       log NAME OFFSET SIZE
    elseif FLAG == 1
       clog NAME OFFSET ZSIZE SIZE
    elseif FLAG == 2
       log NAME OFFSET ZSIZE

       # Script files use AES-256 encryption.
       # log MEMORY_FILE4 0 0
       # putvarchr MEMORY_FILE4 ZSIZE 0 
       # comtype base64
       # clog MEMORY_FILE4 OFFSET ZSIZE ZSIZE
       # get ZSIZE asize MEMORY_FILE4
       # encryption aes_256_cbc SCRIPT_KEY SCRIPT_IV 0 32
       # log MEMORY_FILE4 0 ZSIZE MEMORY_FILE4
       # encryption "" ""

       # Temporary set unzip dynamic because not all data decompressed while extracting files. Decompress it manually, if you want (Use OffZip for example) ^^
       # comtype unzip_dynamic
       # clog NAME 0 ZSIZE SIZE MEMORY_FILE4
    endif
next i