Home Posts Projects Tags RSS

[EN] CTF writeup: CyberSlaskie 2024


Oct 24, 2024 - ctf cyberslaskie

Between 21 and 23 October 2024 I participated in a regional CTF challenge hosted by my university’s cybersecurity academic circle.

Challenges were divided into 4 categories:

In this write-up I will try to explain every challenge as quickly as possible. Some of the tasks were not as easy as they might look here so I congratulate everyone who took part :)

Alien

We’re given a file containing such cryptic string:

☊⏁⎎{⌿⍀⍜⎎⟟☊⟟⟒⋏⏁_⟟⋏_⏃⌰⟟⟒⋏}

After a while I was able to translate some glyps into letters:

☊ - C  (rotated 90°)
⏁ - T
⎎ - F
⍜ - O
⟟ - I
⟒ - E  (rotated -90°)
⏃ - A

This gives us:

CTF{??OFICIENT_IN_A?IEN}
CTF{PROFICIENT_IN_ALIEN}

Encode

There was a file containing supposedly random emojis. Such emojis could also be found in the challenge description. Connecting each of them with corresponding letter one by one by analyzing word patterns, I was able to get:

kosmici 🛠ostanowili 🔷o💧s💧e🔷💧⬇⚪ swoje d💧ia🌍ania na śląsku. sku🛠iają się g🌍💼wnie
na b🔷an🎵⬇ c⬇be🔷be💧🛠iec💧e🌋stwa, ofe🔷ując nowe, innowac⬇jne technologie. ich g🌍💼wn⬇m
celem jest 💧budowanie be💧🛠iec💧nej inf🔷ast🔷uktu🔷⬇ dla lokaln⬇ch fi🔷m. kosmici ws🛠💼🌍🛠
🔷acują 💧 🛠olskimi eks🛠e🔷tami, ab⬇ w🛠🔷owad💧a⚪ 🔷o💧wią💧ania do🛠asowane do s🛠ec⬇fiki
🔷⬇nku. w s💧c💧eg💼lności inte🔷esują ich s⬇stem⬇ siem i och🔷ona ich flagi 
ctf{custom_encoding_not_safe}.
ju🎵 te🔷a💧 ich technologia w💧bud💧a og🔷omne 💧ainte🔷esowanie.
💧astanawiają się nad otwa🔷ciem nowej sied💧ib⬇ w katowicach.

Robot

Opening the provided file, we see:

00110000 00110000 00110001 00110001 00110000 00110000 00110000 00110000 00100000...

Which obviously looks like a simple binary encoding. However it turned out, it was actually encoded with several layers of binary encoding. I wrote the script that unpacks the layers and gets the flag.

from io import BytesIO

def decode(inbio):
    inbio.seek(0)
    outbio = BytesIO()
    while True:
        bindata = inbio.read(8)
        byte = int(bindata, 2)
        outbio.write(bytes([byte]))
        if inbio.read(1) != b' ': break
    return outbio

bio = BytesIO()
with open('Kod_Maszynowy.txt', 'rb') as f:
    bio.write(f.read())

cnt = 1
while True:
    print('Iteration:', cnt)
    try:
        bio = decode(bio)
    except ValueError:
        break
    cnt += 1

bio.seek(0)
print(bio.read())

Patience

Going to the URL provided by the authors, we see a page with text:

The time of waiting for a flag has been started. Come back in 100 hours!

When I refreshed the page it said a bit less than 100 hours. How can the site know what is the wait time for me?

site screenshot 100 hours of waiting

That’s right - Cookies. Opening the developer console of Firefox I was able to modify the czas_start cookie to some small value and get the flag.

site screenshot with flag

Traffic

We get a pcap file however just by running strings on it, we can already see the message. It was hidden in the ping payload.

$ strings file.pcap | head -n 3
_4N4LYSIS}
_P4CKET
ctf{E4SY

Just reverse the direction and you have the flag :)

Out of TLS

This was quite possibly the most engaging challenge of this CTF. In the pcap file attached, there’s indeed no TLS traffic. Looking through the file with Wireshark I stumbled upon a plain HTTP GET request for jan.p12. From my own client certificate guide I knew, PKCS #12 is a very common format for storing user keys. The one problem is it’s usually encrypted (more on that later).

Digging deeper into the dump I also found an encrypted E-Mail message in pkcs7 format. After manually extracting it from Wireshark I got:

Subject: Tajna =?UTF-8?Q?wiadomo=C5=9B=C4=87?=
From: Jan =?UTF-8?Q?Szyfruj=C4=85cy?= <jan@topsecret.pl>
To: Kinga =?UTF-8?Q?Odszyfrowuj=C4=85ca?= <kinga@topsecret.pl>
Date: Thu, 17 Oct 2024 18:09:42 +0200
Content-Disposition: attachment; filename="smime.p7m"
Content-Type: application/pkcs7-mime; name="smime.p7m"; smime-type="enveloped-data"
Content-Description: S/MIME Encrypted Message
Content-Transfer-Encoding: base64
User-Agent: Evolution 3.46.4-2 
MIME-Version: 1.0

MIAGCSqGSIb3DQEHA6CAMIACAQAxggLIMIIBYAIBADBIMDAxDTALBgNVBAMMBHJvb3QxEjAQBgNV
BAoMCXRvcHNlY3JldDELMAkGA1UEBhMCUEwCFH/8U2AZmuEWqrd+BsJb/ka6lV8GMA0GCSqGSIb3
DQEBAQUABIIBAH2OdXEosWIyyuberW2ZWBlFnTDszxyoiSi2rjzgcC31qs723FskAvcH2UNxA7Up
...

Since we have Jan’s p12 certificate bundle, decryption should be rather easy:

$ openssl pkcs12 -in jan.p12 -nocerts -out /tmp/private.key
Enter Import Password:
Mac verify error: invalid password?

However we don’t have the password to decrypt the bundle. My first thought was the bundle might have been generated using -legacy option in OpenSSL which uses RC2 and SHA-1 (so rather not secure).

However checking the parameters it’s actually SHA256 with AES-256-CBC:

I had to try different ways of getting info about PCKS #12 bundle without actually extracting it, the plain openssl pkcs12 -info would not work.

$ openssl asn1parse -in jan.p12 -inform der -i -strparse 30
    0:d=0  hl=4 l=2709 cons: SEQUENCE          
    4:d=1  hl=4 l=1266 cons:  SEQUENCE          
    8:d=2  hl=2 l=   9 prim:   OBJECT            :pkcs7-encryptedData
   19:d=2  hl=4 l=1251 cons:   cont [ 0 ]        
   23:d=3  hl=4 l=1247 cons:    SEQUENCE          
   27:d=4  hl=2 l=   1 prim:     INTEGER           :00
   30:d=4  hl=4 l=1240 cons:     SEQUENCE          
   34:d=5  hl=2 l=   9 prim:      OBJECT            :pkcs7-data
   45:d=5  hl=2 l=  87 cons:      SEQUENCE          
   47:d=6  hl=2 l=   9 prim:       OBJECT            :PBES2
   58:d=6  hl=2 l=  74 cons:       SEQUENCE          
   60:d=7  hl=2 l=  41 cons:        SEQUENCE          
   62:d=8  hl=2 l=   9 prim:         OBJECT            :PBKDF2
   73:d=8  hl=2 l=  28 cons:         SEQUENCE          
   75:d=9  hl=2 l=   8 prim:          OCTET STRING      [HEX DUMP]:1EC65EB8E311E6A5
   85:d=9  hl=2 l=   2 prim:          INTEGER           :0800
   89:d=9  hl=2 l=  12 cons:          SEQUENCE          
   91:d=10 hl=2 l=   8 prim:           OBJECT            :hmacWithSHA256
  101:d=10 hl=2 l=   0 prim:           NULL              
  103:d=7  hl=2 l=  29 cons:        SEQUENCE          
  105:d=8  hl=2 l=   9 prim:         OBJECT            :aes-256-cbc
  116:d=8  hl=2 l=  16 prim:         OCTET STRING      [HEX DUMP]:927701D69C8EF3DDE1BCA0E78758B90C
...

My next thought was to brute force the key. Turns out there’s already a tool called crackpkcs12 doing exactly that. I left it running over night but it couldn’t guess the password.

$ crackpkcs12 -b jan.p12

Then as a last resort I tried running it with rockyou wordlist since the tool also accepts dictionaries.

$ crackpkcs12 -b jan.p12 -d rockyou.txt

Dictionary attack - Starting 4 threads

*********************************************************
Dictionary attack - Thread 3 - Password found: spongebob
*********************************************************

It was able to find the password in only a few seconds. Now since we have the password which is spongebob, we can extract Jan’s private key.

Type spongebob for Import Password and whatever for PEM pass phrase

$ openssl pkcs12 -in jan.p12 -nocerts -out private.key
Enter Import Password:
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:

Now we need raw p7m file to decrypt the mail so we remove all the E-Mail headers leaving just base64 encoded content in tajna_wiadomosc.p7m:

$ openssl smime -in tajna_wiadomosc_p7m.txt -pk7out tajna_wiadomosc.p7m > tajna_wiadomosc.p7m

To decrypt the mail, we can use openssl smime -decrypt:

Type the PEM pass phrase you set earlier

$ openssl smime -decrypt -in tajna_wiadomosc.p7m -inform PEM -inkey private.key -out decrypted_email.eml
$ cat decrypted_email.eml
Content-Type: application/pkcs7-mime; name="smime.p7m"; smime-type="signed-data"
Content-Description: S/MIME Signed Message
Content-Disposition: attachment; filename="smime.p7m"
Content-Transfer-Encoding: base64

MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwGggCSABIG5
Q29udGVudC1UeXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PSJVVEYtOCINCkNvbnRlbnQtVHJhbnNm
ZXItRW5jb2Rpbmc6IHF1b3RlZC1wcmludGFibGUNCg0KRmxhZ2EgdG86DQoNCkNZQkVSU0xBU0tJ
...

This doesn’t quite look like a decrypted message we anticipated…. The smime-type is now signed-data and it looks like, the s/mime format also enables verification of the message:

$ openssl smime -verify -in decrypted_email.eml -noverify -out verified_email.eml
Verification successful
$ cat verified_email.eml
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

Flaga to:

CYBERSLASKIECTF{d05b6b58-b347-4915-80e8-b3d2e9de43f4}

--=20
Pozdrawiam,
Jan

Now we finally have the flag :)

Corrupted

There’s an “unknown” file attached. The authors told us the first 8 bytes of the file should be changed to get some commonly used file format. When viewing the file in hex editor, we already see it’s a PNG (has IHDR and sRGB chunks).

00000000: 9152 6912 bbff ff0b 0000 000d 4948 4452  .Ri.........IHDR
00000010: 0000 0190 0000 0190 0802 0000 000f dda1  ................
00000020: 9b00 0000 0173 5247 4200 aece 1ce9 0000  .....sRGB.......

On the Wikipedia there’s a list of common file signatures. For PNG it is:

8950 4E47 0D0A 1A0A

Replacing the first 8 bytes with the signature we get a PNG image of troll face and CTF flag.

I used GHex editor to modify the file

Zip

This looks like a simple brute-force zip cracking challenge. We can use zip2john and then john to crack the password protected zip archive.

$ zip2john flag.zip > flag_crack.txt
flag.zip/flag/ is not encrypted!
ver 2.0 flag.zip/flag/ is not encrypted, or stored with non-handled compression type
ver 5.1 flag.zip/flag/flag.txt is not encrypted, or stored with non-handled compression type
$ john flag_crack.txt
...
0897             (flag.zip/flag/flag.txt)
1g 0:00:03:49 DONE 3/3 (2024-10-23 10:52) 0.004353g/s 21088p/s 21088c/s 21088C/s 03mc..morac

Now we can extract the archive with the password 0897 and get the flag.

Cats

We were given a few funny cat pics and a key: d41d8cd98f00. All pictures except for one have the following naming scheme: catX.{png|jpg}. The different one is called nothing_to_hide.jpg so that’s our primary suspect of hiding something.

$ steghide extract -p d41d8cd98f00 -sf nothing_to_hide.jpg
$ cat message
Vigenere has send you a gift: dht{z37g4771e6_1o70_m0id_q41egf4a3}
He is oN his phOne now, but Kudos to hIm for sending the 'indestructible' key, Apparently: 22 666 666 6 33 777

Those numbers look a lot like encoding used on older cell phones. Addicionally the capitalized letters in the last line form the word NOKIA.

Using multitap abc cipher decoder we’re able to extract the key to Vigenere message which is BOOMER. Then we’re able to decipher the flag with vigenere cipher decoder providing BOOMER as a key.

Look deeper

We get this look.jpg file. The first thing I noticed was it’s size (700KB and 8175x1500 resolution). At first I thought something’s hidden below the image (manually changing the JPG resolution) however that didn’t work. Then I noticed the text in lower left corner:

You might need this (this is not a flag!): hiuN~ohque<sh4oor/ag

Turns out, this image also has some data hidden steganographically and the string above is just the password:

$ steghide extract -p 'hiuN~ohque<sh4oor/ag' -sf look.jpg
$ cat look.txt 
Look deeper... 
You might need this (this is not a flag!): yae3aen~aizeeyoi9Ni

Now running binwalk -e on the image, we can extract a hidden zip archive hidden at the end.

It was actually the first thing I found but write-up needs not to be chaotic

$ binwalk -e look.jpg 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
727130        0xB185A         Zip archive data, encrypted at least v2.0 to extract, compressed size: 165, uncompressed size: 613, name: look.txt

$ cd _look.jpg.extracted/
$ unzip -P 'yae3aen~aizeeyoi9Ni' B185A.zip 
Archive:  B185A.zip
replace look.txt? [y]es, [n]o, [A]ll, [N]one, [r]ename: y
  inflating: look.txt

In the look.txt is the flag encoded as base64 (it’s also “hidden” using a ton of new lines).

$ echo 'Q1lCRVJTTEFTS0lFQ1RGe2ZlMGFiNTMyLTA4ZTYtNDI0Yy1hYWY4LWQxNTI0NzMxNTYzMH0K' | base64 -d
CYBERSLASKIECTF{fe0ab532-08e6-424c-aaf8-d15247315630}

Rev1

After putting code into Ghidra and cleaning it up a little, we see a simple xor of two 16 byte buffers (string_p3 and string_p1).

void check_code(char *arg1)
{
  int comparer;
  byte result [16];
  char *string_p3;
  char *string_p4;
  char *string_p1;
  char *string_p2;
  int i;
  
  string_p1 = (char *)0x455416952015c66;
  string_p2 = (char *)0x65d034558362803;
  string_p3 = (char *)0x77653339636a6832;
  string_p4 = (char *)0x613330336b645157;
  for (i = 0; i < 16; i = i + 1) {
    result[i] = *(byte *)((long)&string_p1 + (long)i) ^ *(byte *)((long)&string_p3 + (long)i);
  }
  string_p3 = (char *)0x77653339636a6800;
  comparer = strcmp(arg1,(char *)result);
  if (comparer == 0) {
    puts("Poprawna flaga!");
  }
  else {
    puts(&DAT_00102014);
  }
  return;
}

I believe you could write a script that does the xoring in a matter of minutes however initially I took a different route. Since the file is a regular amd64 Linux executable, we can run it with GDB and break just before this comparison to read the result.

comparer = strcmp(arg1,(char *)result);

We first start GDB on the executable, disassemble the check_code function and find the call to strcmp. We set breakpoint before the call and start the program (it doesn’t matter what we type as flag). The program passses the strcmp arguments via rsi and rdi registers so after the breakpoint triggers we’re able to print char pointers from them. We get our input and then the flag it’s compared to.

$ gdb ./reverse_engineering
(gdb) disassemble check_code
...
   0x00000000000011eb <+130>:	mov    %rdx,%rsi
   0x00000000000011ee <+133>:	mov    %rax,%rdi
   0x00000000000011f1 <+136>:	call   0x1050 <strcmp@plt>
   0x00000000000011f6 <+141>:	test   %eax,%eax
...
(gdb) b *check_code+136
Breakpoint 1 at 0x11f1
(gdb) r
Starting program: /home/oloke/Documents/polsl-ctf/reverse_engineering 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Podaj flage: asd

Breakpoint 1, 0x00005555555551f1 in check_code ()
(gdb) info registers
rax            0x7fffffffe4d0      140737488348368
rbx            0x7fffffffe638      140737488348728
rcx            0x0                 0
rdx            0x7fffffffe480      140737488348288
rsi            0x7fffffffe480      140737488348288
rdi            0x7fffffffe4d0      140737488348368
rbp            0x7fffffffe4c0      0x7fffffffe4c0
rsp            0x7fffffffe470      0x7fffffffe470
r8             0x31                49
r9             0xffffffffffffff88  -120
r10            0x0                 0
r11            0xa                 10
r12            0x1                 1
r13            0x0                 0
r14            0x7ffff7ffd000      140737354125312
r15            0x555555557dd8      93824992247256
rip            0x5555555551f1      0x5555555551f1 <check_code+136>
eflags         0x212               [ AF IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
fs_base        0x7ffff7d99740      140737351620416
gs_base        0x0                 0
(gdb) p (char*)0x7fffffffe4d0
$1 = 0x7fffffffe4d0 "asd"
(gdb) p (char*)0x7fffffffe480
$2 = 0x7fffffffe480 "T4k1Pr0sTyR3v3ng"

Exe

We are provided with a Windows executable this time. Launching it in Ghidra I first noticed this _srand(101) which to my understanding makes subsequent _rand() calls deterministic however I didn’t have Windows machine on hand to test msvcrt’s implementation of _srand.

In the code below the _Memory buffer gets allocated on heap and then it’s filled with those “random” values. Next we check the password using _check_password.

int __cdecl _main(int _Argc,char **_Argv,char **_Env)
{
  size_t arg1_len;
  int ret;
  void *_Memory;
  undefined4 pass_ok;
  int i;
  
  ___main();
  if (_Argc < 2) {
    _puts("Nie podales klucza!");
  }
  arg1_len = _strlen(_Argv[1]);
  if (arg1_len == 16) {
    _srand(101);
    _Memory = _malloc(64);
    for (i = 0; i < 16; i = i + 1) {
      ret = _rand();
      *(int *)(i * 4 + (int)_Memory) = ret % 30;
    }
    pass_ok = _check_password(_Argv[1],(int)_Memory);
    if ((char)pass_ok == '\0') {
      _bad_key();
    }
    else {
      _valid_key(_Argv[1],(int)_Memory);
    }
    _free(_Memory);
    ret = 0;
  }
  else {
    _bad_key();
    ret = -1;
  }
  return ret;
}

Taking a quick look at _check_password function reveals some weird XORing with randomly generated list of values from earlier.

undefined4 __cdecl _check_password(char *arg1,int memory_rand)
{
  undefined4 ret;
  
  if ((((((*(uint *)(memory_rand + 4) ^ (int)*arg1) == 0x44) &&
        ((*(uint *)(memory_rand + 8) ^ (int)arg1[2]) == 0x45)) &&
       ((*(uint *)(memory_rand + 0xc) ^ (int)arg1[4]) == 0x41)) &&
      (((*(uint *)(memory_rand + 0x18) ^ (int)arg1[6]) == 0x44 &&
       ((*(uint *)(memory_rand + 0x1c) ^ (int)arg1[9]) == 0x42)))) &&
     (((*(uint *)(memory_rand + 0x20) ^ (int)arg1[10]) == 0x45 &&
      (((*(uint *)(memory_rand + 0x24) ^ (int)arg1[0xc]) == 0x45 &&
       ((*(uint *)(memory_rand + 0x28) ^ (int)arg1[0xe]) == 0x46)))))) {
    ret = 1;
  }
  else {
    ret = 0;
  }
  return ret;
}

Finally the _valid_key function gives us some insights on how the flag is created.

void __cdecl _valid_key(char *arg1,int memory_rand)
{
  _printf("Poprawny klucz! Flaga to CTF{");
  _putchar(*(uint *)(memory_rand + 4) ^ (int)*arg1);
  _putchar(*(uint *)(memory_rand + 8) ^ (int)arg1[2]);
  _putchar(*(uint *)(memory_rand + 0xc) ^ (int)arg1[4]);
  _putchar(*(uint *)(memory_rand + 0x18) ^ (int)arg1[6]);
  _putchar(*(uint *)(memory_rand + 0x1c) ^ (int)arg1[9]);
  _putchar(*(uint *)(memory_rand + 0x20) ^ (int)arg1[10]);
  _putchar(*(uint *)(memory_rand + 0x24) ^ (int)arg1[0xc]);
  _putchar(*(uint *)(memory_rand + 0x28) ^ (int)arg1[0xe]);
  _puts("}");
  return;
}

The flag is just the “random” memory list XORed with user input. If you look closely into _check_password function, it appears the same XORed values compared in _check_password are then printed in _valid_key. From that we can conclude, the flag is: {0x44, 0x45, 0x41, 0x44, 0x42, 0x45, 0x45, 0x46} which is DEADBEEF in ASCII.

Rev2

Running strings on reverse_medium executable gives a lot of stuff associated with Python.

$ strings ./reverse_medium
...
blib-dynload/_bz2.cpython-312-x86_64-linux-gnu.so
blib-dynload/_codecs_cn.cpython-312-x86_64-linux-gnu.so
blib-dynload/_codecs_hk.cpython-312-x86_64-linux-gnu.so
blib-dynload/_codecs_iso2022.cpython-312-x86_64-linux-gnu.so
blib-dynload/_codecs_jp.cpython-312-x86_64-linux-gnu.so
blib-dynload/_codecs_kr.cpython-312-x86_64-linux-gnu.so
blib-dynload/_codecs_tw.cpython-312-x86_64-linux-gnu.so
blib-dynload/_contextvars.cpython-312-x86_64-linux-gnu.so
blib-dynload/_decimal.cpython-312-x86_64-linux-gnu.so
blib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
blib-dynload/_lzma.cpython-312-x86_64-linux-gnu.so
blib-dynload/_multibytecodec.cpython-312-x86_64-linux-gnu.so
blib-dynload/resource.cpython-312-x86_64-linux-gnu.so
blibbz2.so.1.0
blibcrypto.so.3
blibexpat.so.1
bliblzma.so.5
blibpython3.12.so.1.0
blibz.so.1
blibzstd.so.1
opyi-contents-directory _internal
xbase_library.zip
zPYZ-00.pyz
8libpython3.12.so.1.0
...

Currently the most popular tool used to create Python executables is Pyinstaller. There’s no compilation involved in this process. From what I remember, Pyinstaller just packs Python standard library into an archive and then creates an exe containing custom stub and the archive. It gets unpacked every time user starts the executable. From there on, the main script is run by a regular CPython interpreter.

There are some tools for unpacking the archive contained in the executable, notably the pyinstxtractor.

$ python pyinstxtractor.py reverse_medium
[+] Processing reverse_medium
[+] Pyinstaller version: 2.1+
[+] Python version: 3.12
[+] Length of package: 7906371 bytes
[+] Found 30 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: rev_2.pyc
[+] Found 102 files in PYZ archive
[+] Successfully extracted pyinstaller archive: reverse_medium

You can now use a python decompiler on the pyc files within the extracted directory

The tool even suggests the possible entry points. I think the most probable one is rev_2.pyc. Since .pyc file is just compiled bytecode, we need a tool to get the source out of it. I remember uncompyle6 being able to reverse the bytecode into human readable Python code in the past, however it doesn’t seem to support recent Python versions (we have 3.12 here while uncompyle6 supports up to 3.8).

Researching a bit more, I found pylingual which does the same thing but also works on newer Python bytecodes. Using it I was able to get this source code:

# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: rev_2.py
# Bytecode version: 3.12.0rc2 (3531)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)

def reverse_swap_in_blocks(text):
    parts = [text[i:i + 3] for i in range(0, len(text), 3)]
    reversed_parts = []
    for part in parts:
        if len(part) == 3:
            reversed_part = part[2] + part[1] + part[0]
        else:
            reversed_part = part
        reversed_parts.append(reversed_part)
    return ''.join(reversed_parts)

def reverse_caesar_cipher(text, shift):
    result = []
    for char in text:
        if char.isalpha():
            shift_base = ord('A') if char.isupper() else ord('a')
            new_char = chr((ord(char) - shift_base - shift) % 26 + shift_base)
            result.append(new_char)
        else:
            result.append(char)
    return ''.join(result)
encoded_flag = 's3YzqPSeHG31QQdby40IDhdBl4n31s4nH'
shift_value = 5
after_swap = reverse_swap_in_blocks(encoded_flag)
original_flag = reverse_caesar_cipher(after_swap, shift_value)
print('Podaj flagę: ')
expected_flag = input()
if original_flag == expected_flag:
    print('Poprawna flaga!')
else:
    print('Otóż nie.')

Just like in Rev1, the program decodes the flag into memory and does the comparison with whatever the user put in. We can add print(original_flag), run the script and get the flag.

Rev3

This challenge was definitely the hardest one, not necessarily in reverse engineering but more on that later. The reverse_hard executable also appears to be Pyinstaller executable so I will skip the extracting and decompiling part (look at Rev2).

In the decompiled source code there’s a comparison which checks whether the flag is correct. We can bypass that.

- if OADJSIKWHDWJw == KWDNoWwikwhdnw:
+ if OADJSIKWHDWJw == KWDNoWwikwhdnw or 1:

Now the script will think we put the right input every time. Since the flag is not dependent on user input, we can just skip this comparison. To run the script, you will probably need to add closing brackets for lists on lines 69 and 104.

Despite the program saying it will delete your files, it’s just a scare tactic. I analyzed the decompiled code thoroughly before running it :)

$ python rev3.py
Zagrajmy w ruletkę! Zgadnij liczbę z pewnego zakresu! 
Z jakiego zakresu? To jest reverse engineering, musisz się domyślić! 
Jeżeli zgadniesz poprawnie, to dostaniesz flagę! 
Jeżeli się pomylisz, to usunę twój folder roota! 
2137
Brawo! To jest poprawna liczba!
Oto zaszyfrowana flaga: VvBF58dHJ574Qx5r1tVG7f3nGif6db0aGkDn5Sh5RqM7wPu3l64v15eNm4vc1PP9tX9Zb
A oto tajemniczy klucz: Tw0j3Plik1S4B3zpi3cZn3.

We got the “encrypted” flag and a secret key. Here is where a lot of people got stuck. I tried decrypting the flag in many ways. At first it looks just like base64 however the length doesn’t match and in the source code there are many “decoy” flags with the same length. Trying to XOR the key with the flag in many ways, I couldn’t get anything meaningful out of it.

Hint

The CTF organizers gave us hint as pcap file containing captured IRC traffic. In the dump, authors were discussing this challenge and looking through the packets I found the alphabet needs to be 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ. I immediately went to vigenere cipher decoder since I tried that before with default settings. Sure enough, after putting the given alphabet, typing the encrypted flag and secret key for the password, the flag was revealed.

Note: it wasn’t in the typical flag format like CTF{...}, it still looked like garbage.