CCT2019 โ re3
The key for this challenge is a 32-character hex blob, NOT in CCT{.*} format. Don't waste time looking for flag wrappers.
Challenge Overview
We're given a Windows PE executable with a GUI containing 4 sliders and a "Check!" button. The goal is to find the correct combination of slider values to make the program reveal a 32-character hex key.
This is a .NET GUI reverse engineering challenge where we must find the correct slider values to unlock a hidden key through decompilation, constraint solving, and XOR decryption.
Initial Reconnaissance
First, let's identify the file type:
$ file re3.exe
# PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections
It's a .NET assembly โ perfect for decompilation. Running strings reveals interesting details including class names like byteA, byteB, checkButton, bar1-bar4, and methods like goodBoy, badBoy.
.NET assemblies compile to IL (Intermediate Language) rather than native machine code. This makes them trivially decompilable โ even obfuscated assemblies can often be fully recovered with tools like dnSpy, JetBrains dotPeek, or Ghidra's .NET module.
Decompilation with Ghidra/JetBrains
Decompiling the .NET assembly reveals the complete source code. The key components are:
The Byte Arrays
Two critical byte arrays are found in the class:
private byte[] byteA = new byte[32]
{
20, 22, 100, 23, 21, 99, 100, 97, 99, 98, 21, 97, 100, 97, 16, 21,
16, 23, 22, 17, 98, 21, 102, 16, 23, 18, 19, 101, 17, 99, 102, 18
};
private byte[] byteB = new byte[32]
{
125, 92, 93, 86, 19, 64, 91, 82, 95, 95, 19, 67, 82, 64, 64, 18,
19, 114, 84, 82, 90, 93, 12, 19, 116, 90, 69, 86, 19, 70, 67, 12
};
The Check Logic
The event handler for the "Check!" button implements three constraints:
private void eventHandlerA(object A_0, EventArgs A_1)
{
int v1 = this.bar1.Value;
int v2 = this.bar2.Value;
int v3 = this.bar3.Value;
int v4 = this.bar4.Value;
int sum = 711;
int product = 711000000;
if (v1 + v2 + v3 + v4 == sum &&
v1 * v2 * v3 * v4 == product &&
v1 > v2 && v2 > v3 && v3 > v4)
{
MessageBox.Show(this.goodBoy(v1, v2, v3, v4, (byte[]) this.byteA.Clone()));
}
else
{
MessageBox.Show(this.badBoy(v1, v2, v3, v4, (byte[]) this.byteA.Clone()));
}
}
Three constraints must be satisfied simultaneously: sum = 711, product = 711,000,000, and strictly descending order (v1 > v2 > v3 > v4). This severely limits the search space.
The Decryption Methods
Two methods handle the success/failure paths:
private string goodBoy(int v1, int v2, int v3, int v4, byte[] data)
{
for (int i = 0; i < data.Length; i++)
data[i] = (byte)(data[i] ^ (byte)(this.c ^ v2));
return Encoding.ASCII.GetString(data);
}
private string badBoy(int v1, int v2, int v3, int v4, byte[] data)
{
for (int i = 0; i < data.Length; i++)
data[i] = (byte)(this.byteB[i] ^ this.d);
return Encoding.ASCII.GetString(data);
}
Where c = 177 and d = 51. The goodBoy method uses c ^ v2 as the XOR key on byteA, while badBoy uses the constant d on byteB.
Solving the Slider Values
We need to find 4 values where:
- Sum = 711
- Product = 711,000,000
- Ordering: v1 > v2 > v3 > v4
This is essentially factoring 711,000,000 into 4 factors that sum to 711:
target_sum = 711
target_product = 711000000
# 711000000 = 711 * 1000000
# Factoring: 711 = 3 * 3 * 79, 1000000 = 10^6
# After analysis: 325, 150, 120, 116
# 325 + 150 + 120 + 116 = 711 โ
# 325 * 150 * 120 * 116 = 711000000 โ
# 325 > 150 > 120 > 116 โ
The factorization can be derived by observing that 711,000,000 = 711 ร 10โถ. Breaking down the prime factorization 2โถ ร 3ยฒ ร 5โถ ร 79, and distributing the prime factors into four groups that satisfy both the sum and ordering constraints, uniquely yields 325, 150, 120, 116.
Decrypting the Key
With v2 = 150, the goodBoy XOR key is: 177 ^ 150 = 39
byteA = [20, 22, 100, 23, 21, 99, 100, 97, 99, 98, 21, 97, 100, 97, 16, 21,
16, 23, 22, 17, 98, 21, 102, 16, 23, 18, 19, 101, 17, 99, 102, 18]
c = 177
v2 = 150
key = c ^ v2 # = 39
result = ""
for b in byteA:
result += chr(b ^ key)
print(result)
# 46d75cdacb5ada050761b5f0723e1cf2
The key is a raw 32-character hex blob โ NOT wrapped in CCT{...}. This threw off many participants who expected the standard flag format.
Verification
We can verify the bad path too with d = 51:
byteB = [125, 92, 93, 86, 19, 64, 91, 82, 95, 95, 19, 67, 82, 64, 64, 18,
19, 114, 84, 82, 90, 93, 12, 19, 116, 90, 69, 86, 19, 70, 67, 12]
d = 51
result = ""
for b in byteB:
result += chr(b ^ d)
print(result)
# None shall pass! Again? Give up?
The bad path simply outputs a discouraging message โ confirming that our decryption approach is correct and the good path is the only way to the flag.
Key Takeaways
- .NET decompilation โ .NET assemblies are trivially decompilable with Ghidra, dnSpy, or JetBrains dotPeek. Even obfuscated assemblies can be fully decompiled.
- Constraint solving โ Factoring problems where sum and product constraints narrow down solutions. The combination of
sum = 711andproduct = 711,000,000with ordering constraints leaves only one valid solution. - XOR decryption โ Simple single-byte XOR with a derived key (
c ^ v2). Once the slider values are known, the XOR key is deterministic. - Dotfuscator โ Even obfuscated .NET assemblies can be fully decompiled. Obfuscation of variable names doesn't hide the logic.
- Integer factorization โ Breaking down products into factors satisfying ordering constraints. Prime factorization of
711,000,000 = 2โถ ร 3ยฒ ร 5โถ ร 79is the key insight.
Always check the key format carefully. This challenge uses a raw hex key instead of the typical CCT{...} wrapper. Many teams wasted time looking for a flag format that didn't exist. Read the challenge description thoroughly!