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:
- Crypto - Cryptographic
- Net - Networking (mostly
pcap
dumps) - Misc - Miscellaneous
- Reverse - Reverse Engineering
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 :)
-
Crypto
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())
-
Net
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?
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.
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 plainopenssl 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 :)
-
Misc
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}
-
Reverse
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.