Tuesday, September 24, 2013

CSAW CTF Quals: Cryptography 300 Onlythisprogram

For this challenge, we are given the onlythisprogram.tar.gz file. We extracted the files and in that file was the onlythisprogram.py script and an output file with 9 fileX.enc files. Looking through the python code, this line is of interest:
args.outfile.write(chr(ord(keydata[counter % len(keydata)]) ^ ord(byte)))
This line shows that the operation of interest is an XOR operation. Here is a reading on that vulnerability:

The first thing that I thought might have been it was crib dragging, exactly like it showed in the reading. Still brainstorming, however, I guessed that it probably wasn't just text files, but the ENC files were the encrypted files.

Okay, still looking at the same line we have above, the modulus is happening with the length of the keydata. Further above, you see this:
while (args.secretkey.tell() < blocksize):
Blocksize is set to 256 at the top so you know that the secretkey file is a 256 byte key that is used over and over again until the files are done being encrypted.

Great! Now it's time to build the decryption script.

I want the script to take the bytes from two files and then XOR them together and output that to an outfile. This is pretty much a straight Ctrl+C, Ctrl+V from the CSAW provided python file! #EPIC!

So, if each fileX.enc represented a file, then they would probably have headers that we know are at the beginning. This is the library that I used for the file headers.

So now you need to understand what cribbing does. Here's an example. If you have encrypted byte AE and encrypted byte BE, both of which use the same key (K), you can XOR AE and BE together. What that does is it gets rid of the key and gives you A ^ B, where A and B are the original bytes. At that point, if you XOR that with the original byte A, you will get the original byte of B.

So for this particular challenge, if we suspect that the file1 is supposed to be a DOC file, for example, we can assume the original byte in that position should be the header byte of a DOC file. If the bytes returned is not gibberish, then we know that one of file1 or whatever file we XOR'd it against to be a DOC file. Here's the code for this function:
header=open('header','w+')
header.write('\xFF\xD8\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00')
header.seek(0)

...

while 1:
    byte1, byte2 = args.infile1.read(1), args.infile2.read(1)
    if not byte1 or not byte2:
        break
    temp.write(chr(ord(byte1) ^ ord(byte2)))

temp.flush()
temp.seek(0)

while 1:
    byte1, byte2 = args.infile1.read(1), temp.read(1)
    if not byte1 or not byte2:
        break
    args.outfile.write(chr(ord(byte1) ^ ord(byte2)))
I did this with file0 and ran it against the other files until something gave me another header set that was in the website that I showed you. File0 was not a doc file, as most other files gave me gibberish in return, but one of the files was. I then took that file and ran the same operation against all the other files and was able to find all the file types.

file0 - MID, MIDI
file1, 3 - JFIF, JPE, JPEG, JPG
file2 - PNG
file4 - GZ, TGZ
file5 - BMP, DIB
file6 - GIF
file7 - DOC, DOT, PPS, PPT, XLA, XLS, WIZ
file8 - PDF, FDF

The longest header available to us is the JPG file type. After that, I was able to use trailer bytes to find out with certainty 4 more bytes. At this point, I was a little stumped so I downloaded a lot of files matching the file types I needed. Eventually, I realized that BMP files (at least in the ones I downloaded) had several large blocks of FF bytes. (Read using Hex Editor - I used HxD)

So using a byte of FF for the original message, I ran that against file5. Hopefully, I would get a large chunk of the secretkey.dat file. Maybe even the whole thing! Here's the code to do that:
while 1:
    byte1, byte2 = args.infile1.read(1), '\xff'
    if not byte1 or not byte2:
        break
    args.outfile.write(chr(ord(byte1) ^ ord(byte2)))
I named this file buzzbmp and went through every 256 byte block until I found one that matched up with my first 11 byte block. I copy and pasted that using HxD to a new secret.dat file and ran the original onlythisprogram.py script with the following code commented out:
#args.secretkey.truncate()

#while (args.secretkey.tell() < blocksize):
# maybe remove the next line for release since it makes it more obvious the key only generates once?
#	sys.stdout.write('.')
#	args.secretkey.write(os.urandom(1))
The file to decrypt I used was the PDF file, file8. After several iterations of error checking for correct and incorrect bytes, a decrypted PDF file that had a few errors. However, I had enough correct bytes in that secret.dat test file that a lot of strings were intact. I had about two bytes that were off. Here is that secret.dat file: secretkeywrong
Incorrect Secret.dat File

To narrow those down and correct them, I went through the file looking for intact strings. I was able to narrow down the incorrect bytes and, because I knew what a few of the strings should have looked like, I was able to correct the incorrect bytes and build the correct key! Manually. #CRYPTOOOO

secretkeycorrect
Correct Secret.dat File :)

I decrypted every file and then extracted everything from file4.gz. File4 contained the key, which we opened up in Notepad++. It says in ASCII block text, "For some reason psifertex really likes Aglets (??? - Not sure about this word really.) In this case it's necessary because the file size should not be a huge giveaway. Though I suppose images would have worked too. Anyway, the key: BuildYourOwnCryptoSoOthersHaveJobSecurity"

And there's your key! Here's all the code that I used to build my secret.dat files. It's a bit messy with the commenting though!
import sys
import argparse

parser = argparse.ArgumentParser(description="n.a")
parser.add_argument('--infile1', metavar='i1', nargs='?', type=argparse.FileType('rb'), help='input1 file, defaults to standard in', default=sys.stdin)
parser.add_argument('--infile2', metavar='i2', nargs='?', type=argparse.FileType('rb'), help='input2 file, defaults to standard in', default=sys.stdin)
parser.add_argument('--outfile', metavar='o', nargs='?', type=argparse.FileType('w+'), help='output file, defaults to standard out', default=sys.stdout)

# outfile a+ option opens file for both append and read. New file created for read/write if no a

args = parser.parse_args()
temp = open('temp', 'w+')
temp.truncate()
header=open('header','w+')
header.write('\xFF\xD8\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00')
header.seek(0)
key=open('key', 'w+')
key.write('\x40\x50\x3A\xC8\x2D\x69\xF7\xD3\x02\x3A\xF4\xAC')
key.seek(0)
#fuzz.open('fuzz', 'w+')
#fuzz.write('\xff'*256)

#while 1:
#    byte1, byte2 = args.infile1.read(1), args.infile2.read(1)
#    if not byte1 or not byte2:
#        break
#    temp.write(chr(ord(byte1) ^ ord(byte2)))

#temp.flush()
#temp.seek(0)

while 1:
    byte1, byte2 = args.infile1.read(1), '\xff'
    if not byte1 or not byte2:
        break
    args.outfile.write(chr(ord(byte1) ^ ord(byte2)))


sys.stderr.write('Done.\n')

Please comment down below if you have an comments or questions for me! Thanks for reading!

--dotKasper

No comments:

Post a Comment