avatar
Cyscom
Cybersecurity Student Community of VIT Chennai
  • CTF EVENTS
  • CATEGORIES
  • TAGS
  • ARCHIVES
  • POSTS
  • ABOUT
Home FinalTrace 2025 Echo-Double Combat
Writeup
Cancel

Echo-Double Combat

Echo-Double Combat

  • Category: Reverse Engineering
  • Author: Suraj Kumar

Challenge Description

A binary executable accepts a 9-character input key and validates it through XOR operations. The correct key is encoded in the binary and must be extracted by XORing stored bytes with 0x55. Once the correct key is entered, the binary decrypts and reveals the flag using repeating-key XOR. A decoy flag is stored in encrypted form as a Base64 string (WTNsemUyY3dYMkkwWTJ0ZmREQmZjM1EwY25SemZRPT0=) in the binary’s strings, which decodes to cys{g0_b4ck_t0_st4rts} — this is intentionally wrong and serves as a red herring.

Solution

Initial Analysis

Running basic reconnaissance on the binary revealed an encrypted decoy flag in the strings output. Further analysis required disassembling the binary to understand the validation logic and extract the encoded key array used for XOR operations.

Tools Used

  • Ghidra / IDA Pro / Binary Ninja
  • strings, file, objdump, readelf
  • Python 3
  • base64 (command-line tool)
  • GCC

Step-by-Step Solution

Step 1: Initial Analysis

1
2
file challenge.dat
strings challenge.dat

Found a Base64-encoded string WTNsemUyY3dYMkkwWTJ0ZmREQmZjM1EwY25SemZRPT0=. Decoding it twice reveals cys{g0_b4ck_t0_st4rts} which is a decoy flag (intentionally wrong). The binary requires a 9-character key as input.

Step 2: Decode the Decoy (Red Herring)

1
2
echo "WTNsemUyY3dYMkkwWTJ0ZmREQmZjM1EwY25SemZRPT0=" | base64 -d | base64 -d
# Output: cys{g0_b4ck_t0_st4rts}

This decoy flag is not the answer and will fail if used as input. It exists to mislead players who rely only on strings output.

Step 3: Extract Encoded Arrays from Binary

1
2
3
objdump -s -j .data challenge.dat
# or
readelf -x .data challenge.dat

Located two arrays in the .data section:

1
2
key_enc:  34 39 3a 3d 3a 38 3a 27 34
flag_enc: 12 5f 0c 1a 5c 19 30 43 12 3e 19 01 59 5f 0e 04 17 05

Step 4: Analyze Validation Logic in Disassembler

Opened the binary in Ghidra and decompiled the main function. Identified the key derivation and validation:

1
2
3
4
5
6
7
8
9
10
unsigned char key[9];
for (int i = 0; i < 9; i++) {
    key[i] = key_enc[i] ^ 0x55;
}

if (memcmp(input, key, 9) == 0) {
    for (int i = 0; i < 18; i++) {
        flag[i] = flag_enc[i] ^ key[i % 9];
    }
}

Step 5: Compute the Real Key

1
2
3
key_enc = bytes.fromhex('34393a3d3a383a2734')
key = bytes(b ^ 0x55 for b in key_enc)
print(key.decode())  # Output: alohomora

The real key is: alohomora

Step 6: Decrypt the Flag

1
2
3
4
flag_enc = bytes.fromhex('125f0c1a5c193043123e1901595f0e041705')
key = b'alohomora'
flag = bytes(flag_enc[i] ^ key[i % 9] for i in range(len(flag_enc)))
print(flag.decode())  # Output: s3cr3t_1s_un10cked

Step 7: Verify and Retrieve Flag

1
2
3
./challenge.dat
Speak the binding spell: alohomora
Flag: cys{s3cr3t_1s_un10cked}

Successfully retrieved the real flag by entering the correct key.

Python solver script

Flag

1
cys{s3cr3t_1s_un10cked}

solver python code

the code could change for new binary file as hex dump locations might change

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def extract_data_from_binary(filename='l'):
    """Extract data from the binary file"""
    try:
        with open(filename, 'rb') as f:
            data = f.read()
        
        print(f"[+] File size: {len(data)} bytes (0x{len(data):x})")
        
        # The addresses are 0x404060 and 0x404070
        # We need to find the file offset for these virtual addresses
        
        # Common data section offsets to try
        possible_offsets = [
            0x3060,  # 0x404060 - 0x401000 (common base)
            0x2060,  # Alternative
            0x4060,  # Alternative (if base is 0x400000)
            0x3040,  # Another common offset
        ]
        
        for base_offset in possible_offsets:
            offset_60 = base_offset
            offset_70 = base_offset + 0x10
            
            if offset_60 + 9 <= len(data) and offset_70 + 18 <= len(data):
                data_404060 = data[offset_60:offset_60+9]
                data_404070 = data[offset_70:offset_70+18]
                
                print(f"\n[*] Trying base offset: 0x{base_offset:04x}")
                print(f"    data_404060 (offset 0x{offset_60:04x}): {data_404060.hex()}")
                print(f"    data_404070 (offset 0x{offset_70:04x}): {data_404070.hex()}")
                
                # Try to decode and check if it makes sense
                result = solve_with_data(data_404060, data_404070)
                if result and result['valid']:
                    return result
        
        # If nothing worked, try scanning the file
        print("\n[*] Trying to scan entire file for valid patterns...")
        return scan_for_data(data)
        
    except FileNotFoundError:
        print(f"Error: File '{filename}' not found")
        return None
    except Exception as e:
        print(f"Error: {e}")
        import traceback
        traceback.print_exc()
        return None

def scan_for_data(data):
    """Scan the file for potential encoded data"""
    print("[*] Scanning for 9-byte sequences that decode to printable ASCII...")
    
    candidates = []
    for i in range(len(data) - 9):
        # Try XORing with 0x55
        spell = bytearray()
        valid = True
        for j in range(9):
            char = data[i+j] ^ 0x55
            if char < 0x20 or char > 0x7e:  # Not printable ASCII
                valid = False
                break
            spell.append(char)
        
        if valid:
            spell_str = spell.decode('ascii')
            print(f"    Found at offset 0x{i:04x}: {spell_str}")
            candidates.append((i, data[i:i+9]))
    
    if candidates:
        print(f"\n[*] Found {len(candidates)} candidate(s)")
        # Use the first candidate
        offset, data_404060 = candidates[0]
        # Look for data_404070 nearby (16 bytes after)
        data_404070 = data[offset+16:offset+16+18] if offset+16+18 <= len(data) else None
        
        if data_404070:
            print(f"[*] Using data at offset 0x{offset:04x}")
            return solve_with_data(data_404060, data_404070)
    
    return None

def solve_with_data(data_404060, data_404070):
    """Solve the challenge with extracted data"""
    
    # Step 1: Calculate the spell (key)
    # var_1d[i] = data_404060[i] ^ 0x55
    spell = bytearray()
    for i in range(9):
        spell.append(data_404060[i] ^ 0x55)
    
    print(f"\n[+] Calculated Spell:")
    print(f"    Hex: {spell.hex()}")
    
    # Try to decode as ASCII
    try:
        spell_str = spell.decode('ascii')
        print(f"    ASCII: {spell_str}")
        is_printable = all(32 <= b <= 126 for b in spell)
    except:
        spell_str = spell.decode('ascii', errors='replace')
        print(f"    ASCII (with errors): {spell_str}")
        is_printable = False
    
    # Step 2: Decode the flag (Mirror Key)
    # var_88[i] = var_1d[i % 9] ^ data_404070[i]
    flag = bytearray()
    for i in range(18):
        flag.append(spell[i % 9] ^ data_404070[i])
    
    print(f"\n[+] Decoded Mirror Key:")
    print(f"    Hex: {flag.hex()}")
    
    # Try to decode as ASCII
    try:
        flag_str = flag.decode('ascii')
        print(f"    ASCII: {flag_str}")
        flag_printable = all(32 <= b <= 126 for b in flag)
    except:
        flag_str = flag.decode('ascii', errors='replace')
        print(f"    ASCII (with errors): {flag_str}")
        flag_printable = False
    
    print(f"\n{'='*60}")
    print(f"SOLUTION:")
    print(f"{'='*60}")
    print(f"Ancient Spell to Enter: {spell_str}")
    print(f"Mirror Key (Flag): {flag_str}")
    print(f"{'='*60}\n")
    
    return {
        'spell': spell_str,
        'flag': flag_str,
        'valid': is_printable and flag_printable
    }

def manual_solve():
    """Manual solver - paste your hex data here"""
    print("="*60)
    print("MANUAL MODE")
    print("="*60)
    print("Enter the hex data from the binary\n")
    print("To get the data manually, use:")
    print("  readelf -x .data l")
    print("  or")
    print("  objdump -s -j .data l")
    print()
    
    data_404060_hex = input("Enter data_404060 (9 bytes in hex, e.g., 1a2b3c...): ").strip()
    data_404070_hex = input("Enter data_404070 (18 bytes in hex, e.g., 4d5e6f...): ").strip()
    
    try:
        # Remove spaces and common separators
        data_404060_hex = data_404060_hex.replace(" ", "").replace("0x", "")
        data_404070_hex = data_404070_hex.replace(" ", "").replace("0x", "")
        
        data_404060 = bytes.fromhex(data_404060_hex)
        data_404070 = bytes.fromhex(data_404070_hex)
        
        if len(data_404060) != 9:
            print(f"Error: data_404060 should be 9 bytes, got {len(data_404060)}")
            return
        
        if len(data_404070) != 18:
            print(f"Error: data_404070 should be 18 bytes, got {len(data_404070)}")
            return
        
        print()
        solve_with_data(data_404060, data_404070)
        
    except ValueError as e:
        print(f"Error parsing hex: {e}")

def main():
    import sys
    
    print("="*60)
    print("Echo-Double Challenge Solver")
    print("="*60)
    print()
    
    # Get filename
    if len(sys.argv) > 1:
        filename = sys.argv[1]
    else:
        filename = 'l'
    
    # Try automatic extraction first
    print(f"[*] Attempting automatic data extraction from '{filename}'...\n")
    result = extract_data_from_binary(filename)
    
    if not result:
        print("\n" + "="*60)
        print("Automatic extraction failed. Switching to manual mode...")
        print("="*60)
        print()
        manual_solve()

if __name__ == "__main__":
    main()

Flag

cys{s3cr3t_1s_un10cked}
Edit on GitHub
Trending Tags
authentication idor sql-injection ssti xss

© 2025 Cyscom. Some rights reserved.

Using the Jekyll theme Chirpy.

A new version of content is available.