Rating:

I opened up the zip file containing the encrypted files and malware.py.

One thing I noticed about malware.py is that it uses the AES CTR algorithm
to encrypt the files. That's essentially a stream cipher and I will refer
to the stream being xor'd with the plaintext as "keystream".

First, let's review AES CTR. What AES CTR does is that, instead of encrypting
the plaintext by running AES on the plaintext, as is done in ECB, it generates
a keystream by taking a counter block and, for every block that needs to be encrypted,
runs AES on the counter block and increments it by 1 after each iteration. This
keystream is then xor'd with the plaintext to get the ciphertext. An important
thing to note here is that this is completely deterministic.

More documentation about AES can be found here
https://pycryptodome.readthedocs.io/en/latest/src/cipher/aes.html

Now, I also notice that the IV used to encrypt the files varies only by 1
every time. That's an absurdly small amount and is enough to raise
doubt on the efficacy program used here.

I noticed that a PNG is in there. Now, those who have studied PNGs will know
that the first 16 bytes of a good PNG are predictable (8 byte magic header,
and 8 bytes for a length of 13 and chunk header 'IHDR'). So one might have the
initial idea to get the keystream for that file by doing an xor of the encrypted
file with the predicted bytes to get the original keystream.

This is a good idea, except that 16 bytes isn't enough to get the flag.
Let's not abandon this idea completely yet, though.

It took me a bit to actually realize that one of the files encrypted is
malware.py. That's actually really awesome, because we have the entire
plaintext of that file, so we can get a really long keystream from that.

How do we use that keystream, though? It's not immediately obvious.
After a bit of thinking, I realized I don't know what order os.listdir()
actually outputs filenames. I decided copy the encrypted files to a test folder,
renamed them without the extension. Then I ran os.listdir on my local machine
inside this new folder and I had an epiphany after I saw the following output

['malware.py', 'flag.txt', 'shopping_list.txt', 'CTF-favicon.png']

If this was the order the files were encrypted in, then getting the flag
is actually really easy. malware_keystream starting at byte 16 is the same
as flag_keystream starting at byte 0, since malware_keystream is initialized
with initial value IV and flag_keystream is initialized with initial value
IV + 1 (thus, at block 1 in malware, counter block = (IV) + 1, and at block
0 in flag, counterblock = (IV + 1)).

So I then opened up the python interpreter and went ahead to write the code
I needed. I made an assumption when writing it, which is that malware.py is
the first file encrypted.

After that, I entered the following in my interpreter

plaintext = open('malware.py', 'rb').read()

ciphertext = open('malware.py.enc', 'rb').read()

keystream = bytes([p ^ c for p, c in zip(plaintext, ciphertext)])

open('keystream', 'wb').write(keystream)

flagplain = open('flag.txt.enc', 'rb').read()

flagkeystream = keystream[16:]

print(bytes([p ^ k for p, k in zip(flagplain, flagkeystream)]))

flagkeystream = keystream[32:]

print(bytes([p ^ k for p, k in zip(flagplain, flagkeystream)]))

It actually outputs the flag when flagkeystream = keystream[32:]
but you'd keep going with this pattern for keystream[48:],
keystream[64:], ..., until you get the flag, or until
you realize that flag.txt was actually encrypted before malware.py
(which you hope is not the case)

After you do this, you get the flag, which is
UMASS{m4lw4re_st1ll_n33ds_g00d_c4ypt0}