MTA CTF 2026

This writeup follows my home approach.

REzero [rev]

A suspicious Windows x64 executable was found from a compromised host. Local analysis alone may not reveal the full picture. Can you analyze the executable, follow its trail beyond the victim machine, and determine what additional evidence can be recovered?

The challenge artifact is REZero.exe, a PE. Let’s start with static analysis.

First stage

Initial static

This program expects an input file. The filename is passed to sub_140015010 to get a bit array.

After digging around, I confirmed that it must be a png.

Back in main, the array is handled by sub_140001000.

lol.

There is an odd block in the middle of the graph. It looks like a loader, and it uses the bit array from main to decrypt the payload.

The challenge gives us only the PE and no additional files, so we need to derive the bit array from this state-machine-like function.

Emilia where r u

Let’s start by locating the parts:

From this, we can conclude that var_3B80 is the state variable.

The init block prepares the bit string and seeds the machine:

The dispatcher fetches a new state value and passes it to the check chain:

Each dispatcher check block has this format:

trigger = cmp_const ^ unk_140001E350[idx]

if state_var != trigger:
	goto next_check_block;
else:
	run_this_case_body();

Each matched dispatcher block falls through into a generated case body, and they all have this general behavior:

Each case consumes one nibble from the bit array and transforms the state for the next dispatcher pass.

With that information, we can try emulating to find the nibbles.

The blind worst case is O(16^k), which would be impossible for this challenge: k = 110.

However, each state has concrete values for x := var_3B88 and y := var_3B78; most guessed nibbles lead to an x that never hits useful dispatcher triggers; the loop has a hard cap of 0x33c = 828 ticks.

With a bit more pruning, we get this core searcher:

def solve():
	cases = extract_dispatcher_cases()
	cases_by_trigger = {case.trigger: case for case in cases}
	
	queue = [{
		"x": 0xee47dd44,
		"y": 0x5c3e3dc7,
		"ticks": 0,
		"nibbles": {},   # nibble_index -> nibble_value
	}]
	
	seen = set()
	
	while queue:
		state = queue.pop(0)
	
		x = state["x"]
		y = state["y"]
		ticks = state["ticks"]
		nibbles = state["nibbles"]
	
		while ticks <= 0x33c:
			ticks += 1
		
			y = (y * x * x * x - 0x90aa - x) & 0xffffffffffffffff
		
			case = cases_by_trigger.get(y)
			
			if case is None:
				continue
			
			if case.is_emilia_loader:
				return nibbles
			
			idx = case.nibble_index
			
			if idx in nibbles:
				candidates = [nibbles[idx]]
			else:
				candidates = range(16)
			
			for nibble in candidates:
				new_nibbles = nibbles.copy()
				new_nibbles[idx] = nibble
				
				new_x = emulate_case_body(case, x, nibble)
				
				next_state = (
					new_x,
					0x5c3e3dc7,
					ticks,
					tuple(sorted(new_nibbles.items())),
				)
				
				if next_state in seen:
					continue
				
				seen.add(next_state)
				
				queue.append({
					"x": new_x,
					"y": 0x5c3e3dc7,
					"ticks": ticks,
					"nibbles": new_nibbles,
				})
		break

My full script was AI-generated (skill issue), so I won’t include it here. The recovered nibble string is:

febbfc11906eb4bb7595dba92ec13d07faafe00300f2fcece1e671fd5ee2619be297804ad7f9fb90401eba4bc5d79faeaa99050a1fea46

There is also one last bit that is not involved in the state machine.

From here, we can turn the nibble string into a bit string and recreate the image required for the next stage:

Second stage

Xin chao nguoi dep

This confirms the expected input among the reconstructed images. Now we can dump the decrypted stage.

I set a breakpoint where the decryption returns.

I dumped it at that point. The result is a valid PE, and running it standalone confirms that the Windows loader can parse and map it successfully.

Dynamic & asm

I loaded it into IDA, but the obfuscation seems too strong for IDA to handle, so I used another approach.

The imports have these interesting entries. My guess was that it did something with the username and sent it somewhere online.

The PE also creates a file named flag.txt.emilia, so I set these breakpoints:

GetUserNameA hits first and writes my VM username to the stack. Then the code concatenates it with something, UNK.

The next breakpoint to hit was

CreateFileA("flag.txt", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)

And then

CreateFileA("flag.txt.emilia", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL)

The pattern here is that it encrypts flag.txt into flag.txt.emilia with something derived from the concatenated string.

I created a test flag.txt and ran the binary again.

After two hits at CreateFileA, the next breakpoints to hit were ReadFile and WriteFile:

Here, note that rdx, the second argument, is the destination/source buffer pointer for ReadFile/WriteFile. The encryption function should lie between them because rdx is loaded from the same source, [rsp+78].

By stepping through, I excluded the other candidates and confirmed that this is the encryption function:

At this point, the function looks like encrypt(buf, len, key).

I checked [r8] in the dump. It is a 32-byte blob.

The previous analysis suggests that this could be sha256("UNK_" + username) or something similar.

Jackpot.

With the encryption key located, I set a hardware breakpoint on that clb_dep_trai(test flag buffer), which got me to this part:

Given that this is inside bcryptprimitives.dll, it is likely RC4 here. A later external test confirmed it.

The hardware breakpoint only hits inside that loop, so we can conclude that it uses RC4 to encrypt flag.txt into flag.txt.emilia.

The prompt said to “follow its trail beyond the victim machine, and determine what additional evidence can be recovered”, so let’s keep digging to get the real flag.txt.emilia.

Going online

After writing the encrypted test flag to flag.txt.emilia, the PE creates a .cab with the name fields and timestamp in the filename, which contains that same .emilia file.

It then calls InternetOpenA with the agent string “Emilia”.

It connects to discord.com.

It then sends the .cab file to the Discord channel.

At this point, the missing piece is the authorization token. A breakpoint at HttpAddRequestHeadersA should get it.

Now we have all the pieces, so let’s see what is in that channel.

import json, urllib.request

h = {"Authorization": "Bot " + "*REDACTED*", "User-Agent": "Emilia"}
url = f"https://discord.com/api/v10/channels/*REDACTED*/messages?limit=5"

msgs = json.load(urllib.request.urlopen(urllib.request.Request(url, headers=h)))

print(msgs)

The channel contains the author’s .cab:

At this point, the rest is just decryption.

Flag

Based on what we have found, the SHA-256 of the name fields from the .cab filename is used as the RC4 key to encrypt the flag. This script should give us the flag:

from hashlib import sha256
from pathlib import Path
from Crypto.Cipher import ARC4

key = sha256(b"NEX0_Nexo").digest()
ct = Path("flag.txt.emilia").read_bytes()

pt = ARC4.new(key).decrypt(ct)
print(pt.decode())

Nice challenge.

MTA60{Im_n07_90Nn4_b3_7h3_0nLy_0n3_t0_r3@p_Wh47_I_d0n7_50w}