Free Ski

  Challenge Progession:
Act III
Difficulty:
  Location:
Retro Emporium
FreeSki
Advice from a Goose

This challenge is about uncovering a flag thats hidden inside this SkiFree clone called "FreeSki". Speak to the goose Olivia in the Retro Emporium. She mentions that this game is impossible to win legitimately, and that "if you ain't cheatin' you ain't tryin'". I really like that motto. To win the game, you simply have to recovers all 5 treasures.
You can download it here:
FreeSki.exe

This game is written in Pygame, a python framework for game creation. It is then compiled to exe by PyInstaller. The good thing about Pyinstaller is that the resulting exe can *usually* be decompiled. So lets start by decompiling FreeSki.exe and seeing what we can find.

Skiing in Reverse

This game is written in python 3.13, which seems to result in only partial decompilation. To decompile the game, you must first extract the files. You can do this with PyInstaller Extractor.
Just run the tool likes so:

py pyinstxtractor.py FreeSki.exe
The resulting directory produceds will have the files from the compiled game, such as zip files, dll files, as well as PYC files. This FreeSki.pyc file is a compiled python file that we need to decompile.
FreeSki.pyc
I used a very cool online python decompiler called PyLingual to do the decompilation. I uploaded FreeSki.pyc to Pylingual, and it did have some errors during decompilation, but atleast we got part of the source! Check out the original decompiled FreeSki.py here:
FreeSki_Decompiled.py

Looking through the source, much of the game data is generated "randomly", but the seed for the random values are static. For instance the location of each treasure chest is determined by a random value, but the seed for the random function is the name of the mountain, which is a static value. The mountain names never change. So on each run, the locations of the treasures will be the same, because the seed never changes.

def GetTreasureLocations(self):
	locations = {}
	random.seed(binascii.crc32(self.name.encode('utf-8')))
	prev_height = self.height
	prev_horiz = 0
	for i in range(0, 5):
		e_delta = random.randint(200, 800)
		h_delta = random.randint(int(0 - e_delta / 4), int(e_delta / 4))
		locations[prev_height - e_delta] = prev_horiz + h_delta
		prev_height = prev_height - e_delta
		prev_horiz = prev_horiz + h_delta
	return locations
Taking a look out the treasures are stored in the Mountain class, treasures are stored as a dictionary, where the elevation is the key and the horizontal offset is the value. This will be important when it comes time to decode the flag.

Another thing to note here is that the function to generate the flag text is in the source. Each Mountain class has a class member that is the encoded flag text. Then the SetFlag function is called to decode it.
def SetFlag(mountain, treasure_list):
    global flag_text_surface
    product = 0
    for treasure_val in treasure_list:
        product = product << 8 ^ treasure_val
    random.seed(product)
    decoded = []
    for i in range(0, len(mountain.encoded_flag)):
        r = random.randint(0, 255)
        decoded.append(chr(mountain.encoded_flag[i] ^ r))
    flag_text = 'Flag: %s' % ''.join(decoded)
    print(flag_text)
    flag_text_surface = flagfont.render(flag_text, False, pygame.Color('saddle brown'))	
The flag text is decoded by looping through the mountain.encoded_flag byte by byte and performing an XOR by a not-so-random value. This value is not truely random because if you know the seed, you can predict all the values generated. The seed is generated by looping over the values in "treasure_list".

So to decode the flag ourselves, we need to know the values of "treasure_list" for any particular mountain object. If you look at the code, the part of the code that creates "treasure_list" is missing!

Much of the game logic in the main() game loop is missing. We know how to create the mountain objects, because that source is available:

Mountains = [
    Mountain(
        "Mount Snow",
        3586,
        3400,
        2400,
        b'\x90\x00\x1d\xbc\x17b\xed6S"\xb0<Y\xd6\xce\x169\xae\xe9|\xe2Gs\xb7\xfdy\xcf5\x98',
    ),
    Mountain(
        "Aspen",
        11211,
        11000,
        10000,
        b"U\xd7%x\xbfvj!\xfe\x9d\xb9\xc2\xd1k\x02y\x17\x9dK\x98\xf1\x92\x0f!\xf1\\\xa0\x1b\x0f",
    ),
    Mountain(
        "Whistler",
        7156,
        6000,
        6500,
        b"\x1cN\x13\x1a\x97\xd4\xb2!\xf9\xf6\xd4#\xee\xebh\xecs.\x08M!hr9?\xde\x0c\x86\x02",
    ),
    Mountain(
        "Mount Baker",
        10781,
        9000,
        6000,
        b"\xac\xf9#\xf4T\xf1%h\xbe3FI+h\r\x01V\xee\xc2C\x13\xf3\x97ef\xac\xe3z\x96",
    ),
    Mountain(
        "Mount Norquay",
        6998,
        6300,
        3000,
        b'\x0c\x1c\xad!\xc6,\xec0\x0b+"\x9f@.\xc8\x13\xadb\x86\xea{\xfeS\xe0S\x85\x90\x03q',
    ),
    Mountain(
        "Mount Erciyes",
        12848,
        10000,
        12000,
        b"n\xad\xb4l^I\xdb\xe1\xd0\x7f\x92\x92\x96\x1bq\xca`PvWg\x85\xb21^\x93F\x1a\xee",
    ),
    Mountain(
        "Dragonmount",
        16282,
        15500,
        16000,
        b"Z\xf9\xdf\x7f_\x02\xd8\x89\x12\xd2\x11p\xb6\x96\x19\x05x))v\xc3\xecv\xf4\xe2\\\x9a\xbe\xb5",
    ),
]
What we don't know is how the game constructs the treasure_list. One amazing thing about PyLingual is that it does present us with the bytecode of the compiled python. We can look through the bytecode and see if we can piece together how it was constructed.

This is the section of bytecode that proves that SetFlag is called with the current Mountain map as well as the treasure_collected list.

SetFlag Call
When a treasure is collected the following bytecode is ran.
Treasure Collected Bytecode
The in the above code, the collided_row value is multiplied by the current mountain width, then the collided_row_offset is added to it. This collided_row_offset is just a way of saying the treasure's horizontal offset from 0. Since we know that treasures are stored in the Mountain class as a dictionary where the elevation (collided_row) is the key and the horizontal offset (collided_row_offset) is the value, we can write a function to decode the flag.

Decoding the Flag

The things we need to borrow from FreeSki.py is the Mountain class as well as the list of Mountain objects. After that we need the copy the SetFlag function. Then we need to create a function that generates the treasure_list as the game would have. Here is the function that recreates the treasures_collected list. Then we can just call SetFlag with a mountain and that list.

def decode_flag(mnt):
    mountain_width = 1000
    treasures_collected = []
    for collided_row,collided_row_offset in mnt.treasures.items():
        treasures_collected.append(collided_row * mountain_width + collided_row_offset)
    flag = SetFlag(mnt, treasures_collected)
    return ''.join(flag)

if __name__ == "__main__":
    mnt = Mountains[0]
    flag = decode_flag(mnt)
    print(flag)
    encode_flag(mnt, flag)
I selected the first mountain on the list and ran it through the decode_flag function and got the flag text!
frosty_yet_predictably_random
Enter that in your badge to complete the challenge!

If you want my full flag decode script check here:
freeski_flag_decode.py

Winning (semi) Legit

I wanted to actually play the game! So I ran FreeSki.exe and it crashes. There are some local assets that are missing.

Missing Local Files
If you find an asset to use and place it in the directory it was looking, you can move on to the next error, then the next, until the game actually starts. All in all you need to create quite a few files for it to run. To make the game run, you need to create a directory called "img" in the same directory as the game. Then add the following images to it.
boulder.png
skier.png
skier_crash.png
skier_left.png
skier_left.zip
skier_pizza.png
skier_right.png
treasure.png
tree.png
victory.png
yeti.png
I just found some placeholder images, but if you want to use mine go ahead!
img.zip
Placeholder Sprites

One last file you need to replace is a font. Create a directory "fonts" in the same directory as the game and copy the fonts/VT323-Regular.ttf file in there. I found it after a quick Google search.

The game launches!

Game runs!
The sprites could use some resizing to make the collision 100% accurate but the game runs well enough to play. You can also look at the game source code to determine the elevation and horizontal offset of each treasure for your current mountain! See? Only semi-cheating.
Victory