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.

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.

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.

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.

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.

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

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.

Then, I edited the import table.

Finally, I updated the CompanyName using Resource Hacker.

Submitting the file retrieved the flag.

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.

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.

Pressing F5 reveals the clear logic.

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.

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.

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

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!