Link: https://wargame.whitehat.vn/Challenges/DetailContest/143
Points: 200
Category: Forensics
Description
http://material.wargame.whitehat.vn/contests/11/for1_206e72e52f2f73fa1a1080b70d528657.zip
nc 118.70.80.143 7337
tl;dr
https://codisec.com/veles/.
Zip archive containing disk image. Mount it with ntfs-3g. There is a binary file and after looking for deleted files you can also find a .pyc file. Turns out the python code is a custom RSA implementation. It also includes code to contact server and query it for public key. After implementing function to decode files encrypted with python app we can use it to retrieve jpeg image from the first file we found on the disk image. The flag is visible on the image.
Solution
The first part of this task was done by Jakub, the second by me. So I’m basing the initial part on his notes.
In this task we got a zip archive. After unpacking it we see a single file:
1 2 3 4 5 6 7 8 |
$ file image > image: DOS/MBR boot sector, code offset 0x52+2, OEM-ID "NTFS ", sectors/cluster 8, Media descriptor 0xf8, sectors/track 63, heads 255, hidden sectors 128, dos < 4.0 BootSector (0x0), FAT (1Y bit by descriptor); NTFS, sectors/track 63, physical drive 0x80, sectors 20479, $MFT start cluster 853, $MFTMirror start cluster 2, bytes/RecordSegment 2^(-1*246), clusters/index block 1, serial number 060aabb3daabb0e92; contains Microsoft Windows XP/VISTA bootloader BOOTMGR |
Hmm, looks like a this image. Maybe we can mount it?
1 2 3 4 5 6 7 8 9 |
$ sudo ntfs-3g image /mnt/img $ ls -al /mnt/img > drwxrwxrwx 1 root root 4096 Jun 24 22:28 . drwxr-xr-x 3 root root 4096 Jun 25 04:19 .. -rwxrwxrwx 1 root root 220186 May 31 18:59 file $ file /mnt/img/file > /mnt/img/file: data |
Ok, so we have a file. But it’s just some binary data, no idea what to do with it. Maybe we can get something more? After a few minutes of googling we know enough to try it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ sudo umount /mnt/img $ ntfsundelete image > Inode Flags %age Date Time Size Filename ----------------------------------------------------------------------- 16 F..! 0% 1970-01-01 01:00 0 <none> 17 F..! 0% 1970-01-01 01:00 0 <none> 18 F..! 0% 1970-01-01 01:00 0 <none> 19 F..! 0% 1970-01-01 01:00 0 <none> 20 F..! 0% 1970-01-01 01:00 0 <none> 21 F..! 0% 1970-01-01 01:00 0 <none> 22 F..! 0% 1970-01-01 01:00 0 <none> 23 F..! 0% 1970-01-01 01:00 0 <none> 34 FN.. 100% 2016-06-24 19:08 6971 encrypt.pyc ... Files with potentially recoverable content: 1 ntfsundelete -u image -m encrypt.pyc |
Nice, we got a .pyc file. We can now recover it using uncompyle2. At this point Jakub said the he has some python file to analyse, so naturally I jumped on a chance to work on it 🙂
Turns out encrypt.py is a custom implementation of RSA. The code is somewhat obfuscated, with a bunch of dead code, some unused variables with strange values, etc. After reading the main function it is pretty clear that running it will encrypt all files in the current directory subtree, which matches the list of extensions defined in the code (.doc, .docx, .mp3, .txt, .jpeg, …). So instead of going deeper into the code I just created a directory with a single .txt file containing aaaaaaa. I’ve added a bunch of breakpoints in code using pdb and just printed out the exponent, the modulus and the data after each step of the algorithm (how it’s encoded to a number, padding, etc).
It looks like the program is reading the file in 100 byte chunks, encrypting each chunk and writing it to a resulting .encrypted file. However, it also appends some additional data after each chunk. This additional bytes are generated by the below function:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def init_spilt_string(): spilt_s = '' printable = string.printable len_ss = random_int(len(printable)) if len_ss <= 50: A = '@' spilt_s = printable[ord(A):printable.index(A)] + A spilt_s += chr(15) + chr(21) else: A = ord('@') spilt_s = printable[A:printable.index(chr(A))] + chr(A) spilt_s += chr(15) + chr(21) return spilt_s |
Like the rest of the code this function is intentionally complicated, but all it really does is return the #$%&'()*+,-./:;<=>?@\x0f\x15.
Interestingly the encrypt.py script also has a decrypt method.
1 2 3 4 5 6 7 8 9 10 11 |
def decrypt(): privateEx = getEx() def getEx(): s = socket.socket() server = ('118.70.186.203 ', 7337) s.connect(server) data_req = 'id=AOXo==&getEx=1' s.send(data_req) Ex = s.recv(500) return Ex |
Ok, looks like all it does is contact a remote server and get us the RSA private exponent. And that’s really all we need – with private exponent we can decrypt the RSA and that is the only one-way function in the script. For everything else we can just write a reverse function.
After some coding I have a script that can decrypt the simple text file I encrypted earlier. It also works with a larger “lorem ipsum” text file. Ok, let’s try something more difficult – a .png image of a cat. That one didn’t work out so well. It took me some debugging to find out that there is no logic in encrypt.py to add padding in case of RSA result being small enough to produce a hexstring at least 2 character shorter than expected).
Finally I managed to write a working decryption 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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
import sys # set file names here in_filename = 'to_decrypt' out_filename = 'result' # RSA params fname = 'to_decrypt' n=105635707994215785064592688829431603295092019332941340218063024030214112435477388955472078061967366591274439713776563251824009429910796926009945639353007371855413993984379598868696195164099879394346788069941545731813097331015058786201697958822353538490747425524617437354269664766909763679684047454688278011031 e=65537 priv=88564621686225804143599949348031629074448419889036649215278253785863628513354908121326408001477927349804924920474953757949280896835049634504821293313553289355709328104008395204711274370387211439046775523078243154006826451358656526662979651456823488028759120748152472580144809988367304986290600684644152983809 separator = [ord(x) for x in "#$%&'()*+,-./:;<=>?@"] separator.append(15) separator.append(21) def unrsa(data): return pow(data, priv, n) def to_hexlist(stuff): result = [] for c in stuff: num = ord(c) result.append("{:02x}".format(num)) return result def hexstring2data(hex): out = '' for (a, b) in zip(hex[0::2], hex[1::2]): out += chr(int(a + b, 16)) return out def decrypt_pack(data, use_padding=True): hexlist = to_hexlist(data) crypted = int(''.join(hexlist), 16) unc = unrsa(crypted) uhex = "{:x}".format(unc) if use_padding and len(uhex) < 200: uhex = ((200 - len(uhex)) * '0') + uhex return hexstring2data(uhex) def decrypt(in_file, out_file): with open(in_file, 'rb') as f: data = f.read() with open(out_file, 'wb') as outf: stuff = [] sstep = 0 for i, x in enumerate(data): if sstep==len(separator)-1 and ord(x) == 21: sstep = 0 use_padding = i + 1 != len(data) # pad to 200 bytes, unless it's last chunk outf.write(decrypt_pack(''.join(stuff[:-21]), use_padding)) # ignore 21 bytes of separator stuff = [] continue if ord(x) == separator[sstep]: sstep += 1 elif ord(x) == separator[0]: sstep = 1 else: sstep = 0 stuff.append(x) if __name__ == '__main__': decrypt(in_filename, out_filename) |
Running this script on the encrypted file we found on the disk image produced a JPEG image:
Later Jakub pointed out to me that the last step (padding) wasn’t really necessary. Without it the resulting jpeg was a bit garbled, but still easily readable.
Leave a Reply
You must be logged in to post a comment.