Go to the retro store and help Goose Olivia ski down the mountain and collect all five treasure chests to reveal the hidden flag in this classic SkiFree-inspired challenge.
Olivia
This game looks simple enough, doesn't it? Almost too simple. But between you and me... it seems nearly impossible to win fair and square.
My advice? If you ain't cheatin', you ain't tryin'. wink
Now get out there and show that mountain who's boss!
We get an executable named FreeSki.exe while talking to Olivia.
Its a PyInstaller-compiled executable containing a SkiFree-inspired skiing game with hidden treasure chests and flag mechanics.
Extract – Unpack the PyInstaller executable to recover the embedded Python bytecode.
Reverse – Decompile the game logic, identify deterministic treasure generation, and locate the unused flag-decoding routine.
Decode – Recreate treasure values, reseed the RNG, XOR-decode the encoded flag, and recover the plaintext.
%%{init: {"themeVariables": {
"fontSize": "25px",
"nodeTextSize": "18px",
"clusterTextSize": "22px"
}}}%%
flowchart TD
subgraph Row1["Extract"]
direction LR
A[FreeSki executable]
B[PyInstaller extraction]
C[Recovered pyc files]
A --> B --> C
end
subgraph Row2["Reverse"]
direction LR
D[Decompile game logic]
E[Identify treasure generation]
F[Locate flag decode routine]
D --> E --> F
end
subgraph Row3["Decode"]
direction LR
G[Recreate treasure values]
H[Seed RNG deterministically]
I[XOR decode flag]
J[Recover plaintext flag]
G --> H --> I --> J
end
Row1 --> Row2
Row2 --> Row3
# Decompiled with PyLingual (https://pylingual.io)# Internal filename: FreeSki.py# Bytecode version: 3.13.0rc3 (3571)# Source timestamp: 1970-01-01 00:00:00 UTC (0)globalflag_text_surfaceglobalgrace_periodglobalMountainsglobalmountain_widthimportpygameimportenumimportrandomimportbinasciipygame.init()pygame.font.init()screen_width=800screen_height=600framerate_fps=60object_horizonal_hitbox=1.5object_vertical_hitbox=0.5max_speed=0.4accelerate_increment=0.02decelerate_increment=0.05scale_factor=0.1pixels_per_meter=30skier_vertical_pixel_location=100mountain_width=1000obstacle_draw_distance=23skier_start=5grace_period=10screen=pygame.display.set_mode((screen_width,screen_height))clock=pygame.time.Clock()dt=0pygame.key.set_repeat(500,100)pygame.display.set_caption('FreeSki v0.0')skierimage=pygame.transform.scale_by(pygame.image.load('img/skier.png'),scale_factor)skier_leftimage=pygame.transform.scale_by(pygame.image.load('img/skier_left.png'),scale_factor)skier_rightimage=pygame.transform.scale_by(pygame.image.load('img/skier_right.png'),scale_factor)skier_crashimage=pygame.transform.scale_by(pygame.image.load('img/skier_crash.png'),scale_factor)skier_pizzaimage=pygame.transform.scale_by(pygame.image.load('img/skier_pizza.png'),scale_factor)treeimage=pygame.transform.scale_by(pygame.image.load('img/tree.png'),scale_factor)yetiimage=pygame.transform.scale_by(pygame.image.load('img/yeti.png'),scale_factor)treasureimage=pygame.transform.scale_by(pygame.image.load('img/treasure.png'),scale_factor)boulderimage=pygame.transform.scale_by(pygame.image.load('img/boulder.png'),scale_factor)victoryimage=pygame.transform.scale_by(pygame.image.load('img/victory.png'),0.7)gamefont=pygame.font.Font('fonts/VT323-Regular.ttf',24)text_surface1=gamefont.render('Use arrow keys to ski and find the 5 treasures!',False,pygame.Color('blue'))text_surface2=gamefont.render(" find all the lost bears. don't drill into a rock. Win game.",False,pygame.Color('yellow'))flagfont=pygame.font.Font('fonts/VT323-Regular.ttf',32)flag_text_surface=flagfont.render('replace me',False,pygame.Color('saddle brown'))flag_message_text_surface1=flagfont.render('You win! Drill Baby is reunited with',False,pygame.Color('yellow'))flag_message_text_surface2=flagfont.render('all its bears. Welcome to Flare-On 12.',False,pygame.Color('yellow'))classSkierStates(enum.Enum):CRUISING=enum.auto()ACCELERATING=enum.auto()DECELERATING=enum.auto()TURNING_LEFT=enum.auto()TURNING_RIGHT=enum.auto()CRASHED=enum.auto()SkierStateImages={SkierStates.CRUISING:skierimage,SkierStates.ACCELERATING:skierimage,SkierStates.DECELERATING:skier_pizzaimage,SkierStates.TURNING_LEFT:skier_leftimage,SkierStates.TURNING_RIGHT:skier_rightimage,SkierStates.CRASHED:skier_crashimage}classSkier:def__init__(self,x,y):"""X and Y denote the pixel coordinates of the bottom center of the skier image"""self.state=SkierStates.CRUISINGself.elevation=0.0self.horizonal_location=0.0self.speed=0.0self.x=xself.y=yimagerect=skierimage.get_rect()self.rect=pygame.Rect(self.x-imagerect.left/2,self.y-imagerect.height,0,0)defDraw(self,surface):surface.blit(SkierStateImages[self.state],self.rect)defTurnLeft(self):self.StateChange(SkierStates.TURNING_LEFT)defTurnRight(self):self.StateChange(SkierStates.TURNING_RIGHT)defSlowDown(self):self.speed-=decelerate_incrementifself.speed<0.0:self.speed=0.0self.StateChange(SkierStates.DECELERATING)defSpeedUp(self):self.speed+=accelerate_incrementifself.speed>max_speed:self.speed=max_speedself.StateChange(SkierStates.ACCELERATING)defCruise(self):self.StateChange(SkierStates.CRUISING)defStateChange(self,newstate):ifself.state!=SkierStates.CRASHED:self.state=newstatereturnNonedefUpdateLocation(self):"""update elevation and horizonal location based on one frame of the current speed and turning status speed will be split between down and to the turning side with simplified math to avoid calculating square roots"""ifself.state==SkierStates.TURNING_LEFT:self.elevation-=self.speed*0.7self.horizonal_location-=self.speed*0.7ifself.elevation<0:self.elevation=0returnNonedefisMoving(self):ifself.speed!=0:passreturnTruedefCrash(self):self.StateChange(SkierStates.CRASHED)self.speed=0.0defReset(self):self.state=SkierStates.CRUISINGself.speed=0.0self.elevation=0.0self.horizonal_location=0.0defisReadyForReset(self):ifnotself.state==SkierStates.CRASHEDandself.elevation==0.0:passreturnTrueclassObstacles(enum.Enum):BOULDER=enum.auto()TREE=enum.auto()YETI=enum.auto()TREASURE=enum.auto()ObstacleImages={Obstacles.BOULDER:boulderimage,Obstacles.TREE:treeimage,Obstacles.YETI:yetiimage,Obstacles.TREASURE:treasureimage}ObstacleProbabilities={Obstacles.BOULDER:0.005,Obstacles.TREE:0.01,Obstacles.YETI:0.005}fakeObstacleProbabilities={Obstacles.BOULDER:0.1,Obstacles.TREE:0.1,Obstacles.YETI:0.1}defCalculateObstacleProbabilityRanges(probabilities):remaining=1.0last_end=0.0range_dict={}forkeyinprobabilities.keys():new_last_end=last_end+probabilities[key]range_dict[key]=(last_end,new_last_end)last_end=new_last_endreturnrange_dictObstacleProbabilitiesRanges=CalculateObstacleProbabilityRanges(ObstacleProbabilities)classMountain:def__init__(self,name,height,treeline,yetiline,encoded_flag):self.name=nameself.height=heightself.treeline=treelineself.yetiline=yetilineself.encoded_flag=encoded_flagself.treasures=self.GetTreasureLocations()defGetObstacles(self,elevation):obstacles=[None]*mountain_widthifelevation>self.height-grace_period:returnobstaclesdefGetTreasureLocations(self):locations={}random.seed(binascii.crc32(self.name.encode('utf-8')))prev_height=self.heightprev_horiz=0foriinrange(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_deltaprev_height=prev_height-e_deltaprev_horiz=prev_horiz+h_deltareturnlocationsMountains=[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')]classObstacleSet(list):def__init__(self,mountain,top,max_distance):super().__init__([])self.mountain=mountainself.top=Noneself.max=max_distanceself.Update(top)defUpdate(self,newtop):ifself.topandnewtop>=self.top:passreturnNonedefCollisionDetect(self,skier):forrowinself:ifrow[0]>skier.elevation:continueifrow[0]<skier.elevation-object_vertical_hitbox:passreturnNonedefSetFlag(mountain,treasure_list):globalflag_text_surfaceproduct=0fortreasure_valintreasure_list:product=product<<8^treasure_valrandom.seed(product)decoded=[]foriinrange(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'))defmain():victory_mode=Falserunning=Truereset_mode=Trueifrunning:screen.fill(pygame.Color('white'))ifreset_mode:player_started=Falsetreasures_collected=[]skier=Skier(screen_width/2,skier_vertical_pixel_location)mnt=random.choice(Mountains)skier.elevation=mnt.height-skier_startobstacles=ObstacleSet(mnt,mnt.height-skier_start,obstacle_draw_distance)reset_mode=Falseforeventinpygame.event.get():running=Falseifevent.type==pygame.QUITelseFalseifevent.type==pygame.KEYDOWN:ifskier.isReadyForReset():reset_mode=Truebreakelifevent.type==pygame.KEYUP:passelse:skier.Cruise()ifvictory_mode:screen.blit(victoryimage,(42,42))x=screen_width/2-flag_text_surface.get_width()/2y=screen_height/2-flag_text_surface.get_height()/2+40screen.blit(flag_text_surface,(x,y))pygame.display.flip()dt=clock.tick(framerate_fps)/1000pygame.quit()if__name__=='__main__':main()
There is this interesting function named SetFlag which is never called.
It uses the treasure list to generate a unique seed (line 4, 5 and 6), and that seed then drives the deterministic random number generation needed to encode the data (lines 8-10).
Since the seed used in the random function to encode is deterministic from the teasure list, we can use the similar code to decode to get the original ASCII value if there is one for each mountain.
So, now we write a different program by taking the key parts of the original program.
The change here is instead using the init function, It takes the height as parameter[line 18-33].
This gives the dictionary of location of mountain with key being the elevation row and the value being the horizontal offset.
e.g.
This iterates through mountain list, calls the GetTreasureLocation() to get the location dictionary.
but since SetFlag is operating on integer, not a dictionary line like below :
for treasure_val in treasure_list:
product = product << 8 ^ treasure_val
random.seed(product)
so, we need to "flatten" the location to a single value using the below pseudocode
importrandomimportbinascii# ConstantsMOUNTAIN_WIDTH=1000# Data: Mountain names, heights, and their encoded flagsMOUNTAINS=[("Mount Snow",3586,b'\x90\x00\x1d\xbc\x17b\xed6S"\xb0<Y\xd6\xce\x169\xae\xe9|\xe2Gs\xb7\xfdy\xcf5\x98'),("Aspen",11211,b'U\xd7%x\xbfvj!\xfe\x9d\xb9\xc2\xd1k\x02y\x17\x9dK\x98\xf1\x92\x0f!\xf1\\\xa0\x1b\x0f'),("Whistler",7156,b'\x1cN\x13\x1a\x97\xd4\xb2!\xf9\xf6\xd4#\xee\xebh\xecs.\x08M!hr9?\xde\x0c\x86\x02'),("Mount Baker",10781,b'\xac\xf9#\xf4T\xf1%h\xbe3FI+h\r\x01V\xee\xc2C\x13\xf3\x97ef\xac\xe3z\x96'),("Mount Norquay",6998,b'\x0c\x1c\xad!\xc6,\xec0\x0b+"\x9f@.\xc8\x13\xadb\x86\xea{\xfeS\xe0S\x85\x90\x03q'),("Mount Erciyes",12848,b'n\xad\xb4l^I\xdb\xe1\xd0\x7f\x92\x92\x96\x1bq\xca`PvWg\x85\xb21^\x93F\x1a\xee'),("Dragonmount",16282,b'Z\xf9\xdf\x7f_\x02\xd8\x89\x12\xd2\x11p\xb6\x96\x19\x05x))v\xc3\xecv\xf4\xe2\\\x9a\xbe\xb5'),]defGetTreasureLocations(name,height):""" Recreates the treasure locations using the mountain's name and height. """random.seed(binascii.crc32(name.encode('utf-8')))locations={}prev_height=heightprev_horiz=0foriinrange(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_deltaprev_height=prev_height-e_deltaprev_horiz=prev_horiz+h_deltareturnlocationsdefDecodeFlag(encoded,treasure_list):""" Decodes the flag by seeding the random generator with the treasure list and XORing the encoded flag. """product=0fortintreasure_list:product=(product<<8)^trandom.seed(product)decoded=[]foriinrange(0,len(encoded)):r=random.randint(0,255)decoded.append(chr(encoded[i]^r))returndecodeddefmain():print("\n=== FreeSki Flag Decoder ===\n")best_overall=Noneforname,height,encoded_flaginMOUNTAINS:print(f"[*] Processing mountain: {name}")# Recreate the treasure locations for this mountainlocs=GetTreasureLocations(name,height)# Convert the treasure locations into integer valuestreasure_values=[row*MOUNTAIN_WIDTH+horizforrow,horizinlocs.items()]# Decode the flag using the treasure valuesdecoded=DecodeFlag(encoded_flag,treasure_values)print(f" Decoded text: {''.join((decoded))}\n")if__name__=="__main__":main()
Upon running the above script, we get the ASCII value for only Mount snow.
frosty_yet_predictably_random
We submit that as the answer and that is accepted.
Looks like you found your own way down that mountain... and maybe took a few shortcuts along the way.
No judgment here—sometimes the clever path IS the right path. Now I'm one step closer to figuring out my own mystery. Thanks for the company, friend!