Introduction

This book contains the writeups for some of the CTFs I have played.

This book is created using mdBook. You can navigate to different CTFs using the table of contents at the left and change the color scheme using the button at the top.

RTN CTF 2021

The RTN CTF was the first CTF held by the RTN team, aimed at beginner to intermediate reverse engineers.

Clavdivs (100)

  • Challenge description: AVE CLAVDIVS!
  • Points: 100
  • Contents: message.txt
AVE CLAVDIVS!

MOIv/q.Z>\`n/mZH+m,opm,ZO.ZN/gpo\iox%

The first thing I did when seeing this challenge was google "CLAVDIS cipher" on google. Googling the name of the challenge, or any hints given in, it will usually help you on the right path. In this case, the first google result was "Caesar cipher". As most people know, the Caesar cipher works by adding an offset to each alphabetic character of the input, wrapping around if needed. Essentially, with an offset of 2, A would become C, M would become O, Z would become B, etc.

However, the input contained symbols as well. My first instinct was that perhaps instead of doing a normal Caesar cipher, I had to disregard the wrapping part and add an offset to the ascii value of each character instead. I threw the input into CyberChef and used the ADD operation to add a constant offset to each character in the encrypted part of the input. And sure enough, at offset 5 it produced the text RTN{4v3_Caes4r_M0r1tur1_T3_S4lutant}*. Cut off the trailing * character and we have our first flag.

CyberChef operation

NOTE: a previous version of the challenge required using SUB 5 instead. This was later changed because it produced invalid ASCII, which would trip some text editors up.

Mixitup (200)

  • Challenge description: We intercepted some secret pictures of which we believe contains a prototype of our competitor's newest smoothie recipe. It seems that they have mixed up more than just fruits eXclusively. Can you help us finding out what they added?
  • Points: 200
  • Contents: mixitup.zip

For this challenge, we're given a zip file. This zip file contains a .txt file containing a copy of the challenge description, and 2 bitmap files which contain what looks like noise.

From the challenge title and description, we can notice 2 hints. The first one is the challenge name, being "mixitup". Looks like we need to mix the 2 images in some way. Another hint seems to be the capitalization of the word "eXclusively". To me, this seemed to indicate the xor operation.

For those unfamiliar with how xor works, it is a bit-level operation where the output indicates if the 2 input bits differ. If we were to write this out in code, it would be like this:

0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0

To extend this operation to work on byte-level, we simple apply the operation on every bit of the input bytes. For example: 0x12 ^ 0x34 == 0x26. This becomes more obvious if we write the bytes down in their bit representation:

0001 0010
0011 0100
---------
0010 0110

Now that we understand how xor works, we can look at the files again. At first, I just used a small program by NirSoft to xor the 2 files together. The problem was that now, I no longer had a working bitmap file anymore. This is because a bitmap file normally contains a header with metadata. Luckily, bitmap files are quite simple, so I wanted to try copying over the header from one of the other files to fix it. Instead of trying to understand the structure of the file header, I just looked at both files in a hex editor to figure out where the structured, non-random data ended and where the encrypted, random data started.

Image 1, Hex Image 2, Hex

Can you tell where the header ends? An interesting property of xor is that if we xor some data with itself, it becomes all 00. Because of this, the header part is easier to notice in the xored file.

XORed image, Hex

Now it's easy to see! Let's copy over the header information from one of our encrypted images and put it in the xored image.

Final image, Hex

Now what do we get if we open this image?

Final image

I have to say that I agree with that. We can now submit the flag in the image for 200 points.

Ssssecret (400)

  • Challenge description: Let's share a secret...
  • Points: 400
  • Contents: message.txt
I want to share a secret,
A secret dear to me.
But the secret to keeping a secret
Is to never give out the key.

So I broke it up in little pieces
As small as they can be.
Then after that you wouldn't expect
I handed them out for free.

Follow up these pieces
Understand them to a degree
At the very root of it all
The answer you will see

AES-ECB('6eeb7683e9242ed56080e847c2f94cfc50634c29a3a50f14b31e7e3f97e34df0', key)

(1, d8877abe8c795a869b1fe28ed9a328a4)
(2, 1a1c134fc5c474899ee9d573b8895388)
(3, da143ab7a656e69a91b22414de846d0e)
(4, e4c6d40f50bd5f37d5c2f5e356a0af94)
(5, f344401d9a20a8ea1f08598434bfbf8e)

When I saw the key data, I immediately thought of some key splitting algorithm I had heard of in the past. I didn't remember its name, but after some googling I found the Shamir's Secret Sharing algorithm Wikipedia page.

To save you some time, Shamir's Secret Sharing algorithm allows you to split up a key into pieces, where a subset of the pieces allow you to reconstruct the full key. This means that if you can choose to generate 5 pieces, and from any 3 of them generate the original key again. The reason this works is because for a polynomial (a mathematical function in the format of f(x) = a, f(x) = ax + b, f(x) = ax^2 + bx + c, f(x) = a^4 + bx^3 + cx^2 + dx + e, etc.), you can only recover all the terms of the function by knowing at least N points on its graph, where N is the number of terms (in the case of f(x) = ax + b, that would be 2, ie. a and b). For example, an infinite amount of polynomials can go through 2 points, but once you add a third point, only 1 can go through.

Wikipedia has a great image that explains this visually:

3rd degree polynomials through 2 points

Image by Vlsergey on Wikipedia

By specifying eg. 5 points on on a 3rd degree polynomial, you can pick any 3 of them to recover the terms of that polynomial. Shamir's Secret Sharing algorithm is based on this idea. It creates a polynomial with terms where f(x) for x=0 is the secret we wish to share, and it then outputs the values f(x) for x being 1, 2, 3, etc. for as many pieces you want.

Knowing how Shamir's Secret Sharing works, we can go back to our input. It should be relatively trivial to recover the original key now, but reality is sadly never that simple. I tried many different online tools, but none of them produced a key that worked. I also tried the ssss-combine commandline tool, but that didn't work either. Eventually, a friend told me to use a specific commandline flag (-D) to disable some new feature, which was supposed to work.

I wrote the keys into a file called shamir in the following format:

01-d8877abe8c795a869b1fe28ed9a328a4
02-1a1c134fc5c474899ee9d573b8895388
03-da143ab7a656e69a91b22414de846d0e
04-e4c6d40f50bd5f37d5c2f5e356a0af94
05-f344401d9a20a8ea1f08598434bfbf8e

Then, I sent them to the commandline tool with cat shamir | ssss-combine -t 5 -D -x. The parameter -t 5 indicates that we require 5 pieces to reconstruct a key (using a 5th degree polynomial) and -x indicates that the output should be encoded in hex. This produced the key 5f616c65615f69616374615f6573745f. Some of you may recognize that this would result in ascii text when decoded, and you would be correct! This key would be _alea_iacta_est_, which is Latin for "the die is cast". This is a clear indication we've correctly decoded the key.

We're still not quite there yet. We now need to use this key to decode the ciphertext in the challenge. When solving this challenge I just used some online AES decryptor but it turns out CyberChef can do this too. We now have our flag, RTN{ssso_v4ry_s3cr3t}, which we can hand in for 400 points.

Rehasher (450)

  • Challenge description: I wrote a new asymmetric encryption algorithm, and it is truly unbreakable!
  • Points: 450
  • Contents: rehasher.zip (containing message.txt and smoothiecrypt.py)

message.txt

I wrote a new asymmetric encryption algorithm, and it is truly unbreakable!

Don't believe me? What if I told you I repeatedly hashed the input using SHA-256?

See you in fifty million years or so when you finally bruteforced my private key! >:)

Cipher text: 15e1c8c4bd2fcb12838d3f48e8fc567adf4fac1c36648eb270ad899a717dfadfccd9b49fc32a726711dd1d7faab7ed81
Public key: 537472347762337272795f536d303074686933535f42337374

We are given some ciphertext, a "public key" and a python program. Let's go through the program step-by-step.

    p = bytes.fromhex(sys.argv[1])
    k = bytes.fromhex(sys.argv[2])

    encrypted = encrypt(p, k)
    print(encrypted.hex())

To start, the python script parses its commandline arguments into bytes objects. To my knowledge, these are equivalent to byte[] in c# and other related languages.

def encrypt(plain_text, key):
    result = bytearray()

    padding_size = math.ceil(len(plain_text) / BLOCK_SIZE) * BLOCK_SIZE
    plain_text = plain_text.ljust(padding_size, b'\0')

    for i in range(0, len(plain_text), BLOCK_SIZE):
        result.extend(encrypt_block(plain_text[i:i+BLOCK_SIZE], key))

    return result

The program will pad the plaintext with 00 bytes until it is a multiple of 16 bytes (the block size). Then, it will loop over each block, encrypt it with the public key, and write it to a result bytes object which is later returned. This looks an awful lot like ECB (Electronic codebook) encryption. In this encryption mode, each block is encrypted on its own, without being influenced by surrounding blocks. This means that, if we wanted to crack this encryption, we only would have to do it 1 block at a time. Additionally, since the last block is padded with zeroes, it may be an easy block to crack since its actual size may be anywhere from 1 to 16 bytes.

def encrypt_block(block, key):
    a = block[0:BLOCK_SIZE // 2]
    b = block[BLOCK_SIZE // 2:BLOCK_SIZE]

    for k in key:
        c = hashlib.sha256(b + bytes([k])).digest()[0:BLOCK_SIZE // 2]
        a, b = b, bytes(x ^ y for x, y in zip(c, a))

    return b + a

The encrypt_block function takes a block and the key, and does some funny stuff to it. Because this code was somewhat hard to understand at first, I decided to rewrite it and sprinkle in some print statements to print the intermediary state.

def encrypt_block(block, key):
    print(f"encrypt_block {block.hex()}")
    a = block[0:BLOCK_SIZE // 2]
    b = block[BLOCK_SIZE // 2:BLOCK_SIZE]

    for k in key:
        print("\tk[i]")

        # take quarter of hash!
        c = hashlib.sha256(b + bytes([k])).digest()[0:BLOCK_SIZE // 2]

        print(f"\t\tc = {c.hex()} sha256(prev_b + key[i]) -> sha256({b.hex()} + {bytes([k]).hex()})")

        # a ^= sha256(b+key[i])
        a = bytes(x ^ y for x, y in zip(c, a))

        # swap(a, b)
        b, a = a, b

        print(f"\t\ta = {a.hex()} (prev_b)")
        print(f"\t\tb = {b.hex()} (c ^ prev_a)")

    return b + a

That looks a lot better! It seems the program will take your block, split it into 2, and iterate over the key to do a series of operations on the half-blocks. These operations seem to be hashing the second half with a character of the key as seed, taking a section of that hash and xoring the first block with it, then swapping the 2 blocks and continuing with the next character in the key. In pseudocode, it would look like this:

for k in key:
    c = hash(b + k)
    a = a ^ c
    a, b = b, a

At this point, I thought I completely understood the algorithm and figured that, since it uses a SHA256 hash, my only option was to create some brute-force cracker. So I looked at my options:

  • SHA-256 is part of the SHA-2 family, with a blocksize of 32bit words (4 bytes). In our algorithm, we only use 1/4th of the output it produces, which means we could make a slightly optimized brute-force algorithm. However, in the end the gains would be minimal, so I assumed I would have a rate of about 25MH/s (or 25 000 000 tries per second), which is what my GTX 1660 Super gets with Hashcat.
  • I could look into cracking the last block, since its length could be anything between 1 and 16 bytes in size. Additionally, since I knew that the input blocks would have to be valid ASCII (as they contain the flag) and had to end with } (as that is the end of a valid flag), I could optimize my brute-force cracker to only check those. I ended up trying up to length 5 on my CPU, but to no avail. In the end, this would not have been useful anyway, since the middle block is 16 bytes in size with no known parts.
  • Looking at the algorithm, I noticed I could just crack the 8-byte half-blocks and work my way backwards through the algorithm. This would mean cracking ~300 values where we have 64 unknown bits in each input. This also went nowhere, because even with the optimistic 25MH/s, it would still require 300 * 2^64 / 2 checks, which would take 110680464442257 seconds, or about 200 times the time since "the last glacial maximum" according to Wolfram Alpha (3.5 million years in total, if you're less historically inclined).

So brute-forcing wouldn't get me anywhere. I decided to let this challenge rest for a bit and to come back to it later.

You may remember that I put some comments in my refactored sourcecode. Assuming some simple input (0000000000000000FFFFFFFFFFFFFFFF) for plaintext, 000102 for public key, we get the following output:

encrypt_block 0000000000000000ffffffffffffffff
        k[i]
                c = ba67e86d827ab4c9 sha256(prev_b + key[i]) -> sha256(ffffffffffffffff + 00)
                a = ffffffffffffffff (prev_b)
                b = ba67e86d827ab4c9 (c ^ prev_a)
        k[i]
                c = 7958108e887d823f sha256(prev_b + key[i]) -> sha256(ba67e86d827ab4c9 + 01)
                a = ba67e86d827ab4c9 (prev_b)
                b = 86a7ef7177827dc0 (c ^ prev_a)
        k[i]
                c = bdd1d6913fe75782 sha256(prev_b + key[i]) -> sha256(86a7ef7177827dc0 + 02)
                a = 86a7ef7177827dc0 (prev_b)
                b = 07b63efcbd9de34b (c ^ prev_a)
07b63efcbd9de34b86a7ef7177827dc0

I ended up staring at this for a bit and finally noticed something odd: the value a after a round is the same value that was passed into the hash function! This allows us to completely reverse the algorithm by doing the following steps:

  • take the final output (07b63efcbd9de34b86a7ef7177827dc0) and split it up into block b (07b63efcbd9de34b) and a (86a7ef7177827dc0)
  • calculate c by appending the last character of the key 02 to a, which was b in the previous round. This gives us bdd1d6913fe75782
  • calculate the a value of the previous round by xoring b with c. this gives us ba67e86d827ab4c9
  • repeat for every character in the reversed key

I have implemented this algorithm in a C# project and then used it to decrypt each block. The code looks like this:

        private static readonly byte[] PublicKey = "537472347762337272795f536d303074686933535f42337374".FromHexString();

        private static readonly string[] CipherText = {
            "15e1c8c4bd2fcb12838d3f48e8fc567a",
            "df4fac1c36648eb270ad899a717dfadf",
            "ccd9b49fc32a726711dd1d7faab7ed81",
        };

        private static void Main(string[] args)
        {
            Console.WriteLine("Hello, world!");
            Console.Write(Encoding.ASCII.GetString(Decrypt(CipherText[0].FromHexString())));
            Console.Write(Encoding.ASCII.GetString(Decrypt(CipherText[1].FromHexString())));
            Console.Write(Encoding.ASCII.GetString(Decrypt(CipherText[2].FromHexString())));
            Console.WriteLine();
        }

        private static byte[] Decrypt(byte[] full)
        {
            var arr1 = new byte[8];
            var arr2 = new byte[8];
            Array.Copy(full, 0, arr1, 0, 8);
            Array.Copy(full, 8, arr2, 0, 8);
            return Decrypt(arr1, arr2);
        }

        private static byte[] Decrypt(byte[] b, byte[] a)
        {
            foreach (byte k in PublicKey.Reverse())
            {
                var c = a.CrippledHash(k);
                byte[] prev_b = a.ToArray();
                b.XorWith(c);
                a = b;
                b = prev_b;
            }

            var newArr = new byte[16];
            a.CopyTo(newArr, 0);
            b.CopyTo(newArr, 8);
            return newArr;
        }

If we run this, this prints out the following flag RTN{3ncrypt_b3c0m3s_D3crypT_1f_K3y_is_R3v3rs3d}.

After solving this challenge, the challenge author told me that it essentially implements a Feistel cipher, which "makes irreversible operations reversible". So in the end, this encryption wasn't really symmetric and our "public key" was just an encryption key!

The full C# sourcecode can be downloaded here, while the refactored python code (with comments and notes) can be downloaded here.

Noncense (800)

  • Challenge description: Dear RTN,

As you might know, I am the CEO of False Fruits Ltd, and we are one of the main producers of smoothies in the country.

Recently, I received a message from our IT department that our security system has problems. Two of our employees did not follow the protocol to the letter, which resulted in the formula of our latest smoothie getting leaked to the public.

As one of our regulars, it is my duty to inform you about any possible security breaches in our systems. We are still investigating this incident, but since you are an expert in the field, any help is appreciated.

Attached are some of the documents that resurfaced which you might find interesting.

Best regards, Dr. E. Zecutive CEO of False Fruits Ltd

P.S: The IT department said something about SHA 256 and public keys. Personally I have a hard time making heads and tails out of this nonsense.


noncense.zip contains the following files:

  • document01.pdf through document05.pdf
  • document01.sig through document05.sig
  • berry.pub, vgtables.pub
  • get_flag.sh
  • message.txt

message.txt contains a copy of the challenge description, so we can safely skip that.

get_flag.sh contains the following shellscript:

#!/bin/sh
scp -i key_file.pem vgtables@smoothie.rtn-team.cc:/home/vgtables/secret_document.pdf flag.pdf

vgtables.pub contains the following public key (and berry.pub contains a similar one):

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0UCtYSD1mAZ6bhWceNOXgF3MmKye
P/d6ZGsZfL5RZdYYuruVqVgxLZdvT3gZPVKGWvQlJAUyUI7oDfnTc07RIw==
-----END PUBLIC KEY-----

The .sig files contained some binary data.

So judging from the files, it seems that the PDF documents are signed through these public keys, and we need to generate key_file.pem so we can connect to the remote server over SSH to get the flag.

I started of by looking into the public keys. Using this site, I decoded the public key of vgtables into the following structure:

Algo ECDSA
Format X.509
 ASN1 Dump
EC Public Key [06:03:30:8a:a9:1a:d3:4e:0a:60:69:a0:c5:1c:7e:f0:14:ac:ac:9d]
            X: d140ad6120f598067a6e159c78d397805dcc98ac9e3ff77a646b197cbe5165d6
            Y: 18babb95a958312d976f4f78193d52865af425240532508ee80df9d3734ed123

It looks like ECDSA is used for this public key. According to Wikipedia it is possible to recover the public key if a nonce is re-used. And guess what, the title of this challenge hints towards nonces being important. If you want to see an informative and entertaining explanation of this vulnerability, this talk by fail0verflow for 27c3 on hacking the PS3. It's only ~5 minutes, and shows a legendary moment in console hacking history, so I definitely recommend watching it.

To abuse a nonce reuse vulnerability, we need to first find a reused nonce. We can list a hexdump of all signatures with the shellscript for i in *.sig; do print "$i:"; hexdump "$i"; done:

document01.sig:
0000000 4630 2102 fa00 9646 c7cd 25c5 d4a5 c381
0000010 87e1 1485 99b2 1765 b0dd d374 91b3 7ac2
0000020 0eb5 ff57 029e 0021 50ae a65f 4382 51e2
0000030 2a76 561c 24b3 2df3 085b 1c08 a1e2 f48e
0000040 328e 04b7 2ecb a088
0000048
document02.sig:
0000000 4530 2102 aa00 20f6 8a08 5609 58bd 51e9
0000010 9257 a802 c5ac 3a2b 4ffa 3fd7 30fd c68b
0000020 fdef fc1e 025c 2220 bf41 669f da35 71e7
0000030 07ae 54ca c15f 4380 2666 d3f6 d031 7393
0000040 b68f 159b 9784 0086
0000047
document03.sig:
0000000 4430 2002 4f68 5b82 9c58 dfcd ac23 8684
0000010 55c5 2142 43ba 8ef9 5516 8086 a368 17ed
0000020 ae02 400d 2002 5543 2b0d 28d3 6f0d 07d0
0000030 336f 77f2 1eb1 d6b6 9112 751e ff94 6a6d
0000040 1ae1 dbb7 7d4d
0000046
document04.sig:
0000000 4630 2102 aa00 20f6 8a08 5609 58bd 51e9
0000010 9257 a802 c5ac 3a2b 4ffa 3fd7 30fd c68b
0000020 fdef fc1e 025c 0021 68c7 1933 78b5 2130
0000030 87b6 8e96 4adb ca41 cf82 9b80 4735 5f90
0000040 c3a3 1800 df22 cea1
0000048
document05.sig:
0000000 4630 2102 d400 2a1a f54d 0649 2dec 6a5f
0000010 d1bb 476b 8ec9 46c7 0af9 9ba7 d59a fefc
0000020 1312 3186 02b6 0021 8bf6 6dfb 7a7b 742f
0000030 47bd dbec 4c96 f3fc 2390 eb8a 6409 f702
0000040 f3ff 8fe1 e9ec 4522
0000048

From looking at the PDF files, we know that documents 1, 3 and 5 are signed by "berry", and 2 and 4 by "vgtables". Looking at the signatures, we can notice that the first 36 bytes of document 2 and 4's signature are the exact same. From that I deduced that they most likely used the same nonce, so these were the signatures to try.

Elliptic-curve crypto is something I don't (and don't want to) understand, so I'll just use this tool by bytemare on GitHub. I checked out commit 68ea2aa3a8871ec1214cfcb0e7fb76590e34f404 and applied the following changes:

diff --git a/ecdsa-nonce_reuse-crack.py b/ecdsa-nonce_reuse-crack.py
index 6596f62..d4fff74 100755
--- a/ecdsa-nonce_reuse-crack.py
+++ b/ecdsa-nonce_reuse-crack.py
@@ -42,31 +42,21 @@ def rs_from_der(der_encoded_signature):
     :return: r, s
     """

-    # Look if library is available
-    try:
-        __import__("ecdsa.der")
-    except ImportError:
-        array = binascii.hexlify(base64.b64decode(der_encoded_signature))
-
-        # r
-        r_length = 2 * int(array[6:8], 16)
-        r = array[8:8 + r_length]
-
-        # s
-        s_offset = 8 + r_length
-        s = array[s_offset + 4:]
-
-        # Cast them to integers
-        # r = long(r, 16)
-        # s = long(s, 16)
-        r = int(r, 16)
-        s = int(s, 16)
+    array = binascii.hexlify(der_encoded_signature)

-    else:
-        rs, _ = der.remove_sequence(der.unpem(der_encoded_signature))
-        r, tail = der.remove_integer(rs)
-        # s, point_str_bitstring = der.remove_integer(tail)
-        s, _ = der.remove_integer(tail)
+    # r
+    r_length = 2 * int(array[6:8], 16)
+    r = array[8:8 + r_length]
+
+    # s
+    s_offset = 8 + r_length
+    s = array[s_offset + 4:]
+
+    # Cast them to integers
+    # r = long(r, 16)
+    # s = long(s, 16)
+    r = int(r, 16)
+    s = int(s, 16)

     if verbosity >= 2:
         print("r : " + str(r))
@@ -306,7 +296,7 @@ def get_file_content(_file):
     :param _file:
     :return:
     """
-    with open(_file, 'r') as file:
+    with open(_file, 'rb') as file:
         data = file.read()

     return data
@@ -334,9 +324,9 @@ def call_from_files(public_key_path, message1_path, message2_path, signature1_pa

     # Launch exploit to try to get private key
     private_key = get_private_key(public_verification_key.curve,
-                                  msg1.encode('utf-8'),
+                                  msg1,
                                   sig1,
-                                  msg2.encode('utf-8'),
+                                  msg2,
                                   sig2,
                                   get_hash_function(hash_alg))

Once we've made these changes, we can run the script with the following command:

python ecdsa-keyrec/ecdsa-nonce_reuse-crack.py -files \
    --pubkey vgtables.pub \
    --message1 document02.pdf --signature1 document02.sig \
    --message2 document04.pdf --signature2 document04.sig \
    --hashalg sha256 --output key_file.pem

And we're done! After making sure the correct file permissions are applied with chmod 700 key_file.pem, we can run the get_flag.sh shellscript and retrieve the PDF. Open it and we can the flag RTN{h0W_mUcH_m0r3_n0nS3ns3_d0_Y0u_w4Nt}.

Dragons (150)

  • Challenge description: Here at RTN be dragons! But where are they really?
  • Points: 150
  • Contents: dragons.zip (containing image.png and message.txt)

This challenge was fairly easy, as indicated by the points assigned to it.

We're given just an image file. As usual when you're given a single file, you should see if there's some interesting data hidden in it. One way to check that is with the strings command on Linux. If we run that, we see that our output ends on this:

...
!dt!
!dt!
IEND
7 is affine number:
WKU{3UC_0Q_Q1G3_1D_0Q73U_1XU0W3C}

This immediately looks like a letter-based cipher such as the Caesar or Vigenère cipher, so normally I would immediately throw this into CyberChef to try, but let's look up what this "affine" thing means first.

According to Wikipedia, Affine Cipher seems to be like Caesar cipher, but instead of f(x) = (x+a) mod 26, we do f(x) = (ax+b) mod 26. Now I don't know about you, but reading an entire Wikipedia for a 150-point challenge seems like a lot of effort to me.

Luckily Google comes to the rescue. Googling "Affine cipher cracker" brings you to decode.fr, a brilliant site with lots of crypto tools. Putting in the ciphertext in the correct box and clicking "automatic brute force decryption" loads the same page but with tons of possible decrypted flags. Search the page for "RTN" and we find the flag A=7,B=7 RTN{3ND_0F_F1L3_1S_0F73N_1GN0R3D}.

Looks like our 2 parameters were this "fine" number 7. But we didn't need to know, because we are lazy and know how to quickly cheese CTF challenges!

unknown_file (300)

  • Challenge description: I don't really know what to make of this file. I wish there was a way to visualize it, but all I see are blocks of data...
  • Points: 300
  • Contents: unknown_file.zip (containing message.txt and file.bin)

We are given a .bin file. A .bin extension usually means it is raw, unstructured data. The challenge description contains plenty of hints, too. It suggests you should try to visualize it, and that it are "blocks" of data.

Let's start with looking at a hexdump of the data:

/mnt/c/Users/HoLLy/Downloads/unknown_file ❯ xxd file.bin
00000000: 0000 0000 ffff ffff ffff ffff ffff ffff  ................
00000010: ffff ffff ff80 0000 ffff ffff ffff ffff  ................
00000020: ffff ffff ff80 0000 ffff ffff ffff ffff  ................
00000030: ffff ffff ff80 0000 e000 00ff f1ff 1f81  ................
00000040: f8e3 ffc0 1f80 0000 e000 00ff f1ff 1f81  ................
00000050: f8e3 ffc0 1f80 0000 e000 00ff f1ff 1f81  ................
00000060: f8e3 ffc0 1f80 0000 e3ff f8fc 0fc0 e071  ...............q
00000070: f81c 7fc0 ff80 0000 e3ff f8fc 0fc0 e071  ...............q
00000080: f81c 7fc0 ff80 0000 e3ff f8fc 0fc0 e071  ...............q
00000090: f81c 7fc0 ff80 0000 e380 38ff f1c0 fc0f  ..........8.....
000000a0: fffc 7e07 0380 0000 e380 38ff f1c0 fc0f  ..~.......8.....
000000b0: fffc 7e07 0380 0000 e380 38ff f1c0 fc0f  ..~.......8.....
000000c0: fffc 7e07 0380 0000 e380 38fc 71c7 fffe  ..~.......8.q...
000000d0: 071c 7ff8 e380 0000 e380 38fc 71c7 fffe  ..........8.q...
000000e0: 071c 7ff8 e380 0000 e380 38fc 71c7 fffe  ..........8.q...

We see a lot of 00 and FF bytes, and the other bytes seem to be repeating too. Combining this with the hint that we need to "visualize" this data, it's pretty safe to bet that this is some kind of bitmap image without header.

GIMP has a nice feature where it allows you to load raw data as an image with various encodings, so let's import it in that. Opening an image allows you to select the file type, and setting it to "raw data" allows us to get the dialog we want:

open file dialog

Then, we can go through the image types until we find something that vaguely represents something that is not noise. After that, adjust the width until everything seems to line up, and if needed change the offset to make sure the top left is actually at the top left. Then we get something like this:

import file dialog

Scan this with your favorite QR code scanner and we get the flag RTN{b1tm4ps_d0nt_n33d_h34d3r_1nf0}.