ACS 2024: A Silver Journey in Ha Long with KMA.Qrange

#ctf#writeup#reverse-engineering#acs

Recently, my team and I were chosen by our university to represent them as KMA.Qrange at the ASEAN Cyber Shield (ACS) 2024 in Ha Long. We finished in 2nd place in the Student Division.

The competition, organized by the Korea Internet & Security Agency (KISA), featured two divisions: Open and Student. The event consisted of a Qualifier followed by a Final, where the top 5 teams from the Qualifiers advanced.

Qualifiers

The first round went well. Our team was consistent across all categories, and we finished the qualifiers in 1st place.

Qualifiers Scoreboard

The Finals

The finals were more challenging. While we performed well in the Jeopardy-style challenges, we were less prepared for the Attack & Defense (Atk/Def) portion. UIT.Oppenheimer performed better there and took the lead.

We ended up in 2nd place overall, winning a $10,000 prize. It was a solid result for the team.

Finals Scoreboard

Other Experiences

Beyond the competition, it was a great opportunity to meet peers from across Southeast Asia. I also had the chance to meet Hiếu PC and some members of the Vietnam cybersecurity community.


Write-up

My primary focus during the competition was on Reverse Engineering (RE); besides that, I also supported the team in other categories.

[Qual] rev/Not So Easy CrackMe

The program processes the input in blocks of four characters, then compares each transformed block against a predefined target value.

There are 11 blocks in total.

The pseudocode appears complex, but the assembly is cleaner.

image

All the encryption instructions are reversible.

I decided to reproduce the process in assembly using Python (Unicorn). Reversing the original flow produces a decryptor that recovers one 4-byte chunk at a time.

The script is as follows:

from unicorn import *
from unicorn.x86_const import *
from keystone import *

# Initialize array
arr0 = [  0x18, 0x19, 0x5A, 0x5D, 0xA4, 0xA4, 0xD7, 0xD2, 0x0B, 0x16,
    0xED, 0xE6, 0xD8, 0xF1, 0xC9, 0x17, 0x49, 0x32, 0x0F, 0xC4,
    0x1F, 0x0A, 0x25, 0x62, 0x27, 0x9C, 0xC2, 0x70, 0xC6, 0xEB,
    0x02, 0x25, 0x11, 0xCC, 0x96, 0xCE, 0xD9, 0x48, 0xC9, 0x6E,
    0x52, 0x7C, 0xBD, 0x9A, 0x00, 0x00, 0x00, 0x00]

arr1 = [  0x0E, 0x50, 0x22, 0xFA, 0x1C, 0xD9, 0x0E, 0x63, 0x26, 0xD1, 
  0x7F, 0x4B, 0x66, 0x3E, 0x13, 0x8D, 0xB5, 0x97, 0x22, 0xD0, 
  0xBD, 0xF9, 0xD2, 0x81, 0xE1, 0xCB, 0xF1, 0x0C, 0xFE, 0xCA, 
  0x05, 0x6B, 0x9C, 0xCB, 0x08, 0xF0, 0xF6, 0x27, 0x0F, 0x4C, 
  0x98, 0xCB, 0xF8, 0x39, 0x00, 0x00, 0x00, 0x00]

arr2 = [  0x86, 0x0D, 0xD8, 0x59, 0xD7, 0xBD, 0xD0, 0xDC, 0x67, 0x8A, 
  0xC3, 0xFD, 0x61, 0x4E, 0x50, 0x12, 0xE0, 0xF5, 0xD2, 0xE0, 
  0x02, 0x00, 0x0A, 0x93, 0x11, 0xD4, 0x0A, 0x1B, 0x85, 0xD9, 
  0xE9, 0x52, 0xF8, 0x04, 0x66, 0xAC, 0x6E, 0x08, 0x4B, 0xD0, 
  0x12, 0xF9, 0xDD, 0x90, 0x00, 0x00, 0x00, 0x00]

arr3 =[  0x24, 0x98, 0xAF, 0x22, 0x8E, 0x17, 0xF2, 0x22, 0x20, 0x3C, 
  0x0B, 0xFD, 0x0C, 0xE7, 0x07, 0xB1, 0x78, 0xDD, 0x41, 0xDC, 
  0x9A, 0x50, 0xBE, 0xC8, 0x23, 0x55, 0x93, 0x52, 0xDF, 0xC4, 
  0x8D, 0xFA, 0x58, 0x0A, 0x68, 0xC3, 0x8E, 0xD8, 0x11, 0x56, 
  0x2F, 0xF2, 0x28, 0x0F, 0x00, 0x00, 0x00, 0x00]

arr4 = [  0xF8, 0x3C, 0xFA, 0x45, 0x8E, 0x6C, 0xBF, 0x89, 0xCB, 0xB0, 
  0xDD, 0xD8, 0x66, 0xDC, 0x79, 0x8E, 0x99, 0x6D, 0x2C, 0xDA, 
  0xBE, 0x05, 0x6A, 0x36, 0x08, 0x83, 0x93, 0xCF, 0x58, 0x0D, 
  0xD0, 0x0E, 0x08, 0x54, 0xD7, 0xB4, 0xEF, 0x71, 0x9B, 0x12, 
  0x3A, 0x2A, 0x06, 0x7E, 0x00, 0x00, 0x00, 0x00]

arr5 = [  0x5B, 0x67, 0x3B, 0x57, 0x23, 0x58, 0xA3, 0x94, 0x06, 0xA0, 
  0xF5, 0x31, 0x89, 0xD6, 0x8A, 0xCA, 0x87, 0x0F, 0xC6, 0x59, 
  0x85, 0x69, 0xBD, 0x07, 0xFF, 0x47, 0x40, 0x41, 0xF1, 0xBE, 
  0xA0, 0x5B, 0x6E, 0x46, 0x16, 0x3D, 0x16, 0x6F, 0x07, 0xB0, 
  0x12, 0xAC, 0xB4, 0x74, 0x00, 0x00, 0x00, 0x00]

arr6 = [  0xAB, 0xC1, 0xFA, 0x68, 0x3E, 0xB0, 0x9D, 0x9E, 0x2C, 0x17, 
  0xB2, 0x90, 0x85, 0xD4, 0xA0, 0x21, 0x00, 0x16, 0x29, 0x4F, 
  0xFB, 0x9D, 0xD9, 0x2C, 0xE0, 0x28, 0x9B, 0x91, 0x9C, 0x81, 
  0x57, 0x61, 0x77, 0x29, 0x03, 0xDB, 0x1E, 0xF6, 0x3B, 0xC1, 
  0x91, 0x45, 0x42, 0x54, 0x00, 0x00, 0x00, 0x00]

arr7 = [  0x5C, 0x7F, 0xBF, 0x21, 0x85, 0x39, 0x15, 0xCB, 0x3B, 0xE1, 
  0x3E, 0x3E, 0x4E, 0x19, 0x2F, 0xE2, 0x85, 0x99, 0xB9, 0x6B, 
  0x38, 0x67, 0x20, 0xBE, 0xEC, 0xB8, 0xD2, 0x76, 0xB8, 0xEC, 
  0xCA, 0x7D, 0x4E, 0x26, 0xD8, 0x6C, 0xB3, 0x9D, 0xCC, 0x34, 
  0x73, 0xEA, 0x3E, 0xD9, 0x00, 0x00, 0x00, 0x00]

arr8 = [  0x6F, 0x9E, 0x11, 0x6E, 0x04, 0x74, 0x8F, 0x27, 0x41, 0xFB, 
  0x08, 0x1B, 0xD1, 0x56, 0x05, 0x62, 0x65, 0x6A, 0xB4, 0xE5, 
  0x96, 0xBE, 0xB6, 0xB2, 0x96, 0x41, 0xF9, 0xDC, 0x44, 0x6D, 
  0x9E, 0x06, 0x72, 0x3C, 0x66, 0xA1, 0x8C, 0x7B, 0xAB, 0x47, 
  0x40, 0x62, 0x2C, 0xB4, 0x00, 0x00, 0x00, 0x00]

arr9 = [  0x38, 0xBE, 0xA1, 0x56, 0x79, 0x87, 0xD0, 0x9C, 0x93, 0x7A, 
  0xEF, 0xF6, 0x92, 0xE0, 0x45, 0xB9, 0xAA, 0x30, 0xF5, 0x68, 
  0x95, 0x87, 0x81, 0x19, 0x9F, 0xA1, 0x5C, 0x5F, 0x72, 0xD8, 
  0x79, 0xBE, 0xD1, 0xDD, 0x4E, 0x3B, 0xCF, 0xA0, 0x46, 0x0C, 
  0x6F, 0xCC, 0xC2, 0x8D, 0x00, 0x00, 0x00, 0x00]

arr10 = [  0x80, 0x38, 0x39, 0xC4, 0x63, 0x26, 0x5E, 0xB7, 0xC2, 0x86, 
  0x77, 0xEC, 0xBF, 0x59, 0x74, 0x7D, 0x7D, 0x7A, 0x61, 0x3A, 
  0x85, 0x8D, 0x51, 0xA6, 0x3C, 0xAC, 0x58, 0x10, 0xF4, 0x24, 
  0x61, 0xDE, 0x74, 0x39, 0x35, 0xFA, 0xB2, 0x2C, 0xE0, 0x4A, 
  0x42, 0xB7, 0x65, 0xE3, 0x00, 0x00, 0x00, 0x00]


target = [
    0x3AA8F34A, 0x415DFC90, 0x0C88AC87B, 0x0B629234C,
    0x8D12FE48, 0x2437D395, 0x19C22544, 0x7A41F1FA,
    0x7F8D8AA3, 0x8FE70E70, 0x8F5EE71E, 0x00, 0x00, 0x00
]

def run_code(arr, RBX, R15):

    # Initialize emulator in x86-64 mode
    mu = Uc(UC_ARCH_X86, UC_MODE_64)

    # Map memory for this emulation - aligned to 4KB pages
    CODE_ADDR = 0x1000000
    ARRAY_ADDR = 0x2000000
    PAGE_SIZE = 4 * 1024
    
    # Map 2MB memory regions
    mu.mem_map(CODE_ADDR, 2 * 1024 * 1024)
    mu.mem_map(ARRAY_ADDR, 2 * 1024 * 1024)

    # Write array to memory
    mu.mem_write(ARRAY_ADDR, bytes(arr))

    # Initialize registers
    mu.reg_write(UC_X86_REG_R15, R15)
    mu.reg_write(UC_X86_REG_RBX, RBX)
    mu.reg_write(UC_X86_REG_RAX, ARRAY_ADDR + 0x2c)

    # Initialize Keystone
    ks = Ks(KS_ARCH_X86, KS_MODE_64)

    r_code0 = """
        xor ebx, r15d
        ror bx, 2
        sub rax, 2
        xor r15w, word ptr [rax]
        sub r15d, ebx
        ror r15w, 9
    """

    r_code1 = """
        sub rax, 8
        xor ebx, r15d
        ror bx, 2
        xor r15w, [rax+6]
        sub r15d, ebx
        ror r15w, 9

        xor ebx, r15d
        ror bx, 2
        xor r15w, [rax+4]
        sub r15d, ebx
        ror r15w, 9

        xor ebx, r15d
        ror bx, 2
        xor r15w, [rax+2]
        sub r15d, ebx
        ror r15w, 9

        xor ebx, r15d
        ror bx, 2
        xor r15w, [rax]
        sub r15d, ebx
        ror r15w, 9
    """

    # Assembly code
    # code = 2 * code0 + 5 * code1 
    code = 5 * r_code1 + 2 * r_code0

    # Compile assembly code
    encoding, count = ks.asm(code)
    
    # Write machine code to memory
    mu.mem_write(CODE_ADDR, bytes(encoding))

    # Emulate the code
    try:
        mu.emu_start(CODE_ADDR, CODE_ADDR + len(encoding))
    except UcError as e:
        print("ERROR: %s" % e)

    # Final register values
    r15_final = mu.reg_read(UC_X86_REG_R15)
    rbx_final = mu.reg_read(UC_X86_REG_RBX)

    # Convert the values to bytes and extract the lower 4 bytes
    bytes_RBX = [(rbx_final >> (8 * i)) & 0xFF for i in range(4)]
    bytes_R15 = [(r15_final >> (8 * i)) & 0xFF for i in range(4)]

    # Combine the bytes into one list and extract the relevant 2 bytes from R15 and RBX
    bytes_combined = [bytes_R15[1], bytes_R15[0], bytes_RBX[1], bytes_RBX[0]]

    # Convert bytes to ASCII characters and join them into a string
    ascii_string = ''.join(chr(byte) for byte in bytes_combined)
    
    return ascii_string
    

if __name__ == "__main__":
    flag = ""
    for i in range(11):
        R15 = target[i] & 0xFFFF
        RBX = (target[i] >> 16) & 0xFFFF
        flag += run_code(eval(f"arr{i}"), RBX, R15)

    print(f"Flag: {flag}")

Recovered flag: ACS{sp3ck_1s_n0t_s0_h4rd_but_rust_1s_h4rd!!}

[Qual] rev/2048

2048 is a popular sliding-tile puzzle game where you combine numbered tiles on a grid to reach the value 2048.

In this qualifier, a score of 10,000,000 is required to reveal the flag, which is impossible to achieve through normal play. I used Cheat Engine to boost the score and reveal the flag.

image

Flag: ACS{3d382b41173fe03f65398f49ecb2f63caedf22a3}

[Qual] rev/cs1388

The main logic is in a file named library, which decompiles to:

-- filename: 
-- version: lua54
-- line: [0, 0] id: 0
return {
  flag = function(r0_1)
    -- line: [1, 17] id: 1
    local r1_1 = "!@#"
    local r2_1 = "q\xf2\xbcf\xb3\xae:p\x95%\xf1\xd4v\xb3\x9a\xb6\xa3ص\xb0\xaf9\xaet"
    local r3_1 = ""
    local r4_1 = 1
    for r8_1 = 1, #r0_1, 1 do
      local r11_1 = (r0_1:byte(r8_1) + r1_1:byte(((r8_1 + -1) % #r1_1 + 1)) * r4_1) % 256
      r4_1 = (r4_1 + r11_1) % 16
      r3_1 = r3_1 .. string.char(r11_1)
    end
    local r5_1 = r3_1 == r2_1
  end,
}

The program processes the input byte by byte, making it straightforward to brute-force each key character separately.

Script for brute-forcing the key:

local function solve_flag()
    -- Target string from original code
    local target = "q\xf2\xbcf\xb3\xae:p\x95%\xf1\xd4v\xb3\x9a\xb6\xa3ص\xb0\xaf9\xaet"
    local key = "!@#"
    local flag = ""
    local multiplier = 1

    -- Try to solve for each character
    for i = 1, #target do
        local target_byte = target:byte(i)
        local key_byte = key:byte((i-1) % #key + 1)
        local found = false

        -- Only test printable ASCII
        for try = 32, 126 do
            local test = (try + key_byte * multiplier) % 256
            if test == target_byte then
                flag = flag .. string.char(try)
                multiplier = (multiplier + target_byte) % 16
                found = true
                break
            end
        end

        -- Exit if no printable character found
        if not found then
            print("No solution found")
            return nil
        end
    end

    -- Print results
    print("Flag: " .. flag)

    return flag
end

solve_flag()

Flag: Pr0f3sS0r_10v3_Sc41pt1ng

Connect to the server to retrieve the real flag.

image

Flag: ACS{feeea13a932b23ca408ed3cbf01e723e3726343a53bd67776caf9601cd20d637}

[Qual] rev/SecureChat

Despite its name, this challenge isn’t very secure; all communications are simply XORed.

First, the server generates a 16-byte secret key. This key is shared with the client by XORing it with 129FE83152B29A1DA9B00D42D63C771E and sending the result over the connection. Once set up, any message is XORed with the secret key before being sent.

XORing it back retrieves the original messages, one of which contains the flag.

from itertools import cycle

def xor(a, b):
    return bytes([x ^ y for x, y in zip(a, cycle(b))])

kek = bytes.fromhex('129FE83152B29A1DA9B00D42D63C771E')

key_enc = bytes.fromhex('8c0952afa58452c55eb837e078ff8a2b')

key = xor(kek, key_enc)

dat = [
0xd9, 0xe4, 0xdf, 0xff, 0x83, 0x1a, 0xe8, 0xac, 
0x9f, 0x69, 0x54, 0xc9, 0xdd, 0xed, 0xdd, 0x74, 
0xf2, 0xe4, 0xd3, 0xf9, 0x9f, 0x42, 0xe4, 0xf8, 
0x83, 0x60, 0x5f, 0x82, 0xc0, 0xa6, 0x8a, 0x15, 
0xee, 0xf7, 0xc9, 0xed, 0x80, 0x59, 0xba, 0xbc, 
0xd7, 0x6e, 0x55, 0xd0, 0x8e, 0xb7, 0x95, 0x50, 
0xbe, 0xe0, 0xdb, 0xeb, 0x9b, 0x42, 0xe8, 0xb1, 
0x84, 0x28, 0x18, 0xe3, 0xed, 0x90, 0x86, 0x71, 
0xae, 0xc9, 0xf4, 0xf1, 0xa3, 0x69, 0xbd, 0x8b, 
0xc4, 0x57, 0x62, 0x92, 0xdc, 0x9c, 0x9b, 0x05, 
0xcc, 0xc9, 0xdf, 0xd0, 0x94, 0x64, 0x91, 0xa8, 
0xc0, 0x39, 0x55, 0xcc, 0xf1, 0xf7, 0xb1, 0x72, 
0xae, 0xe4, 0x8b, 0xca, 0x9f, 0x7b, 0xb5, 0xfa, 
0xd9, 0x28, 0x70, 0xd7, 0xdd, 0xb7, 0xdd, 0x58, 
0xff, 0xfd, 0xdf, 0xbe, 0x84, 0x43, 0xba, 0xbd, 
0xd7, 0x71, 0x55, 0xd7, 0x8e, 0xb6, 0x8d, 0x51, 
0xff, 0xe2, 0xdf, 0xbe, 0x8e, 0x59, 0xbd, 0xaa, 
0xd7, 0x7a, 0x5f, 0xc1, 0xc1, 0xb1, 0x99, 0x46, 
0xbe, 0xf7, 0xd4, 0xfa, 0xd7, 0x52, 0xa7, 0xb6, 
0x56, 0xa7, 0x4e, 0x82, 0xdd, 0xab, 0x9c, 0x47, 
0xfb, 0xb6, 0xd3, 0xea, 0xd7, 0x41, 0xa1, 0xac, 
0x9f, 0x28, 0x5b, 0xcc, 0xd7, 0xac, 0x93, 0x50, 
0xbe, 0xf3, 0xd6, 0xed, 0x92, 0x16, 0xa7, 0xad, 
0x83, 0x7b, 0x53, 0xc6, 0xcb, 0xe3, 0x89, 0x5d, 
0xfb, 0xb6, 0xce, 0xfb, 0x96, 0x5b, 0xe6 ]

print(xor(bytes(dat), key).decode('utf-8', 'ignore'))

Recovered message: “Great, thanks. Alright, the new password for the vault is "ACS{D0_NoT_uS3_X0r_f0R_eNcRYp71on_4LG0r1ThM}". Just make sure you update your records and don’t share it with anyone else outside the team.”

Flag: ACS{D0_NoT_uS3_X0r_f0R_eNcRYp71on_4LG0r1ThM}

[Qual] misc/Lutella

The server allows running Lua scripts to read the flag, but it blocks common Lua keywords for file access. I found that I could access open via the safe_method table.

Code:

local file = debug.getregistry().safe_method.open("./flag", "r"); if file then local flag = file:read("*a") print(flag) end

image

Flag: ACS{Toast_and_chocolate_are_a_fantastic_combination}

[Qual] misc/Hi Alien

The objective was to produce an executable that matches a specific YARA rule. To achieve this, I edited System32/cmd.exe using CFF Explorer.

First, I added sections and modified the .acs section to match the target entropy.

image

Then, I edited the import table.

image

Finally, I updated the CompanyName using Resource Hacker.

image

Submitting the file retrieved the flag.

image

Flag: ACS{97d9bad8791993f95050bf4668f3e1351f39b21fafeb986822915ecc71d75f77}

[Final] rev/AntiKeygen

The program confuses IDA by adding E8 after 75 01 (jmp 1 byte forward), a common anti-disassembler trick.

image

I wrote a simple script to replace all occurrences of 75 01 E8 with 75 01 90 (90 is the opcode for nop). After patching, the disassembly becomes much cleaner.

image

Pressing F5 reveals the clear logic.

image

The flag is solved by brute-forcing each character.

#include <stdio.h>
#include <stdint.h>
#include <string.h>

// Function prototypes
void transform1(char* a1);
void transform2(char* a1);
void swap_nibbles(char* a1);
char xor_transform(char* a1);
int rotate_left(uint8_t a1, char a2);
int rotate_right(uint8_t a1, char a2);

// Print helper function
void print_hex(const char* label, const char* data) {
    printf("%s", label);
    for (int i = 0; i < 27; i++) {
        printf("%02X ", (unsigned char)data[i]);
    }
    printf("\n");
}

// Implementation of transform1
void transform1(char* a1) {
    char v2[27] = { 3,7,5,3,1,5,9,6,8,2,5,1,8,3,9,5,7,3,6,4,7,3,6,5,3,7,5 };
    int v4;

    for (int i = 0; i <= 26; i++) {
        v4 = (uint8_t)a1[i] + (uint8_t)v2[i];
        if (v4 > 122)
            v4 = v4 % 122 + 48;
        a1[i] = v4;
    }
}

// Implementation of transform2
void transform2(char* a1) {
    char v2[27] = { 1,2,1,5,3,3,4,6,1,5,4,6,4,5,2,6,2,5,4,4,5,1,5,4,5,5,6 };

    for (int i = 0; i <= 26; i++) {
        if (i % 2)
            a1[i] = rotate_left((uint8_t)a1[i], v2[i]);
        else
            a1[i] = rotate_right((uint8_t)a1[i], v2[i]);
    }
}

// Implementation of rotate_left
int rotate_left(uint8_t a1, char a2) {
    return ((a1 << (8 - a2)) | (a1 >> a2)) & 0xFF;
}

// Implementation of rotate_right
int rotate_right(uint8_t a1, char a2) {
    return ((a1 >> (8 - a2)) | (a1 << a2)) & 0xFF;
}

// Implementation of swap_nibbles
void swap_nibbles(char* a1) {
    for (int i = 0; i <= 26; i++) {
        a1[i] = ((a1[i] & 0x0F) << 4) | ((uint8_t)a1[i] >> 4);
    }
}

// Implementation of xor_transform
char xor_transform(char* a1) {
    for (int i = 0; i <= 26; i++) {
        a1[i] ^= 0x7C;
    }
    for (int i = 0; i <= 26; i++) {  // Fixed: was using i instead of j
        a1[i] ^= 0x4E;
    }
    return a1[26];
}

// Main transform function with detailed output
int transform(char* a1) {
    transform1(a1);
    transform2(a1);
    swap_nibbles(a1);
    int result = xor_transform(a1);
    return result;
}

int main() {
    char input[28] = "???????????????????????????";  // 27 characters
    char working[28];
    char target[28] = {
          0xB4, 0xAB, 0x3B, 0xA9, 0x85, 0xFA, 0x5E, 0x24, 0xB5, 0x90,
          0x42, 0x2A, 0x00, 0x97, 0x3D, 0x2B, 0xFC, 0x9A, 0x0F, 0x07,
          0xFE, 0x41, 0x40, 0x6C, 0xE6, 0x14, 0x7F
    };
    input[27] = '\0';  // Null terminate the string

    printf("Starting bruteforce...\n");
    printf("Target: ");
    print_hex("", target);

    // Try each position
    for (int pos = 0; pos < 27; pos++) {
        printf("\nBruteforcing position %d\n", pos);
        int found = 0;

        // Try each possible character (printable ASCII)
        for (unsigned char c = 32; c < 127 && !found; c++) {
            // Make a copy of current working input
            memcpy(working, input, 28);
            working[pos] = c;

            // Apply transformation
            transform(working);

            // Check if this position matches target
            if ((unsigned char)working[pos] == (unsigned char)target[pos]) {
                printf("Found match at position %d: char '%c' (0x%02X)\n",
                    pos, c, c);
                input[pos] = c;
                found = 1;
            }
        }

        if (!found) {
            // If no printable ASCII worked, try all byte values
            for (unsigned int c = 0; c < 256 && !found; c++) {
                memcpy(working, input, 28);
                working[pos] = (char)c;

                transform(working);

                if ((unsigned char)working[pos] == (unsigned char)target[pos]) {
                    printf("Found match at position %d: byte 0x%02X\n",
                        pos, (unsigned char)c);
                    input[pos] = c;
                    found = 1;
                }
            }
        }

        if (!found) {
            printf("No match found for position %d!\n", pos);
            return 1;
        }

        // Print current progress
        printf("Current input: ");
        for (int i = 0; i <= pos; i++) {
            if (input[i] >= 32 && input[i] <= 126)
                printf("%c", input[i]);
            else
                printf("\\x%02X", (unsigned char)input[i]);
        }
        printf("\n");
    }

    // Final verification
    printf("\nFinal input found!\n");
    printf("ASCII: ");
    for (int i = 0; i < 27; i++) {
        if (input[i] >= 32 && input[i] <= 126)
            printf("%c", input[i]);
        else
            printf("\\x%02X", (unsigned char)input[i]);
    }
    printf("\n");

    printf("Hex: ");
    print_hex("", input);

    // Verify the result
    memcpy(working, input, 28);
    transform(working);
    printf("\nFinal output: ");
    print_hex("", working);

    // Verify each position
    int matches = 1;
    for (int i = 0; i < 27; i++) {
        if ((unsigned char)working[i] != (unsigned char)target[i]) {
            printf("Mismatch at position %d: got 0x%02X, expected 0x%02X\n",
                i, (unsigned char)working[i], (unsigned char)target[i]);
            matches = 0;
        }
    }

    if (matches)
        printf("\nSuccess! All positions match the target.\n");
    else
        printf("\nError: Output doesn't match target!\n");

    return 0;
}

ASCII: 1_C4n_cR4Ck_*H3_4N71_k3YgEN

The old version of the challenge had a minor issue with the character *

Flag: ACS{1_C4n_cR4Ck_TH3_4N71_k3YgEN}

[Final] rev/Transformation algorithm

This CrackMe combines XTEA encryption with AES S-box substitution. It takes user input, encrypts it using modified XTEA (32 rounds), applies the AES S-box substitution, and finally compares the result with a hardcoded value.

image

The solution is to reverse the S-box substitution using a lookup table, then decrypt XTEA by reversing the operations.

from typing import List
import struct

# S-box from the original program
SBOX = bytes([
  0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01,
  0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D,
  0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4,
  0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC,
  0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, 0x04, 0xC7,
  0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2,
  0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E,
  0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
  0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB,
  0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, 0xD0, 0xEF, 0xAA, 0xFB,
  0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C,
  0x9F, 0xA8, 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5,
  0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C,
  0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D,
  0x64, 0x5D, 0x19, 0x73, 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A,
  0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
  0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3,
  0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 0xE7, 0xC8, 0x37, 0x6D,
  0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A,
  0xAE, 0x08, 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6,
  0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, 0x70, 0x3E,
  0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9,
  0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9,
  0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
  0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99,
  0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
])

# Target encrypted bytes
TARGET = bytes([
    0xE5, 0x1A, 0x8F, 0xA6, 0xC9, 0x3D, 0x18, 0x70, 0xE6, 0xCE,
    0x03, 0xC3, 0x33, 0x58, 0xC8, 0x21, 0x44, 0x46, 0x35, 0x24,
    0xCF, 0xFB, 0x7E, 0xA2, 0x4F, 0x2D, 0x0B, 0xBC, 0x42, 0xAD,
    0x55, 0x60
])

# Encryption key
KEY = [0x1020304, 0x5060708, 0x90A0B0C, 0x0D0E0F10]

def inv_sbox(val: int) -> int:
    inv_table = {v: k for k, v in enumerate(SBOX)}
    b0 = inv_table[val & 0xFF]
    b1 = inv_table[(val >> 8) & 0xFF]
    b2 = inv_table[(val >> 16) & 0xFF]
    b3 = inv_table[(val >> 24) & 0xFF]
    return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)

def decrypt_block(v0: int, v1: int, key: List[int]) -> tuple[int, int]:
    delta = 0x61C88647
    sum_val = (-delta * 32) & 0xFFFFFFFF  # Handle overflow
    
    v0 = inv_sbox(v0)
    v1 = inv_sbox(v1)
    
    for i in range(32):
        # Handle overflow with & 0xFFFFFFFF
        v1 = (v1 - (((v0 << 4) + key[2]) ^ (v0 + sum_val) ^ ((v0 >> 5) + key[3]))) & 0xFFFFFFFF
        v0 = (v0 - (((v1 << 4) + key[0]) ^ (v1 + sum_val) ^ ((v1 >> 5) + key[1]))) & 0xFFFFFFFF
        sum_val = (sum_val + delta) & 0xFFFFFFFF
        
    return v0, v1

def main():
    password = bytearray(32)
    
    # Process each 8-byte block
    for i in range(0, 32, 8):
        block = TARGET[i:i+8]
        v0, v1 = struct.unpack("<II", block)  # Little-endian
        dec_v0, dec_v1 = decrypt_block(v0, v1, KEY)
        password[i:i+4] = struct.pack("<I", dec_v0)
        password[i+4:i+8] = struct.pack("<I", dec_v1)
    
    # Remove any null bytes and decode
    password = password.rstrip(b'\0').decode()
    print(f"Password: {password}")

if __name__ == "__main__":
    main()

Password: ACS{66c010a978dd32021526cf70014}

[Final] rev/loading

Instead of adding the value once, the program enters a loop and increments it one by one, which is time-consuming.

image

I patched these loops to perform the addition in a single step. The program now runs much faster and prints the flag.

image

Script:

def patch_binary(filepath):
    pattern_start = bytes.fromhex('c7 45 fc 00 00 00 00 eb 13 48 8b 45 e8 8b 00 8d 50 01 48 8b 45 e8 89 10 83 45 fc 01 81 7d fc')
    pattern_end = bytes.fromhex('76 e4')
    
    try:
        with open(filepath, 'rb') as f:
            content = bytearray(f.read())

        pos = 0
        patches_made = 0
        
        while True:
            pos = content.find(pattern_start, pos)
            if pos == -1:
                break
                
            if content.find(pattern_end, pos + len(pattern_start) + 4) == pos + len(pattern_start) + 4:
                # Get the comparison value
                comp_val = int.from_bytes(content[pos + len(pattern_start):pos + len(pattern_start) + 4], 'little')
                print(f"Found pattern at offset {hex(pos)}")
                print(f"Comparison value: {hex(comp_val)}")
                
                # Add 1 to the comparison value since loop is inclusive
                comp_val += 1
                comp_bytes = comp_val.to_bytes(4, 'little')
                
                # Create optimized code
                patch = bytes.fromhex('48 8b 45 e8' +          # mov rax, [rbp+var_18]
                                    '8b 10' +                  # mov edx, [rax]
                                    '81 c2') + comp_bytes +    # add edx, comp_val + 1
                                    bytes.fromhex('89 10')     # mov [rax], edx
                
                # Pad with NOPs
                original_size = len(pattern_start) + 4 + len(pattern_end)
                nop_count = original_size - len(patch)
                patch += bytes([0x90]) * nop_count
                
                # Apply patch
                content[pos:pos + len(patch)] = patch
                patches_made += 1
                
            pos += 1

        if patches_made > 0:
            patched_filepath = filepath + '.patched'
            with open(patched_filepath, 'wb') as f:
                f.write(content)
            print(f"\nPatched {patches_made} instances")
            print(f"Saved patched binary as: {patched_filepath}")
        else:
            print("\nNo matching patterns found to patch")

    except FileNotFoundError:
        print(f"Error: File '{filepath}' not found")
    except Exception as e:
        print(f"Error while processing file: {str(e)}")

if __name__ == "__main__":
    filepath = "loading"
    patch_binary(filepath)

Run the patched binary to get the flag.

Flag: ACS{L0aDingGgGgGgGgggGG}


Full team write-ups:

Conclusion

ACS 2024 was a great experience. Thanks to the KMA.Qrange team for the effort. See you at the next one!