Post

UofTCTF 2024 - Writeups

UofTCTF 2024 - Writeups

Secret Message 2 - Forensics

The super secret organization changed their flag again. Can you work your magic again?

Hint: The flag characters contain abcdefghijklmnopqrstuvwxyz_

Solution

When we look at the given png file, we notice the redacted, pixelated text.

Screenshot

There is a tool called Unredacter that can uncover redacted, pixelated text to reveal the original text.

This blog provides extensive information about the tool: https://bishopfox.com/blog/unredacter-tool-never-pixelation.

I’ll follow the tool manual on how to use it.

Cropping the image

I’ll be using GIMP as recommended in the tool repo; we need to crop the image down to just the pixelated area.

Notice that each pixelated block size is `8x8 px’.

Screenshot1

Editing the tool configurations

Let’s take a look at the character set the tool uses in the preload.ts file.

It’s abcdefghijklmnopqrstuvwxyz

Screenshot1

Going back to the challenge description, the hint states that there is an underscore in the flag characters, so let’s add it to guessable_characters to make it look like this:

Screenshot1

Running the tool

We are now ready to start the tool. Run npm start and the tool GUI will pop up.

Screenshot1

Hit the Click to start button.

The tool is stuck on the first character :(

Screenshot1

Troubleshooting

I spent some time on this. The problem is with our cropped image. I noticed a small difference in colors using a color selector in the block before and upward of the pixelated area. The whole page white color is #ffffff:

Screenshot1

But the block before and upward of the pixelated area contains some other white shades like #fefefe and #fdfdfd, which means that it’s a part of the pixelated area.

Screenshot1

So I cropped the image again with one block before and upward of the previous pixelated area, so it looks like this:

Screenshot1

Getting the Flag

Now let’s run the tool again:

Screenshot1

uoftctf{pokemon_catching_ezz}

Illusion - Forensics

Good Luck.

Solution

We are provided with a file called traffic.pcapng. After exploring the file for a while, we can notice an unusual thing in the http traffic, which is the GET HTTP requests. it contains a weird path and parameter called /images?guid= with a base64 encoded data. Screenshot1

Decoding the base64 data will not give us anything readable. The data appears to be encrypted. Screenshot1 I exported the files from the http traffic. Screenshot1

and then viewed one of the files and it looks like google webpage Screenshot1

After some searching, I found out that it looks like TrevorC2 traffic. This is from the TrevorC2 repo:

TrevorC2 is a client/server model for masking command and control through a normally browsable website. Detection becomes much harder as time intervals are different and does not use POST requests for data exfil.

I read this article about TrevorC2 and it was very useful : https://nasbench.medium.com/understanding-detecting-c2-frameworks-trevorc2-2a9ce6f1f425

So now we know that the C2 conversation is hidden in two things:

  1. The server side hides its commands in <!-- oldcss=>
  2. The client side hides its data in /images?guid=

Let’s extract the client side data. you can use tshark to extract the data:

tshark -r traffic.pcapng -Y "http.request" -T fields -e http.request.uri > clientdata.txt

Now we can decrypt the data, to decrypt the data we can check the TrevorC2 repo to see the decryption method.

Screenshot1

We still need the encryption key :( , Lets try the default one which is Tr3v0rC2R0x@nd1s@w350m3#TrevorForget to see if it works.

Screenshot1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
ct["VWZvY0RaWTd1a2kwYmFkMlhyYXpBRWFNRVhNbTZwUFgrUUtVenhKdWtrUDlib3pcUNIS2Y2S3owNGVsRVdvZw==""ajcrT1pmTC9kMy8xNkRXQzFvdStUWFo5ZVV1OTgyMDduV0R1dWJoemdRUTN5OXFI2NXY2VKOFhKM3FQRGRKQldIVkgzYW1RYkZ5ckdHNVZtOGs4SXNkZEdpaHhVeGR2aFYN3dLMUF6RjZOM1FRc2trYkdjZFYrb1RLR3ZPb2puQUEzTUpLakRQd1FlREg1ZlFZmp3PT0=""WkN0eTk1TGdFb1JrZWZIL29VU0hhV0t0VjJocmJnTjVDUDR4eWU1OU8vSEppeVp6nJqS1IvcUprOVZPUVpHYnk2RjArZFQ0K25BTUkreEFtM2xxK00zVEZyQk1DQ3k4MFwNHVaSCszSUVCaDBrZ3ZDVWo4Tk0wMGNQblNZdDlqOTVBVzRCelBNYXV4cE9wbUVimN4N042QzBPckhNcVFtR2MybjBOTVh6Q0NmZlAzWXQ4dThkN0ozeWtYK0hEZmkrSTQTmdBRlA2RTJKblRNVGdZOGVVUGU3enJmYzVEb0RydG9ETVhWZ2dPMElZQUF3b3pDEN3eUZTdmxHeU5iV3VpYVpaSVR2TVNnOW9leW9RZU10bVBRZXQ2ejV4Zm4rZDQ0eHNdkhRR1llNXJZVERQSzYwYUJUbDMrNmRhUUlzbXRieXpwaW1nQjlQWmV1dVdDUytYmhQNXlCcHh1cjJ3L1BZTzRsLzVFTFFYa3JUblF3eTRxc25yMlR4YW9idk1LN3FzaGFVGZsbkZwMktxY09SUTZMTlo4bEVUOTdxQ2tHaDNkTGdjTVFMc2E0M3ZLdVp4YzdBUcwOFFRUjBjMHlLK3ptaVNnUCtjM2djeEg3UzVweU1NNlJNakd5Nk9neHcvbTB0QTEcjJRWDBBTU9YRm53MEpYbzhNcTVwWUhndGw2R0laak5OcUtWdG5JcnVpb0RTcW1imF1T295bngrbjNNdjVrWVZ1QXVQbDB4YXU1Q2ZOZERrWnRJaVZZUW1JZmpsZU1CaH1emRKWVREVXhhK2JwOXB3UHkxR21UZW4rK2QwZks4VGFEdXB3eWdqSnRTc1pydXV6DZ3RVliWG95dnUrQlhHcFE5Y1NCRWpnN2FmcWxzbGtmbDJ4R1FTWi9Sdy96ZGxKeF4SEZKc3oweE5PTC9CV3QzVUVMLzViZjljQ1Vva2tETFFQV2hHajJPZmYvU2xoU0o0Ux4TCtNPQ==""UzltSmsxK2RQM0ROWWJoWGwyWC9mSnZBY1VkSkJVeUVDS0hzNXZMOXoxYz0=""ZTcreEtlMzFSamMrRGNsMVloWDJyVmJuTFJIdzY5VGFZZWNqbzlxb1IvcjVsQnpIU1YS2YrbFZEZWdiQzM1M3I0QVpqSUY5UHF3ck15LzF4R3ZjLzFINWRxeFdWRVk4NWLclUxM0VBeEM1U3lxSU8xMnlrRkpNUG1XRTFNWUM=""ME9BMzRkK1I0cFAwc2pmTEFzWFk0N3lPMDBFRjZ1bGVGUnc0RGxlZ1RRdz0=""S2VHSVF1RGVhMjFrTm5vM1hoR052U25yVzZnSy9sWCtnYVN1WTVJSnJSRitmd20x2x3K2xCeXVsZUNWS3VCUkJxTUJQYjRQWERmV1Zjb29mbUtqRHFSbFU4alZyTzZ2eVxbkdsdXlINCtxZ0NQQWN2bFBzeUxGbEJOOWk2L2FlS0hhQmJqR0lqL1FPalJWbHFn21ZRnY2SG1uUGMrK3o3KytKcGdpTlNoQWtJZzM1QzZaZ1VXTXg1ck9qNmduTmQxaDFTzcxME9PakpnbEpsbVFoOHdSUWpKWkh0cnA1N3ovOUgrblJHR2FZeHp3c1F6YlJxWx3WVZpOGVmNlBlRlJYMFVIR0h3MittMGkxS1ErQ2lHUlB6ekhhYmIxSDlxRE50cHMck5hNWhPV1hYblJEVHc1RjQwQTBEUkhVQXV1OWcwT1NZTExtNGk3MGFaWmVUd1Fhk9XQjRucTVIR2hNcHBVS2hEb0V1YnhlMHM3Z2VuR2loVmtaRUo0NVpkYTdnNEFLWj2L1pjdUxHYkZGZjJhdzNoS3NBemZ3R09wc0JxblA4WTh6amRZbERlQmpqWVMxRkd3HNOMEpPMWdBeTl4TmI4NlBlYXJORVRBdFlJdGVnaGZDUnpBcHVnODhnR0dHa3h6UVEakdUbkg3K3VUWWMyMkJQM2I3LzNTNlQxc29KbzBJbGxZbEdwQ2srWHh4QjN5Z0QzXNicnUzekJuazlDWTh2MFVYcEl4V2FTZUlmWXlGT3Jxa1pqbkoxWTNRU1hGcUhBRmwNUc5dz09""OVdReUJzZ0MwR3JNRGljNnNqaEdPYzB5b1dTUUpwMkJMcGxzTyszSWJUUT0="]

import hashlib
import base64
from Crypto.Cipher import AES
class AESCipher(object):
    """
    A classical AES Cipher. Can use any size of data and any size of password thanks to padding.
    Also ensure the coherence and the type of the data with a unicode to byte converter.
    """
    def __init__(self, key):
        self.bs = 16
        self.key = hashlib.sha256(AESCipher.str_to_bytes(key)).digest()

    @staticmethod
    def str_to_bytes(data):
        u_type = type(b''.decode('utf8'))
        if isinstance(data, u_type):
            return data.encode('utf8')
        return data

    def _pad(self, s):
        return s + (self.bs - len(s) % self.bs) * AESCipher.str_to_bytes(chr(self.bs - len(s) % self.bs))

    @staticmethod
    def _unpad(s):
        return s[:-ord(s[len(s)-1:])]

    def encrypt(self, raw):
        raw = self._pad(AESCipher.str_to_bytes(raw))
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return base64.b64encode(iv + cipher.encrypt(raw)).decode('utf-8')

    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        iv = enc[:AES.block_size]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')

# add cipher key here
CIPHER = ("Tr3v0rC2R0x@nd1s@w350m3#TrevorForget")
cipher = AESCipher(key=CIPHER)


for i in ct: 
    print(cipher.decrypt(base64.b64decode(i)))

It worked !

Screenshot1

We’ve gotten one side of the conversation. Now we need the other part which is the server side. To extract the oldcss data, I simply used the OG method of strings and grep and I parsed the data manually

strings traffic.pcapng | grep "oldcss"

Lets feed the data again to our script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
ct=["cFl2gZ64Xe9FdCr2nMGlPPHKr//oqB/bWOaDiLiOLmQ=","KOw3LLlCqO1H0OzTaSCtdtDQEc1kaUHYDHK3URMinE=","kofJyYXCyx+kuQkUISD8xb+3wGEninpPGkCfO7cSdk=""pthScpHMvLajfJArnoBOuaJE4qyruZUvV9RH1W1xJglZllVYTUApMAqfCXFe0hvq8Ym5FBzjg6aIGGOO5HTBA==""cLLXq0nk9MPX9fcJgK7PdYhjHwrGu6qEkLPMNV7+QQ4=""uMS9CwMZkXrXyXA4LTbRKL6FJkKBxyM9Pi96rGVuHEGyu1h+Qa7UIPejb2PH1QL","sGpByUYockKmYpDyu84eFIOWENercq7Twup5JVfoh6w=""3unUUEgRnp/Cb5zCdqtNZk10QW1jno8SJ1x5elH408U=","TJMEJ6awBY5OJO8RDuIj1oQNItQ6QvaBkpb0vsGT6Cmj2DOIIhgAkVOw6M9xrey""IkMrNxaUs4zRtjU81TvQAaLFGe2L9wp3lowIDxexlEg=""seiTsoy2I3arpIXH3DhPqLWDq3Q5VyFfTSPg4FLgJnwzBNJwnFKoQmpit2Bihe+","noJvVRGs/silHFz2oPTldiey+XvgYNsutIOo4xVi2i4=""C9XqWpYeqCIn8Dk8gCVtpdg47vm8e8peFqkfQJ6WVbUvL7ucvQ0ayWnKRBF2G+ltFBWNMa+wawqeuvFK61RGvKVWogAqAVg4J7qmScn+HRF0QZFgEunXlAdu+16nnf","0uVBecny+wd0QC7IC2UV0O/5itgE4ZMSRKBkaeNvLbM="]

import hashlib
import base64
from Crypto.Cipher import AES
class AESCipher(object):
    """
    A classical AES Cipher. Can use any size of data and any size of password thanks to padding.
    Also ensure the coherence and the type of the data with a unicode to byte converter.
    """
    def __init__(self, key):
        self.bs = 16
        self.key = hashlib.sha256(AESCipher.str_to_bytes(key)).digest()

    @staticmethod
    def str_to_bytes(data):
        u_type = type(b''.decode('utf8'))
        if isinstance(data, u_type):
            return data.encode('utf8')
        return data

    def _pad(self, s):
        return s + (self.bs - len(s) % self.bs) * AESCipher.str_to_bytes(chr(self.bs - len(s) % self.bs))

    @staticmethod
    def _unpad(s):
        return s[:-ord(s[len(s)-1:])]

    def encrypt(self, raw):
        raw = self._pad(AESCipher.str_to_bytes(raw))
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return base64.b64encode(iv + cipher.encrypt(raw)).decode('utf-8')

    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        iv = enc[:AES.block_size]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')

# add cipher key here
CIPHER = ("Tr3v0rC2R0x@nd1s@w350m3#TrevorForget")
cipher = AESCipher(key=CIPHER)


for i in ct: 
    print(cipher.decrypt(i))

Screenshot1

uoftctf{Tr3V0r_C2_1s_H4rd_T0_D3t3c7}

EnableMe

You’ve received a confidential document! Follow the instructions to unlock it.

Note: This is not malware

Solution

We got a file called invoice.docm, .docm files are Microsoft Word macro-enabled document. I ran the file using an online sandbox called any.run and this what I got : Screenshot1

So let’s check the files macros using olevba. Screenshot1

Sub AutoOpen()
    Dim v6 As Variant, v7 As Variant
    v6 = Array(98, 120, 113, 99, 116, 99, 113, 108, 115, 39, 116, 111, 72, 113, 38, 123, 36, 34, 72, 116, 35, 121, 72, 101, 98, 121, 72, 116, 39, 115, 114, 72, 99, 39, 39, 39, 106)
    v7 = Array(44, 32, 51, 84, 43, 53, 48, 62, 68, 114, 38, 61, 17, 70, 121, 45, 112, 126, 26, 39, 21, 78, 21, 7, 6, 26, 127, 8, 89, 0, 1, 54, 26, 87, 16, 10, 84)
    
    Dim v8 As Integer: v8 = 23

    Dim v9 As String, v10 As String, v4 As String, i As Integer
    v9 = ""
    For i = 0 To UBound(v6)
        v9 = v9 & Chr(v6(i) Xor Asc(Mid(Chr(v8), (i Mod Len(Chr(v8))) + 1, 1)))
    Next i

    v10 = ""
    For i = 0 To UBound(v7)
        v10 = v10 & Chr(v7(i) Xor Asc(Mid(v9, (i Mod Len(v9)) + 1, 1)))
    Next i

    MsgBox v10
End Sub

The code takes the bytes in v6 and XOR them using the key (23) Simply apply that to the bytes in v6. Screenshot1 uoftctf{d0cx_f1l35_c4n_run_c0de_t000}

This post is licensed under CC BY 4.0 by the author.