Hard

CCT2019 โ€” re3

QA210 ยท 2019 ยท Reverse Engineering ยท TryHackMe
Challenge
re3
Category
Reverse Engineering
Difficulty
Hard
Platform
TryHackMe
Binary Type
.NET GUI
Key Format
32-char hex
Important Note

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:

bash
$ 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.

Why .NET Matters

.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:

csharp
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:

csharp
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()));
    }
}
Constraint Analysis

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:

csharp
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:

This is essentially factoring 711,000,000 into 4 factors that sum to 711:

python
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  โœ“
Correct Slider Values
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

python
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
Flag
46d75cdacb5ada050761b5f0723e1cf2
Remember

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:

python
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

CTF Lesson

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!