Espionage CTF 2024 rev challenge writeups Link to this heading

Here are my writeups for all the reverse engineering challenges in Espionage CTF 2024. I managed to get first blood on all of the RE challenges except for ScrambledSquares.

UoftCTF Members:

  • __fastcall (me): rev
  • drec: pwn
  • Tyler_: forensics and osint
  • SteakEnthusiast: web

NOTE: All code in this writeup has been beautified manually for your reading pleasure. It may not represent the exact disassembly, but it does represent the semantics of the code.

Coin Hunt (15 points) Link to this heading

This challenge gives us a single file CoinHunt.

Let’s take a look at the file with Detect It Easy, a useful program for determine the type of files and signatures of common compilers, packers, etc.

PE file coinhunt shown in detect it easy

DIE recognizes this file as a UPX-packed PE (portable executable) file. All other signatures can be ignored since they are of the packer and not the original executable. I thought of 3 possible scenarios after seeing this (from increasing order of likely-hood):

  1. The file is packed with a modified version of UPX.
  2. The file is packed with a packer that pretends to be UPX, or DIE’s signatures are wrong.
  3. The file is packed with a unmodified version of UPX.

As a sanity check, I decided to verify that this wasn’t a unmodified version of UPX, even though DIE says otherwise.

$ upx -d CoinHunt

txt
1                       Ultimate Packer for eXecutables
2                          Copyright (C) 1996 - 2024
3UPX 4.2.2       Markus Oberhumer, Laszlo Molnar & John Reiser    Jan 3rd 2024
4
5        File size         Ratio      Format      Name
6   --------------------   ------   -----------   -----------
7     15360 <-      9728   63.33%    win64/pe     CoinHunt
8
9Unpacked 1 file.

???????

It turns out that it’s just unmodified UPX and we didn’t have to do any manual unpacking :D

Since this is a MSVC C/C++ binary, I opened it up in the industry standard disassembler, IDA Pro

Load the binary as a x86-64 PE with the default options…

Load a new file dialog in IDA PRO

IDA finds and drops me off at the main function

Disassembly of main function in IDA PRO

Let’s hit F5 to decompile the binary…

c
 1int __fastcall main(int argc, const char **argv, const char **envp)
 2{
 3  HANDLE coin1_handle;
 4  HANDLE coin2_handle;
 5  HANDLE coin3_handle;
 6  HANDLE coin4_handle;
 7  const char *ascii_flag;
 8  char wrong_file_msg[56];
 9  DWORD NumberOfBytesWritten = 0;
10
11  strcpy(wrong_file_msg, "Wrong Coin \n,---. \n' __O>` \n( (__/  ) \n.-----, \n `---'\n");
12  
13  CreateDirectoryW(L"C:\\Users\\Public\\Documents\\Coin1", NULL);
14  coin1_handle = CreateFileW(L"C:\\Users\\Public\\Documents\\Coin1\\Coin1.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
15  WriteFile(coin1_handle, wrong_file_msg, strlen(wrong_file_msg), &NumberOfBytesWritten, NULL);
16  CloseHandle(coin1_handle);
17
18  CreateDirectoryW(L"C:\\Users\\Public\\Downloads\\Coin2", NULL);
19  coin2_handle = CreateFileW(L"C:\\Users\\Public\\Downloads\\Coin2\\Coin2.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
20  WriteFile(coin2_handle, wrong_file_msg, strlen(wrong_file_msg), &NumberOfBytesWritten, NULL);
21  CloseHandle(coin2_handle);
22  
23  CreateDirectoryW(L"C:\\Users\\Public\\Music\\Coin3", NULL);
24  coin3_handle = CreateFileW(L"C:\\Users\\Public\\Music\\Coin3\\Coin3.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
25  WriteFile(coin3_handle, wrong_file_msg, strlen(wrong_file_msg), &NumberOfBytesWritten, NULL);
26  CloseHandle(coin3_handle);
27  
28  ascii_flag = "      __-----__ \n"
29                "..;;;--'~~~`--;;;.. \n"
30                "/;-~EspionageCTF{$ilver@_C0In}~-. \n"
31                "//      ,;;;;;;;;      \\ \n"
32                ".//      ;;;;;           \\ \n"
33                "||       ;;;;(   /.|       || \n"
34                "||       ;;;;;;;   _      || \n"
35                "||       ';;  ;;;;=        || \n"
36                "||LIBERTY | '';;;;;;      || \n"
37                "\\     ,| '  '|><| 1995 // \n"
38                " \\   |     |        A // \n"
39                "  `;.,|.    |      '.-'/ \n"
40                "     ~~;;;,._|___.,-;;;~' \n"
41                "         ''=--' \n";
42    
43  CreateDirectoryW(L"C:\\Users\\Public\\Pictures\\Coin4", NULL);
44  coin4_handle = CreateFileW(L"C:\\Users\\Public\\Pictures\\Coin4\\Coin4.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
45  WriteFile(coin4_handle, ascii_flag, strlen(ascii_flag), &NumberOfBytesWritten, NULL);
46  CloseHandle(coin4_handle);
47  
48  return 0;
49}

We can get a very clear picture of what the binary does, writing to various directories in the Public user folder. The flag is visible in the ASCII art. EspionageCTF{$ilver@_C0In}

Skull (15 points) Link to this heading

We are given a encrypted 7-zip file, Skull.7z, and the password infected.

After extracting the zip file, we are once again greeted with a MSVC C/C++ binary. There are no DIE signatures for any packers, and we can confirm it’s not packed by looking at the entropy graph. Both compression and encryption will reduce the uniqueness of the bytes in the PE file, making it less random. A consistently high entropy is a good sign a file is packed.

Entropy graph of skull binary in detect it easy

Loading the PE in IDA Pro, and examining the main function.. we find the flag, in plaintext? What?!

c
1qmemcpy(flag_buffer, "{F@LC0N$_B!rd}", sizeof(flag_buffer));

Looking at the disassembly, we see a stack string, that contains our flag.

At the time of writing, both IDA Pro (8.3) and Binary Ninja (3.5.4526) correctly recognize this pattern and outline the instructions into a memcpy(), with only Ghidra (11.0) failing. To future challenge authors, it seems like just using simple stack strings won’t be tricking even the most novice reverse engineers anymore!

txt
1mov     [rbp+320h+var_20], 4C40467Bh
2mov     [rbp+320h+var_1C], 244E3043h
3mov     [rbp+320h+var_18], 7221425Fh
4mov     [rbp+320h+var_14], 7D64h
txt
1mov     [rbp+320h+var_20], 'L@F{'
2mov     [rbp+320h+var_1C], '$N0C'
3mov     [rbp+320h+var_18], 'r!B_'
4mov     [rbp+320h+var_14], '}d'

EspionageCTF{F@LC0N$_B!rd}

Top Secret (15 points) Link to this heading

This challenge provide us with another encrypted 7zip archive, with the same password of infected. The extracted binary is another MSVC C/C++ PE file, and I loaded it into IDA Pro just as with the last challenge.

The flag in this challenge is in plain text in the .rodata section, and you can even strings for it. In the disassembly, the strcpy() call is outlined, revealing the flag.

c
1strcpy(lpBuffer, "EspionageCTF{CL@55iFed_C0nt$nt}");

EspionageCTF{CL@55iFed_C0nt$nt}

ScrambledSquares (87 points) Link to this heading

Now this sponsor challenge from Aliakbar Zahravi at TrendMicro is a lot more interesting, uses techniques similar to real malware like dynamic api resolving, and was overall very fun to solve. Thanks for creating it!

We are given an archive containing a lot of .DAT files, what looks to be presumably a decrypter written in Python, ctf.py, and the following instructions:

txt
 1CTF Challenge: Brief Hints
 2
 3    •    Stage 1: Investigate the Python code’s method of serial number generation. Focus on MD5 and the relevance of dates.
 4    •    Stage 2: Look for hidden elements within the code for the password.
 5    •    Stage 3: Use the password wisely to reveal a new file format.
 6    •    Stage 4: The executable holds clues about its own usage.
 7    •    Stage 5: Apply what you learned from the exe analysis for decryption.
 8    •    Stage 6: A QR code awaits, holding more than meets the eye.
 9    •    Stage 7: Decode the QR contents to transition to a new file type.
10    •    Stage 8: Unpack the final layer to uncover the flag.
11
12Remember: Each clue is a piece of a larger puzzle. Pay attention to the details and think creatively.
python
 1import sys 
 2from PyQt5 .QtWidgets import QApplication ,QWidget ,QLineEdit ,QPushButton ,QVBoxLayout ,QLabel 
 3import hashlib 
 4import subprocess 
 5from Crypto .Cipher import AES 
 6from Crypto .Protocol .KDF import PBKDF2 
 7import os 
 8from datetime import datetime 
 9import pefile 
10def tthfghfjfr4343 (OOOO00000OOO00O00 ):
11    OOOO00O00000OOOOO =hashlib .md5 ()
12    with open (OOOO00000OOO00O00 ,"rb")as OO0O0O00OOOOO0OOO :
13        for OOOOOO0OO0O0OOO00 in iter (lambda :OO0O0O00OOOOO0OOO .read (4096 ),b""):
14            OOOO00O00000OOOOO .update (OOOOOO0OO0O0OOO00 )
15    return OOOO00O00000OOOOO .hexdigest ()
16class CTFChallenge (QWidget ):
17    def __init__ (O0OOOOOOO00OO0OO0 ):
18        O0OOOOOOO00OO0OO0 .attemptCount =0 
19        O0OOOOOOO00OO0OO0 .final_password =(((0x2a |0x42 )>=(0x8d |0x5 ))and (chr (0x2d ^0x62 ))or (chr (0x53 &0x57 )))+(((0x84 |0x1 )<(0xc0 |0xc0 ))and (chr (0x34 |0x60 ))or (chr (0x7c &0x7b )))+(((0x2a |0x7a )>(0x17c &0x114 ))and (chr (0x7a |0x20 ))or (chr (0x72 |0x22 )))+(((0x105 |0x121 )<=(0x2 |0x110 ))and (chr (0x2a ^0x1 ))or (chr (0x23 |0x11 )))+(((0xc0 |0xe2 )>(0x122 ^0x3d ))and (chr (0x3f &0x32 ))or (chr (0x10 |0x24 )))+(((0x83 |0x8 )<=(0xb4 |0xec ))and (chr (0x48 |0x2d ))or (chr (0x28 |0x60 )))+(((0x84 |0x1 )>(0x94 |0x84 ))and (chr (0x40 |0x3 ))or (chr (0x4b &0x43 )))+(((0x60 |0x10 )<=(0x17e &0x118 ))and (chr (0x11 |0x21 ))or (chr (0x31 &0x35 )))+(((0x48 |0x32 )==(0x31 |0x63 ))and (chr (0x26 |0x45 ))or (chr (0x77 &0x78 )))+(((0xd7 &0xfb )<=(0x3c ^0xdf ))and (chr (0x68 |0x48 ))or (chr (0x30 ^0x55 )))+(((0xf3 &0xbc )!=(0x14e &0x10f ))and (chr (0x33 |0x23 ))or (chr (0x31 ^0x2 )))+(((0x83 |0xb0 )>=(0x1d0 ^0xcb ))and (chr (0x3a ^0x56 ))or (chr (0x3b ^0x49 )))+(((0xa0 |0xa0 )!=(0x94 |0x90 ))and (chr (0x4e |0x5f ))or (chr (0x40 |0x5d )))+(((0x151 &0x190 )>=(0xd7 &0xdf ))and (chr (0x34 &0x3b ))or (chr (0x13 ^0x34 )))+(((0xff &0xff )==(0xbd &0xb5 ))and (chr (0x11 ^0x65 ))or (chr (0x76 |0x46 )))+(((0xee &0xdf )==(0x56 |0x8d ))and (chr (0x7 ^0x3c ))or (chr (0x33 |0x22 )))+(((0x15d ^0x76 )>(0xff &0xfe ))and (chr (0x30 |0x42 ))or (chr (0x78 &0x7d )))+(((0x2 ^0x90 )==(0x88 ^0x35 ))and (chr (0x15 ^0x7c ))or (chr (0x7c &0x6d )))+(((0x12 |0x112 )>=(0x92 |0xb ))and (chr (0x20 |0x10 ))or (chr (0x21 |0x30 )))+(((0xa1 |0xcc )<=(0x147 &0x107 ))and (chr (0x30 |0x4 ))or (chr (0x28 |0x11 )))+(((0x6e &0x7f )<=(0xc |0x7b ))and (chr (0x65 &0x74 ))or (chr (0x64 |0x44 )))
20        super ().__init__ ()
21        O0OOOOOOO00OO0OO0 .fgdgdfh4545 ()
22    def fgdgdfh4545 (OO00O0O00O000O0O0 ):
23        OO00O0O00O000O0O0 .setWindowTitle ('ISSessions 2024 CTF')
24        OO00O0O00O000O0O0 .resize (400 ,100 )
25        OO00O0O00O000O0O0 .serialNumberInput =QLineEdit (OO00O0O00O000O0O0 )
26        OO00O0O00O000O0O0 .validateButton =QPushButton ('Validate',OO00O0O00O000O0O0 )
27        OO00O0O00O000O0O0 .validateButton .clicked .connect (OO00O0O00O000O0O0 .kljfskjyi737iy2 )
28        OO00O0O00O000O0O0 .resultLabel =QLabel ('',OO00O0O00O000O0O0 )
29        OO00O0O00O000O0O0 .passwordInput =QLineEdit (OO00O0O00O000O0O0 )
30        OO00O0O00O000O0O0 .passwordInput .setEchoMode (QLineEdit .Password )
31        OO00O0O00O000O0O0 .passwordInput .hide ()
32        OO00O0O00O000O0O0 .serialLabel =QLabel ((((0xcc^0x35)==(0x54|0x68))and(chr(0x47^0xb))or(chr(0x55&0x4d)))+(((0xbf&0x9a)<=(0x41|0x64))and(chr(0x1c^0x69))or(chr(0x7f&0x6e)))+(((0x30^0x5a)<=(0x80|0x0))and(chr(0x4|0x70))or(chr(0x79&0x7a)))+(((0xaa^0x67)!=(0x16a^0x75))and(chr(0x65&0x75))or(chr(0x3f^0x5f)))+(((0x15d&0x19f)!=(0x49^0xb9))and(chr(0x76&0x72))or(chr(0x33|0x47)))+(((0x67^0xda)>(0xf5&0xf6))and(chr(0x32^0x14))or(chr(0x39&0x20)))+(((0x199^0x98)!=(0x95|0xd3))and(chr(0x5f&0x73))or(chr(0x7d&0x5d)))+(((0xc8^0x3d)==(0x108|0x110))and(chr(0x6e&0x7d))or(chr(0x67&0x75)))+(((0x110&0x1f0)<(0xc1^0x5))and(chr(0x68|0xe))or(chr(0x34^0x46)))+(((0x22|0x122)==(0xfc&0xfd))and(chr(0x7f^0x1c))or(chr(0x4^0x6d)))+(((0xc1|0xea)>=(0xbb&0xeb))and(chr(0x40|0x21))or(chr(0x5f&0x57)))+(((0xf6&0xfe)==(0x1a0^0xb2))and(chr(0x15^0x64))or(chr(0x28|0x64)))+(((0xf2|0x90)<(0x56^0xb9))and(chr(0x17&0x17))or(chr(0x20|0x20)))+(((0x94^0x4)!=(0xfd&0xee))and(chr(0x5e&0x4f))or(chr(0x52&0x72)))+(((0x87&0x9f)<(0xca&0x8b))and(chr(0x77&0x7d))or(chr(0x66^0x17)))+(((0xeb&0x9f)>=(0x115|0x10a))and(chr(0x10|0x72))or(chr(0x59^0x34)))+(((0x65|0x4c)==(0xa3^0x4e))and(chr(0x28|0x4a))or(chr(0x2f^0x4d)))+(((0xb5^0x3)>(0xbe&0xb3))and(chr(0x5d^0x38))or(chr(0x3e^0x58)))+(((0x121|0x10a)>=(0x4b^0xe1))and(chr(0x50^0x22))or(chr(0x2^0x74)))+(((0x4e^0xc7)>=(0x70&0x72))and(chr(0x3a&0x3f))or(chr(0x4e^0xf))),OO00O0O00O000O0O0 )
33        OO00O0O00O000O0O0 .passwordLabel =QLabel ((((0xe3^0x1e5)<=(0xeb&0xf2))and(chr(0x10^0x2b))or(chr(0x61^0x24)))+(((0x10^0xa7)!=(0x107&0x13f))and(chr(0xa|0x64))or(chr(0x7f&0x77)))+(((0x1c7^0xcc)!=(0x81|0x81))and(chr(0x64|0x70))or(chr(0x28|0x4b)))+(((0xa3&0xf7)>(0x138&0x155))and(chr(0x69^0x36))or(chr(0x11^0x74)))+(((0x88|0xa8)==(0x13^0xfd))and(chr(0x74|0x63))or(chr(0x72&0x7f)))+(((0x8d|0xed)==(0xe2|0x8a))and(chr(0xb^0x12))or(chr(0xe^0x2e)))+(((0x1b^0xeb)<=(0x90|0x47))and(chr(0x7a&0x50))or(chr(0x0|0x50)))+(((0x3d^0x13c)!=(0x3d^0x4c))and(chr(0x71&0x6f))or(chr(0x43^0x24)))+(((0x82^0x18f)>(0x13f&0x12a))and(chr(0x58^0x2d))or(chr(0x73|0x60)))+(((0x2a|0xb9)!=(0xc1&0xdf))and(chr(0x70|0x23))or(chr(0x44^0x31)))+(((0x31^0x98)>(0x11f&0x18d))and(chr(0x77&0x73))or(chr(0x77&0x77)))+(((0xc^0x7f)!=(0x6f&0x6e))and(chr(0x7f&0x6f))or(chr(0x76&0x74)))+(((0xb3&0xb2)>(0x10|0x80))and(chr(0x7b&0x72))or(chr(0x40|0x68)))+(((0xbf^0x5b)==(0xd0|0xc1))and(chr(0x62^0xc))or(chr(0x36^0x52)))+(((0x8^0xa3)<(0x12a&0x19e))and(chr(0x3b&0x3a))or(chr(0x24|0x30))),OO00O0O00O000O0O0 )
34        OO00O0O00O000O0O0 .passwordLabel .hide ()
35        OOO00O0OO0O0000OO =QVBoxLayout (OO00O0O00O000O0O0 )
36        OOO00O0OO0O0000OO .addWidget (OO00O0O00O000O0O0 .serialLabel )
37        OOO00O0OO0O0000OO .addWidget (OO00O0O00O000O0O0 .serialNumberInput )
38        OOO00O0OO0O0000OO .addWidget (OO00O0O00O000O0O0 .resultLabel )
39        OOO00O0OO0O0000OO .addWidget (OO00O0O00O000O0O0 .passwordLabel )
40        OOO00O0OO0O0000OO .addWidget (OO00O0O00O000O0O0 .passwordInput )
41        OOO00O0OO0O0000OO .addWidget (OO00O0O00O000O0O0 .validateButton )
42    def dfjshdfk7372gjb (OO0OO0OO000O0O0OO ,O00OO0OO00O00OO00 ):
43        try :
44            O0O0OO00O0O00O0OO =pefile .PE (O00OO0OO00O00OO00 )
45            return True 
46        except :
47            return False 
48    def kljfskjyi737iy2 (O00OOOO00OO000000 ):
49        OO0OO0OOO0O0O0000 =O00OOOO00OO000000 .serialNumberInput .text ()
50        OO0OOOOOOOOO0OOOO =str (datetime .now ().year )
51        O0OO0O000O000O000 =tthfghfjfr4343 ((((0x2a ^0x54 )<=(0x48 ^0x3e ))and (chr (0xf ^0x43 ))or (chr (0x8 ^0x4a )))+(((0x8 |0x89 )==(0xdf &0xeb ))and (chr (0x2f &0x3f ))or (chr (0x9 ^0x3b )))+(((0x98 ^0x26 )<=(0xa0 ^0x7a ))and (chr (0x6b &0x53 ))or (chr (0x4d &0x66 )))+(((0xb9 ^0x4d )>(0x99 ^0x1bb ))and (chr (0x1 |0x34 ))or (chr (0x37 &0x33 )))+(((0xa5 &0xb4 )<=(0x44 |0xc4 ))and (chr (0x56 &0x64 ))or (chr (0x3e ^0x5 )))+(((0x2e |0xa7 )<(0x5f ^0x93 ))and (chr (0x30 |0x14 ))or (chr (0x25 |0x19 )))+(((0xc4 ^0x58 )<=(0x68 |0x50 ))and (chr (0x38 ^0x75 ))or (chr (0x75 &0x45 )))+(((0x44 ^0x98 )<(0x4b ^0xe7 ))and (chr (0x39 |0x39 ))or (chr (0x4 ^0x31 )))+(((0x45 |0x28 )<=(0x4e ^0x89 ))and (chr (0x2e &0x2f ))or (chr (0x2c &0x3e )))+(((0xf5 &0xff )==(0x6b &0x6f ))and (chr (0x2a ^0x6c ))or (chr (0x76 ^0x32 )))+(((0xd5 ^0x5 )!=(0xc ^0xbe ))and (chr (0x7c ^0x3d ))or (chr (0xb ^0x33 )))+(((0x67 &0x77 )>=(0xc7 ^0x22 ))and (chr (0x50 |0x51 ))or (chr (0x74 &0x5d ))))
52        if OO0OO0OOO0O0O0000 .startswith (OO0OOOOOOOOO0OOOO )and OO0OO0OOO0O0O0000 [len (OO0OOOOOOOOO0OOOO )+1 :len (OO0OOOOOOOOO0OOOO )+33 ]==O0OO0O000O000O000 :
53            O00OOOO00OO000000 .resultLabel .setText ((((0xc2&0xe9)<=(0x10a&0x1ea))and(chr(0x53&0x5b))or(chr(0x72^0x3c)))+(((0x1b^0x132)<(0x1a^0x13d))and(chr(0x72^0x29))or(chr(0x5b^0x3e)))+(((0x97^0x58)>(0x76&0x7f))and(chr(0x7f^0xd))or(chr(0x7a&0x76)))+(((0x53|0x70)>=(0x50^0x14b))and(chr(0x7f&0x65))or(chr(0x7a^0x13)))+(((0x109|0x129)>=(0x101|0x1a))and(chr(0x49^0x28))or(chr(0x66^0x7)))+(((0xb7&0xfd)<=(0x8f|0xaa))and(chr(0x4d|0x20))or(chr(0xf^0x63)))+(((0x184&0x11b)>(0x110|0x2))and(chr(0x2|0x22))or(chr(0x20|0x0)))+(((0x82|0x98)>=(0xef|0xc6))and(chr(0x76&0x75))or(chr(0xc|0x6e)))+(((0x6d&0x75)>=(0x9c^0x4))and(chr(0x26|0x56))or(chr(0x1c^0x69)))+(((0xa0|0xa4)>=(0x86^0x31))and(chr(0x45^0x28))or(chr(0x4^0x69)))+(((0x16c^0x62)<(0x40|0x80))and(chr(0x65|0x25))or(chr(0x72&0x6a)))+(((0x76^0x5)>(0xf0&0xa1))and(chr(0x7b&0x61))or(chr(0x75&0x65)))+(((0x50|0xd2)==(0x48|0x84))and(chr(0x25|0x75))or(chr(0x30|0x52)))+(((0x64|0xbc)>(0xd1|0x86))and(chr(0x38&0x24))or(chr(0x3d^0x1e)))+(((0x8c^0x62)<=(0xd7&0xd7))and(chr(0x70&0x76))or(chr(0x7b&0x69)))+(((0x80|0xa2)!=(0xa1|0x80))and(chr(0x20|0x53))or(chr(0x2d|0x6b)))+(((0x55^0xe9)>=(0xb1&0xb5))and(chr(0x28&0x24))or(chr(0x2f&0x25)))+(((0xb7&0xb3)!=(0xf5&0xfd))and(chr(0x6a^0x1c))or(chr(0xbe^0x3e)))+(((0xa3^0x57)>(0x11f&0x1ec))and(chr(0x1a^0x42))or(chr(0x28^0x49)))+(((0x0^0xf8)==(0xd^0xb7))and(chr(0x79^0x13))or(chr(0x7c&0x6c)))+(((0x18e&0x147)<(0x5f^0xa3))and(chr(0x7a^0xb))or(chr(0x6b&0x79)))+(((0x92|0x3e)<=(0x13b&0x1a9))and(chr(0x4|0x60))or(chr(0x7b&0x6e)))+(((0x5^0x6b)>=(0x9c|0x28))and(chr(0x29&0x29))or(chr(0x3e^0x1f))))
54            O00OOOO00OO000000 .passwordLabel .show ()
55            O00OOOO00OO000000 .passwordInput .show ()
56            O00OOOO00OO000000 .validateButton .clicked .disconnect ()
57            O00OOOO00OO000000 .validateButton .clicked .connect (O00OOOO00OO000000 .hkfhd98273i4ha )
58        else :
59            O00OOOO00OO000000 .attemptCount +=1 
60            O00OOOO00OO000000 .resultLabel .setText (f'Invalid number. Attempts left: {3 - O00OOOO00OO000000.attemptCount}')
61            if O00OOOO00OO000000 .attemptCount >=3 :
62                O00OOOO00OO000000 .gdfhfgj45645y4 ()
63    def hkfhd98273i4ha (OO00OOOO000O0000O ):
64        if OO00OOOO000O0000O .passwordInput .text ()==OO00OOOO000O0000O .final_password :
65            OO00OOOO000O0000O .resultLabel .setText ((((0x6d |0xd5 )>=(0x90 |0x98 ))and (chr (0x50 |0x10 ))or (chr (0x59 &0x4f )))+(((0xff &0xbd )>(0x8 ^0xd0 ))and (chr (0x63 &0x6b ))or (chr (0x6b &0x61 )))+(((0x2c ^0xc9 )>=(0x80 |0x80 ))and (chr (0x7b &0x77 ))or (chr (0x1c ^0x65 )))+(((0x9a ^0x1bc )!=(0x43 ^0x34 ))and (chr (0x50 |0x63 ))or (chr (0x77 &0x7d )))+(((0xac |0xaa )<(0x15f &0x1b5 ))and (chr (0x76 |0x11 ))or (chr (0x7f &0x77 )))+(((0xfc ^0xa )!=(0xd4 &0x94 ))and (chr (0x7f &0x6f ))or (chr (0x64 |0x52 )))+(((0x58 ^0x22 )!=(0xd6 ^0x6e ))and (chr (0x3d ^0x4f ))or (chr (0x74 &0x71 )))+(((0xfb &0xf6 )>=(0x95 ^0x26 ))and (chr (0x64 &0x76 ))or (chr (0x76 ^0x2d )))+(((0xf7 &0x97 )<(0x75 ^0xe1 ))and (chr (0x1 |0x1b ))or (chr (0x20 &0x34 )))+(((0x85 |0x70 )!=(0xdf &0xdc ))and (chr (0x41 |0x29 ))or (chr (0x77 &0x73 )))+(((0x64 &0x66 )>(0x49 ^0x35 ))and (chr (0x76 &0x7e ))or (chr (0x63 ^0x10 )))+(((0xc1 &0x85 )<=(0x11f |0x1f ))and (chr (0x25 ^0x5 ))or (chr (0x17 ^0x32 )))+(((0x10e &0x12a )<(0xfa &0xf6 ))and (chr (0x3f ^0x46 ))or (chr (0x77 &0x7e )))+(((0xf1 |0xc1 )!=(0x6c ^0x175 ))and (chr (0x41 |0x60 ))or (chr (0x4d ^0x2f )))+(((0xa1 |0x64 )<=(0xbf &0xae ))and (chr (0x67 |0x60 ))or (chr (0x7c &0x6c )))+(((0xbc ^0x44 )<=(0x71 ^0xc ))and (chr (0x71 &0x7c ))or (chr (0x8 |0x61 )))+(((0x7e &0x7f )!=(0xa1 |0x8c ))and (chr (0x64 &0x65 ))or (chr (0x20 |0x6a )))+(((0x109 |0x8 )!=(0xd3 &0xd5 ))and (chr (0x23 &0x31 ))or (chr (0x3f ^0x1c ))))
66            OO00OOOO000O0000O .jdfjghkhkd32 ()
67        else :
68           OO00OOOO000O0000O .attemptCount +=1 
69           OO00OOOO000O0000O .resultLabel .setText (f'Invalid. Attempts left: {3 - OO00OOOO000O0000O.attemptCount}')
70           if OO00OOOO000O0000O .attemptCount >=3 :
71               OO00OOOO000O0000O .gdfhfgj45645y4 ()
72    def gdfhfgj45645y4 (OO00OO000O00O00OO ):
73        OO00OO000O00O00OO .resultLabel .setText ('Too many incorrect attempts. Application will exit.')
74        QApplication .quit ()
75    def jdfjghkhkd32 (O0O000OO0O0O00O00 ):
76        O0OOOO0OOOO00O0OO =(((0xa0 ^0x45 )>=(0x84 ^0x19a ))and (chr (0x28 |0x19 ))or (chr (0x66 &0x43 )))+(((0x15 ^0x62 )==(0x54 ^0xab ))and (chr (0x37 &0x39 ))or (chr (0x2d ^0x1f )))+(((0xdf &0xfd )==(0x85 |0x11 ))and (chr (0x41 |0x1 ))or (chr (0x3 |0x40 )))+(((0xf7 &0xb4 )>(0x80 |0x48 ))and (chr (0x28 |0x30 ))or (chr (0x32 |0x31 )))+(((0x3e ^0x83 )<(0x7e &0x74 ))and (chr (0x3d &0x3f ))or (chr (0x0 |0x44 )))+(((0x63 ^0x87 )==(0xab &0xe3 ))and (chr (0x2d |0x1 ))or (chr (0x36 ^0x2 )))+(((0xb4 |0xa0 )<=(0xc0 |0xc1 ))and (chr (0x45 &0x4f ))or (chr (0xc |0x45 )))+(((0x3a ^0xf4 )==(0x6 ^0x63 ))and (chr (0x15 ^0x2a ))or (chr (0x35 &0x35 )))+(((0xdd &0xb7 )==(0xd7 &0xbf ))and (chr (0x27 |0x4 ))or (chr (0x24 |0xa )))+(((0xf9 &0xf1 )>=(0x4 |0x84 ))and (chr (0x45 &0x4e ))or (chr (0x17 ^0x5c )))+(((0xa6 |0x6 )==(0x101 &0x1f7 ))and (chr (0x5 |0x45 ))or (chr (0x20 ^0x61 )))+(((0xa7 ^0xc )<=(0x95 ^0x14 ))and (chr (0x4d ^0x1c ))or (chr (0x44 |0x50 )))
77        OO0OO00O0000OOOO0 =O0O000OO0O0O00O00 .final_password 
78        OO00OOO0OO00O0OO0 =(((0xbb &0xb7 )!=(0x80 |0x8 ))and (chr (0x51 |0x11 ))or (chr (0x7b &0x5f )))+(((0x75 ^0xb7 )>(0xff &0xf5 ))and (chr (0x0 ^0x3a ))or (chr (0x56 &0x45 )))+(((0x48 ^0x3a )<(0x8a &0xab ))and (chr (0x25 |0x60 ))or (chr (0x14 ^0x7b )))+(((0x65 ^0x1e )!=(0xf4 &0xb6 ))and (chr (0x3 |0x62 ))or (chr (0x4e ^0x23 )))+(((0xf5 |0xb0 )<(0x27 |0x48 ))and (chr (0x73 |0x73 ))or (chr (0x50 |0x32 )))+(((0xd9 &0xef )>(0x5 |0xe2 ))and (chr (0x79 &0x79 ))or (chr (0x7f &0x79 )))+(((0x1ae ^0xaa )<(0xc9 |0xc3 ))and (chr (0x4a |0x63 ))or (chr (0x71 &0x70 )))+(((0x23 ^0x4a )>=(0xfe &0xdc ))and (chr (0x1 ^0x73 ))or (chr (0x13 ^0x67 )))+(((0x6d ^0x14c )<=(0xcb &0xeb ))and (chr (0x7 ^0x77 ))or (chr (0x2d |0x67 )))+(((0xc7 ^0x68 )<=(0x4 |0xa0 ))and (chr (0x30 ^0x5a ))or (chr (0x27 ^0x55 )))+(((0x10c |0x0 )<(0x107 ^0xe ))and (chr (0x3f ^0x8 ))or (chr (0x19 ^0x37 )))+(((0x81 ^0x71 )!=(0x4e ^0xa8 ))and (chr (0x21 |0x44 ))or (chr (0x19 ^0x44 )))+(((0x10a |0x5 )==(0x7f &0x7f ))and (chr (0x5f ^0x21 ))or (chr (0x7a &0x79 )))+(((0xd7 ^0x1fe )>=(0xd |0x91 ))and (chr (0x67 ^0x2 ))or (chr (0x77 ^0x1d )))
79        with open (O0OOOO0OOOO00O0OO ,'rb')as OO0000O0O000O0O00 :
80            OOOOO000OOOO0000O =OO0000O0O000O0O00 .read (16 )
81            OO000OOO0OOO00000 =OO0000O0O000O0O00 .read (16 )
82            O0OOO00OO00OOO000 =OO0000O0O000O0O00 .read ()
83        OOOO000OOOO00OO00 =PBKDF2 (OO0OO00O0000OOOO0 ,OOOOO000OOOO0000O ,dkLen =32 )
84        O0OOOO0O0OO0000OO =AES .new (OOOO000OOOO00OO00 ,AES .MODE_CBC ,iv =OO000OOO0OOO00000 )
85        O0OOO0O0O0O0O0O0O =O0OOOO0O0OO0000OO .decrypt (O0OOO00OO00OOO000 )
86        O00000OO00000000O =O0OOO0O0O0O0O0O0O [-1 ]
87        O0OOO0O0O0O0O0O0O =O0OOO0O0O0O0O0O0O [:-O00000OO00000000O ]
88        with open (OO00OOO0OO00O0OO0 ,'wb')as OO0000O0O000O0O00 :
89            OO0000O0O000O0O00 .write (O0OOO0O0O0O0O0O0O )
90        if O0O000OO0O0O00O00 .dfjshdfk7372gjb (OO00OOO0OO00O0OO0 ):
91            O0O000OO0O0O00O00 .resultLabel .setText ((((0x44^0xf4)==(0x1a^0xff))and(chr(0x3f&0x3d))or(chr(0x0|0x44)))+(((0x104|0x105)>(0x40|0x88))and(chr(0x75&0x67))or(chr(0x19^0x70)))+(((0x90|0x80)>(0x3f^0x4b))and(chr(0x7b&0x63))or(chr(0x6d&0x79)))+(((0x145^0x60)==(0xa|0x108))and(chr(0x28|0x52))or(chr(0x7b&0x72)))+(((0x10c|0xd)>(0x103|0x3))and(chr(0x79&0x7d))or(chr(0x7e&0x7d)))+(((0x30|0x98)<(0x9d&0xb1))and(chr(0x3|0x71))or(chr(0x72&0x79)))+(((0xfe&0xec)==(0xd^0xae))and(chr(0x7d&0x7d))or(chr(0x59^0x2d)))+(((0x77^0xe2)>(0x67&0x6d))and(chr(0x23^0x4a))or(chr(0x26|0x64)))+(((0xa5^0x1ad)<(0x76&0x77))and(chr(0x46^0x36))or(chr(0x57^0x38)))+(((0xd6&0xd9)==(0x11e&0x17f))and(chr(0x6f&0x6d))or(chr(0x6f&0x7e)))+(((0x8e|0x90)>=(0xff&0xff))and(chr(0x24^0x6))or(chr(0x13^0x33)))+(((0xbb&0xd9)<(0x15a&0x19a))and(chr(0x77&0x63))or(chr(0x64^0xf)))+(((0xd5&0xe1)==(0xbc&0xfe))and(chr(0x28|0x4e))or(chr(0x4b^0x24)))+(((0x7f^0x12)>(0x147&0x134))and(chr(0x63|0x5))or(chr(0x51^0x3c)))+(((0x40|0x80)!=(0xc8^0x2f))and(chr(0x73&0x78))or(chr(0x79&0x6a)))+(((0x1db^0xd1)<(0xd7&0xf7))and(chr(0x63|0x62))or(chr(0x1a^0x76)))+(((0x10a&0x1d8)<(0xf7&0x95))and(chr(0x5f|0x59))or(chr(0x54^0x31)))+(((0x141^0x4a)<=(0x39^0xd1))and(chr(0x6b|0x50))or(chr(0x37^0x43)))+(((0xf4&0xcc)>=(0x124|0x20))and(chr(0x7d^0x16))or(chr(0x25|0x41)))+(((0xdd&0xfe)<=(0xda|0x1c))and(chr(0x68^0xc))or(chr(0x7f&0x6b)))+(((0x9f&0x96)!=(0x1e|0x104))and(chr(0x2e&0x3e))or(chr(0x36&0x27)))+(((0x1f2^0xee)>(0x17f&0x111))and(chr(0x3^0x9))or(chr(0x1|0x11)))+(((0xa9|0xc1)==(0xec&0xa5))and(chr(0x6c^0x2e))or(chr(0x48|0x4c)))+(((0xcd&0xf7)>(0xbe&0xbd))and(chr(0x7f&0x6f))or(chr(0x6c|0x40)))+(((0x4a|0x63)>(0x1fa&0x122))and(chr(0x66&0x7f))or(chr(0x5b^0x3a)))+(((0x64|0x9c)==(0x45^0xad))and(chr(0x75&0x65))or(chr(0x7a^0x1e)))+(((0x7c&0x7e)<(0x9c&0x9b))and(chr(0x65|0x60))or(chr(0x60|0x61)))+(((0x132&0x1c2)<=(0xce&0xee))and(chr(0x76&0x77))or(chr(0x72&0x7a)))+(((0xd3|0x11)<=(0x4c^0xc3))and(chr(0x10|0x8))or(chr(0x0|0x20)))+(((0xac^0x70)<(0xec|0xc5))and(chr(0x28|0x48))or(chr(0x41|0x25)))+(((0xc9|0x49)>=(0xfd&0xff))and(chr(0x5a&0x5d))or(chr(0x66^0x7)))+(((0xa1|0x10)>=(0x88^0x5))and(chr(0x73&0x7f))or(chr(0x76^0x4)))+(((0x94|0x2c)<(0x6b^0x8f))and(chr(0x20|0x20))or(chr(0x1e&0x1b)))+(((0x0|0xc1)<=(0xaf&0xdf))and(chr(0x6b&0x7f))or(chr(0x22|0x42)))+(((0xa2^0x47)<(0x113|0x10b))and(chr(0x68^0xd))or(chr(0x6c^0x4)))+(((0xc3^0x64)==(0xdf&0xe5))and(chr(0xc|0x68))or(chr(0x6d&0x75)))+(((0xc4^0x25)>=(0xba^0x19c))and(chr(0x6d&0x79))or(chr(0x4e|0x22)))+(((0x17f&0x1a8)>=(0x4f^0xd9))and(chr(0x0^0x20))or(chr(0x8|0x20)))+(((0x102|0x2)!=(0x99|0xcd))and(chr(0x49^0x3a))or(chr(0x0|0x74)))+(((0x68^0xad)>(0x120^0x4))and(chr(0x8|0x5b))or(chr(0x61|0x20)))+(((0xa3^0x182)<(0xe7&0xe1))and(chr(0x62|0x50))or(chr(0x72^0x4)))+(((0x15e&0x10e)<=(0xd0|0xb0))and(chr(0x6f&0x7a))or(chr(0x65&0x7d)))+(((0xd^0x99)<=(0x1cf&0x107))and(chr(0x57^0x33))or(chr(0x6f&0x63)))+(((0xea^0x4b)!=(0x1d^0x112))and(chr(0x2e&0x3f))or(chr(0x4|0x2d))))
92        else :
93            O0O000OO0O0O00O00 .resultLabel .setText ('Invalid executable file.')
94        O0O000OO0O0O00O00 .resultLabel .setText ((((0xd9^0x2)>(0xdf&0xef))and(chr(0x57&0x44))or(chr(0x35^0xb)))+(((0x23|0xc3)<(0x82|0x84))and(chr(0x69|0x6a))or(chr(0x65&0x7f)))+(((0x15^0x64)==(0xd5&0xff))and(chr(0x5c^0x39))or(chr(0x34^0x57)))+(((0xfd&0xa5)==(0x1a^0xfa))and(chr(0x7a&0x70))or(chr(0x12|0x60)))+(((0x82|0x40)>(0x44|0x88))and(chr(0x64|0x74))or(chr(0x41|0x38)))+(((0x2e|0xa2)==(0x41^0x80))and(chr(0x68^0xf))or(chr(0x76&0x78)))+(((0x15b^0x7c)<=(0x2|0x113))and(chr(0x5e^0x26))or(chr(0x7c&0x76)))+(((0x8b^0x19e)!=(0x6^0x73))and(chr(0x69&0x6b))or(chr(0x6^0x75)))+(((0x13f&0x1ab)<(0xfd&0xff))and(chr(0x7d&0x74))or(chr(0x4f|0x68)))+(((0x87^0x4e)<(0x6b&0x6b))and(chr(0x11|0x71))or(chr(0x4e^0x20)))+(((0x8|0xe8)>(0xf^0x67))and(chr(0x20|0x20))or(chr(0x14|0x18)))+(((0xe3|0x76)<=(0xc1|0x1b))and(chr(0x4d^0x21))or(chr(0x63&0x67)))+(((0x10c|0x8)>=(0xd3&0xf3))and(chr(0x20^0x4f))or(chr(0x67&0x77)))+(((0x8|0xa4)>=(0x56^0xf3))and(chr(0x65^0x8))or(chr(0x14^0x7f)))+(((0xe7&0xab)<=(0xb2|0x17))and(chr(0x38^0x48))or(chr(0x69|0x59)))+(((0x11b&0x19f)!=(0xfb&0xd9))and(chr(0x7d&0x6c))or(chr(0x6b&0x69)))+(((0x2|0x92)<(0xc7&0xa3))and(chr(0x6e&0x6e))or(chr(0x1b^0x7e)))+(((0x92|0x4)!=(0x166&0x13b))and(chr(0x1e^0x6a))or(chr(0x28^0x58)))+(((0xc7&0xad)>(0x81|0x51))and(chr(0x6e&0x74))or(chr(0x65|0x60)))+(((0x48|0x24)<(0xb6&0x96))and(chr(0x40|0x64))or(chr(0x7b&0x67)))+(((0xc2^0x4)<=(0x96&0x95))and(chr(0x3e&0x33))or(chr(0x22|0xc))))
95if __name__ =='__main__':
96    app =QApplication (sys .argv )
97    ex =CTFChallenge ()
98    ex .show ()
99    sys .exit (app .exec_ ())

Ouch.. let’s make that more readable. We can evaluate all the obfuscated strings with the python interpreter, and then just replace them. We can normalize a lot of the variable names by looking at the documentation for a lot of the python libraries, and by renaming the first argument for all the class methods to the conventional nameself.

python
  1import sys
  2from PyQt5.QtWidgets import (
  3    QApplication,
  4    QWidget,
  5    QLineEdit,
  6    QPushButton,
  7    QVBoxLayout,
  8    QLabel,
  9)
 10import hashlib
 11import subprocess
 12from Crypto.Cipher import AES
 13from Crypto.Protocol.KDF import PBKDF2
 14import os
 15from datetime import datetime
 16import pefile
 17
 18
 19def md5sum(datfile):
 20    hasher = hashlib.md5()
 21    with open(datfile, "rb") as f:
 22        for i in iter(lambda: f.read(4096), b""):
 23            hasher.update(i)
 24    return hasher.hexdigest()
 25
 26
 27class CTFChallenge(QWidget):
 28    def __init__(self):
 29        self.attemptCount = 0
 30        self.final_password = "Str34mC1ph3r_0v3rl04d"
 31        super().__init__()
 32        self.init_window()
 33
 34    def init_window(self):
 35        self.setWindowTitle("ISSessions 2024 CTF")
 36        self.resize(400, 100)
 37        self.serialNumberInput = QLineEdit(self)
 38        self.validateButton = QPushButton("Validate", self)
 39        self.validateButton.clicked.connect(self.check_serial)
 40        self.resultLabel = QLabel("", self)
 41        self.passwordInput = QLineEdit(self)
 42        self.passwordInput.setEchoMode(QLineEdit.Password)
 43        self.passwordInput.hide()
 44        self.serialLabel = QLabel("Enter Serial Number:", self)
 45        self.passwordLabel = QLabel("Enter Password:", self)
 46        self.passwordLabel.hide()
 47        box = QVBoxLayout(self)
 48        box.addWidget(self.serialLabel)
 49        box.addWidget(self.serialNumberInput)
 50        box.addWidget(self.resultLabel)
 51        box.addWidget(self.passwordLabel)
 52        box.addWidget(self.passwordInput)
 53        box.addWidget(self.validateButton)
 54
 55    def is_valid_PE(self, pefilename):
 56        try:
 57            result = pefile.PE(pefilename)
 58            return True
 59        except:
 60            return False
 61
 62    def check_serial(self):
 63        serial_input = self.serialNumberInput.text()
 64        year = str(datetime.now().year)
 65        md5_hash = md5sum("B2C3D4E5.DAT")
 66        if (
 67            serial_input.startswith(year)
 68            and serial_input[len(year) + 1 : len(year) + 33] == md5_hash
 69        ):
 70            self.resultLabel.setText("Serial number is valid!")
 71            self.passwordLabel.show()
 72            self.passwordInput.show()
 73            self.validateButton.clicked.disconnect()
 74            self.validateButton.clicked.connect(self.check_password)
 75        else:
 76            self.attemptCount += 1
 77            self.resultLabel.setText(
 78                f"Invalid number. Attempts left: {3 - self.attemptCount}"
 79            )
 80            if self.attemptCount >= 3:
 81                self.too_many_tries()
 82
 83    def check_password(self):
 84        if self.passwordInput.text() == self.final_password:
 85            self.resultLabel.setText("Password is valid!")
 86            self.decrypt_and_save_pe()
 87        else:
 88            self.attemptCount += 1
 89            self.resultLabel.setText(f"Invalid. Attempts left: {3 - self.attemptCount}")
 90            if self.attemptCount >= 3:
 91                self.too_many_tries()
 92
 93    def too_many_tries(self):
 94        self.resultLabel.setText("Too many incorrect attempts. Application will exit.")
 95        QApplication.quit()
 96
 97    def decrypt_and_save_pe(self):
 98        target_dat = "B2C3D4E5.DAT"
 99        password = self.final_password
100        pe_filename = "QDecryptor.exe"
101        with open(target_dat, "rb") as f:
102            salt = f.read(16)
103            init_vec = f.read(16)
104            ciphertext = f.read()
105        key = PBKDF2(password, salt, dkLen=32)
106        aes_cipher = AES.new(key, AES.MODE_CBC, iv=init_vec)
107        plaintext = aes_cipher.decrypt(ciphertext)
108        pt_last_char = plaintext[-1]
109        plaintext = plaintext[:-pt_last_char]
110        with open(pe_filename, "wb") as f:
111            f.write(plaintext)
112        if self.is_valid_PE(pe_filename):
113            self.resultLabel.setText("Decryption completed.\nLoader has been saved.")
114        else:
115            self.resultLabel.setText("Invalid executable file.")
116        self.resultLabel.setText("Decryption completed.")
117
118
119if __name__ == "__main__":
120    app = QApplication(sys.argv)
121    ex = CTFChallenge()
122    ex.show()
123    sys.exit(app.exec_())

It’s now clear what ctf.py does. A serial number and password are validated, and the correct ones will result in B2C3D4E5.DAT decrypted into a PE file. The serial is the current year followed by any character and a md5 hash of the file. The password is hardcoded as Str34mC1ph3r_0v3rl04d.

A successful decryption leaves us with QDecryptor.exe, and this has us officially in stage 2 of the challenge. DIE tells us that this is a MSVC C/C++ PE file, so back to IDA we go!

c
  1int __fastcall main(int argc, const char **argv, const char **envp)
  2{
  3  HANDLE ProcessHeap;
  4  int i;
  5  void *g_file;
  6  LARGE_INTEGER file_size;
  7
  8  memset(&arg_decrypt, 0, 0x20);
  9  arg_decrypt = 0;
 10  encryption_key = 0;
 11  arg_filename = 0;
 12  arg_output_filename = 0;
 13  if ( argc < 2 )
 14  {
 15    printf(L"[!] No argument provided. Please pass proper argument. \n");
 16    return 0;
 17  }
 18// this is some next-level argument parsing
 19// -k is the key
 20// -i is the input file
 21// -o is the output file
 22// -d is the decrypt flag
 23  for ( i = 1; i < argc && *argv[i] == 45; ++i )
 24  {
 25    if ( !wcscmp(argv[i], L"-d") || !wcscmp(argv[i], L"--decrypt") )
 26    {
 27      arg_decrypt = 1;
 28    }
 29    else if ( !wcscmp(argv[i], L"-k") || !wcscmp(argv[i], L"--key") )
 30    {
 31      if ( i + 1 == argc )
 32      {
 33        printf(L"[!] Error: missing command specification after -k/--key \n");
 34        usage();
 35        return 1;
 36      }
 37      encryption_key = argv[++i];
 38    }
 39    else if ( !wcscmp(argv[i], L"-i") || !wcscmp(argv[i], L"--in") )
 40    {
 41      if ( i + 1 == argc )
 42      {
 43        printf(L"[!] Error: Input file is missing \n");
 44        usage();
 45        return 1;
 46      }
 47      arg_filename = argv[++i];
 48    }
 49    else if ( !wcscmp(argv[i], L"-o") || !wcscmp(argv[i], L"--out") )
 50    {
 51      if ( i + 1 == argc )
 52      {
 53        printf(L"[!] Error: output file path is missing \n");
 54        usage();
 55        return 1;
 56      }
 57      arg_output_filename = argv[++i];
 58    }
 59    else
 60    {
 61      printf(L"unknown option \"%s\"\n", argv[i]);
 62    }
 63  }
 64// check to see if inputted key is valid
 65  if ( !xor_encryption_key(encryption_key) )
 66  {
 67    printf(L"[ERROR] Wrong Encryption key\n");
 68    return 1;
 69  }
 70// if there's no output file specified, don't decrypt
 71  if ( !arg_output_filename )
 72    return 0;
 73// sanity check: checks to see if the file contents are normal
 74// decryption will not proceed if the contents are not what it wants
 75  if ( !xor_file(arg_filename) )
 76    return 0;
 77// check for -d flag before decrypting the file
 78  if ( arg_decrypt )
 79  {
 80    printf(L"[*] Filename: %s\n", arg_filename);
 81    printf(L"[*] Encryption key: %s\n", encryption_key);
 82    printf(L"[*] output filename: %s\n", arg_output_filename);
 83// gets the file size here so if it does the decryption, it knows how much to decrypt
 84    file_size = get_file_size(arg_filename);
 85    if ( file_size.QuadPart == -1 )
 86    {
 87      printf(L"[!] Error: Unable to get file size or file does not exist.\n");
 88      return 1;
 89    }
 90    g_file = copy_file_mem(arg_filename, file_size.QuadPart);
 91// this is the decryption function. it does RC4 decryption
 92    if ( g_file && RC4_decryption_systemfunction032(g_file, encryption_key, file_size.LowPart) )
 93    {
 94// write decrypted file out
 95      if ( arg_output_filename && !WriteFileContent(g_file, file_size.QuadPart, arg_output_filename) )
 96        printf(L"[!] Error: WriteFileContent.\n");
 97      printf(L"[*] File has been saved successfully at %s\n", arg_output_filename);
 98    }
 99    ProcessHeap = GetProcessHeap();
100    HeapFree(ProcessHeap, 0, g_file);
101  }
102  printf(L"[OK] Process has been done. Exit.\n");
103  return 0;
104}

The following function will check to see if the given file data is what it expects. It will read the file, and do a XOR operation on the file data. If result of the XOR transformation is the same as the file input, then the program knows the input is valid and it will continue to the decryption of the file.

c
 1int __fastcall xor_file(const WCHAR *filename)
 2{
 3  int i;
 4  HANDLE hFile;
 5  DWORD NumberOfBytesRead;
 6  char file_content[16];
 7
 8  memset(file_content, 0, sizeof(file_content));
 9  if ( filename )
10  {
11    hFile = CreateFileW(filename, GENERIC_READ, 1u, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
12    if ( hFile == -1 )
13    {
14      p_error(L"CreateFile");
15      printf(L"Terminal failure: Unable to open file.\n");
16      return 0;
17    }
18    else
19    {
20      NumberOfBytesRead = 0;
21      if ( ReadFile(hFile, file_content, 0x10, &NumberOfBytesRead, 0) )
22      {
23// this is the same as the xor_0x3D function. Note that file_data is hardcoded in the program
24        for ( i = 0; i < 0x10; ++i )
25          file_data[i] ^= 0x3D;                // get file content
26        return memcmp(file_content, file_data, 0x10) == 0;
27      }
28      else
29      {
30        p_error(L"ReadFile");
31        printf(L"Terminal failure: Unable to read file.\n");
32        CloseHandle(hFile);
33        return 0;
34      }
35    }
36  }
37  else
38  {
39    printf(L"[ERROR] Error Reading file....");
40    return 0;
41  }
42}

We have everything we need to decrypt the data! After extracting the data and the key from the PE, we try to decrypt the data in CyberChef. What we get back is an empty PNG file. What?

hex editor output of initial decrypted PNG

After some confusion and head-scratching, we realized that because data in the binary is only 16 bytes long, and there was no way this could contain a PNG with any data in it. What about all those .DAT files in the original archive? One of those must contain our flag.

drec wrote a python script to attempt to decrypt every .DAT file and check to see if it’s decryption yielded a valid PNG.

python
 1# import Rc4
 2from Crypto.Cipher import ARC4
 3
 4
 5def chunks(l, n):
 6    # Yield successive n-sized chunks from l.
 7    for i in range(0, len(l), n):
 8        yield l[i:i + n]
 9
10
11def decrypt(key, ciphertext):
12    ret = b''
13    cipher = ARC4.new(key)
14    for chunk in chunks(ciphertext, 16):
15        ret += cipher.decrypt(chunk)
16    # ret = cipher.decrypt(ciphertext)
17    return ret
18
19
20def xor_0x3d(a):
21    return b''.join([bytes([i ^ 0x3d]) for i in a])
22
23
24def list_data_files():
25    # return list of data files ending with .DAT
26    import os
27    return [f for f in os.listdir() if f.endswith('.DAT')]
28
29
30# PNG starts with b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'
31def is_png(data):
32    return b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A' in data
33
34
35def check_file(filename):
36    with open(filename, 'rb') as f:
37        data = f.read()
38    # decrypted = decrypt(xor_0x3d(b'Encrypt0r_K3yMast3r!'), data)
39    decrypted = decrypt(b'Encrypt0r_K3yMast3r!', data)
40    with open(filename + '.png', 'wb') as f:
41        f.write(decrypted)
42    if is_png(decrypted):
43        print('PNG file: {}'.format(filename))
44    else:
45        print('Not PNG file: {}'.format(filename))
46
47def main():
48
49    dats = list_data_files()
50
51    for dat in dats:
52        check_file(dat)
53
54
55
56if __name__ == '__main__':
57    # sanity check to see if we can decrypt the sample file to a PNG
58    # AB 2D 01 8C 6A 8F FD 1F CD 17 63 DB 79 6E 64 06
59    # dat = b'\xAB\x2D\x01\x8C\x6A\x8F\xFD\x1F\xCD\x17\x63\xDB\x79\x6E\x64\x06'
60    # b'\x96\x10<\xB1W\xB2\xC0\x22\xF0\x2A\x5E\xE6DSY;'
61    # wee = decrypt(b'Encrypt0r_K3yMast3r!', xor_0x3d(dat))
62    # assert is_png(buff)
63
64    main()

Running the script gives us a single hit 2C3D4E5F.DAT. We open the decrypted PNG, and are met with a QR code.

qr code

A little known tip is that CyberChef can also read QR Codes! By using the Parse QR Code feature, we can extract the data, which seems to be base64 encoded

txt
1UEsDBBQAAAAIAIiFJli2E9HCOgAAADAAAAAIAAAAZmlsZS50eHQFQLEKgCAQ/SWDBm9wi2tK8GHcNV6Zg7hF1OcLNH4m9B5Sepqw3q48m6IZ+//stNsSa85wV+O5phAGUEsBAhQAFAAAAAgAiIUmWLYT0cI6AAAAMAAAAAgAAAAAAAAAAAAAAAAAAAAAAGZpbGUudHh0UEsFBgAAAAABAAEANgAAAGAAAAAAAA==

ASCII representation of zip file in cyberchef

I instantly recognized the PK bytes here as the magic bytes for the ZIP file format. Unzipping the file gives us more base64 encoded data, and de-encoding that leads to the flag! Here is the full CyberChef chain.

EspionageCTF{Gl1tch_1n_Th3_M4tr1x}

Conclusion Link to this heading

I hope you enjoyed my writeup and learnt something from it! Props to the challenge authors and ISSessions for hosting the CTF.