Flagvent 2025 - Easy
FV25.01
Challenge
Welcome to Flagvent |
|
| Categories: |
|
| Level: | easy |
| Author: | Last Place |
| Attachments: |
📦 welcome.tar.gz
|
There’s an attachment of welcome.tar.gz, which contains a single large image, welcome.jpeg:
Solution
There are ornaments all over the tree that look like pieces of QRCodes! My first attempt was to use Gimp to cut each out one, and paste it as a layer into a new image. I’ll rotate it and use the perspective tool to try to get everything to line up. The best I could get on the red ones is:
It’s close, but it doesn’t scan. I have the markers in the right places, but it isn’t making a 29x29 grid. I gave up on that and moved to a Google Sheet, making each cell square and setting some conditional formatting rules. I’ll start by labeling each piece:
And then filling in the numbers that fill a square:
If I add a rule at the top that just turns everything that has any value black, this is a QR Code:
That scans to the flag. Originally I solved without the middle piece. The QRCode error correction is good enough that it’s not necessary.
Flag: FV25{Welcome-2-FlagVent}
FV25.02
Challenge
Santa wanted to modernize the sleigh’s startup system and impress the younger elves in the workshop. After scrolling through too many TikToks, he decided to rewrite the entire ignition script using all the latest slang - you know, to seem “skibidi” and “rizz up” his tech cred. The good news? He finished the script. The bad news? In his excitement to use words like “bussin” and “no cap,” Santa completely forgot the password he set. And now, with Christmas Eve approaching, none of the elves can figure out how to decode his brainrot creation. The sleigh won’t start, the reindeer are getting restless, and Rudolph’s nose is stress-flickering. Can you decode Santa’s script and recover the password before Christmas is ruined? The fate of the holiday rests in your hands! Sometimes the most “fr fr” solutions are hiding in plain sight, no cap. |
|
| Categories: |
|
| Level: | easy |
| Author: | 0xdf |
| Attachments: |
📦 lowkey-sleigh.tar.gz
|
The attachment has two files:
oxdf@hacky$ ls
lowkey_sleigh.gyat oracle
oxdf@hacky$ file *
lowkey_sleigh.gyat: ASCII text, with very long lines (6806)
oracle: ASCII text, with CRLF line terminators
oracle is the lyrics to a classic song.
Solution
Recover Python Code
The .gyat file looks like this:
glaze sys
vibe_check = [b",OU\x08\x1dL\n\x0cGHO\x02\t\x11D\x1cO\x19\x1d\x16\x0e\x1cyHR\x03\x0b\t\x04AO\x0b\x06\x15N\x18IB\x10\x1a^G\x00\nV\x02\x15\x15ZdUTRO\x00*EA*PnTONEOOEE\x00NATRnNRVATETNSn\x00N*y\x00OU\x00wDU\x00\x00DEEU\x00EALRnNYY\x0bbNEE\x00OSHNI\x00LK\x00*UO\x00\x00D\x00_O\x1aEAL\x1a\x1aH\x00\x00SEA\x00\x00\x00dY\x00AGTLN\x00MUT\x00\x00D\x00yOGEUTPYV\x00IAEN\x00V\x00NR\x00\x00GVLN\x00MUTORE\x00TNROG\x00\x00U*!YJDE\x0b]REY\x0b\x0bGVEUUGE\x00N\x0c[[\x1a_XT\x1a[\rX_0\x1a[EN\x00VYT\x0c*EUE\x00\x07AKEMGG\x00AAULN\x00ANUE*\x00AE\x00\x00T\x00AHDN*i\x00VTN\x00nGGVAGUNN\x00AN\x00UC\x0bNN\x0b\x0bRFGD_D\\HB]!NAnBGV[]OT\\\x16T0OT]X[[\x1aSIHT\x00LOoCYV*NAnBNIyG\x00FEiOEDOHE\x00OMINRD\x00OOECYE*MNYU*UROYHSnDRN\x00\x0bN!DNBEYO\x0bDN ^\nL!_IDR*@A@KAHt\x1aHt\x1a\x1a_0U___\x1aL[KAOE~NLR\x00\x00E*OTNAOYn\x00Rn\x00\x00OAFAE\x07nHRTNOHDG\x00\x07\x00OE\x00ORNAOY*oNnOOUJNYVCKY]K[UXOO\nO\x0bDN\x0bDHMKFK]G\x0eI\x1aVHUNO\x1d\x1aCMO_\\OEDD*EHOO\x00FONEMNTD\x00NGSWOVUT\x00TWUERSHSVJwRU\x00\x00V\x07N\x00ENLHBNRSNAX[Z\x0eZJ\\\x0e\\Z\x0e\x0eEW$KH\x0b^\\!C\x0eK@\x0eFO@\\]@U\x1aNON\x1a_C\x1a[_iRY\x00\x00*EEU\x00*D\x00EN\x00HA\x00EAE\x00E\x00\x00NNHG\x00EAALYL\x00\x00EUU\x00*DKG*\x00W\x00V*GR@WMX@I\x0eI@K\x0eOIWB\x0e\x0e\\[Z@O\x1d!XO\x0eL\x0e\x0e$GKAK_LT]L]T_\x1a[]B\t\x07\x0cNmE\x00YA*AYUwO\x00*UE*EAVNGVGVTIW\x00NTD\x00OEUAHKTY\x00UNOA*UE*KOJ@\x0e@EZ$YK]YI]@\x0e\\@OIEOW[[@AOK\x06G$W\x0e\x0eItT\x1a\x1dXU\x1dtU]LNNRnkGKAYU\x00VTVRHA\x00YLEYIOEL\x00OOnCGVNNRnTYWA\x00SOD\x00NRN\x0cO@\x0eAWGA\\B\x0eAA`M\x0eA\tG\x0e$KAJKKgA\\Y@J\x1a\x0eAK\x1aACSU_G\x0bENN_NHO\x1aR*VnoOOEIRnNR\x00\x00OE\x00OTU\x00O\x00NVHTGT*A\x00WUDnEONEORnNR\x00\x00aV\x0eX[K\\]@@Y\x1aAI\x1aT_\x1a\x1aO_`AA@KA\x0e\x0eH\\AYO_\x0b\rK\x0bJNN\nA\x0bDXYT\x1a\x1a\x1aOE\x07OIeGUD\x00\x00GDURPSVGIREW\x00E\x00OSDN\x00UPNEINLE\x00LUT\x00R*GMPOX[G\x0eKM\x1a_@G[}\x1a_T\x1a\x1aM]_UTNOG\x0eJ\\@I]JULOS\x1a\x1dEXS[HT\x1aLOU\x1a\x1a[__UN\x00kN\x00HRNGSV*K\t\x00HU\x00*AAA\x00DT*Y\x00EAUE*N\x00AN\x00HLEOIEC@@OKF\x0e\x1aCMV_YT'\x1d\r_[O_0\x1a\x1a\x1a@C\x1a]GOA[[]]S[R\x1aZRNQ[^C0\x1aUuN]s0LCNwD\x00LE\x00O\x00DOGVAG\x00GYELAAVEVOUOHCT\x07*RRNYLE*OEDEIXOI\x0eAWC[MSIH\x1a\x1a]_YBBC0^_XDB_0UKJK$SWSUXH\x1aDJ\\LNHHTNN[BF0CHUYeU\x0cOEHEnIRE\x00E\x00nERSAGENNRTAOKEYMG\x00ON\x0cNNOOOU@K\x0eB\t`A\\`OIK@@\\HXI[AU\rM_JUIN\x1aT\x1a\t[WKUVVtUOeJZR\x12NLU_^U\x1a\\cNOM\x00~N\x00O\x00\x00UYEOLLOOIOMAWWEJNRO\x00HUgMQNMW\x03\x00O\x00\x00O\tN^BMAA[\x0e\x0eZ]\x0eOW\x0e\\t\rB[HC_C@Z\x0eBT\x1a}^[@K]ZN_C\x1a\x1dNF\x1d\x1aCOHt\x1aUL_M_UN\x07R*SNGH\x00N\x00ON\x00E\x00\x00SEN\x00\x00YUN\x08N\x00*\tF\x03V))T\x03K\x02IONO\x00EN\x0eK\x0e\x0e]K@\x0eKIKiAWZ\x0eSH0\x1dOELKX^IXTS\x0eK@\x0e]W\x1d\x1aTRL\x0bXXKMCNED{0\x1a[RBGVzGVNIiGOGY\x00VTGWEN\x00DMO\x00ENYA*\nBQADLR\x0eX\x1dB \nEWKE@$LKO@\x0eJOAKK@W[m\x1aW\x1a\\WOI[GAWH\x0eg`K\x0e[$^_0S\x1aJORF\x03\x03OJFA[\x1aQ\x00YRUkOYi\x00\x00nY\x00L*DE*R\x00W*S\x07GRNNR\x00*OB\x00ZQSO]RK\n\ndS\x0bB]IFI\x0eKAA`\x0eEKAJZK$AK\x0eA\\JOIWOB^G[BOWZDNF^\r_E\\HN)GQFZEO\nEQGV\x03s\x08N\x00OE\x00\x00OVATDT\x00UTROE*DREYOVOOiOR@@\x1d\n_CXMDHXOZKZ@[Z\\A\x0e$KO$^`ZA@KAAKK\x0e@OZ\\`@\\X[^O^DYd\nD0w\x1aEV\x03tGV\x03\x03GFFV\x03oALRnNRR\x00iENN\x00OSHNI\x00LK\x00*UO\x00\x00D\x00EU\x00EAL\n\nX\n\n^KO\x0e\x0e\x0e@W\x0eOIZB@\x0eC[Z\x0e\x0eJ\x0ewAIK[Z^WX\x0eGOKC\n\\\nDX\n\nM]VS\x0bF^_DYN\x0b_EYDG*\x00U**RAON\x00VYNR\x00\x00GVEUUGE\x00N\x0cAA\x00EBN\x00A\x00UE*\nKOD\x1aXWZ\x02\x0eK[K\x0e\tOEKCII\x0eOO[B@\x0eO@[K$\x0eOK\x0e\x0eY\nKBND b\x0b]_E\x0beLL]JL^EE\x0bJN*UH\x00EE\x00\x00YMLOTOWCIV*NAnBGVAGUNF\x0cN*UNGBAA\x00IYO@\x0eBAKMWX$@O`L@GwI\x0eHKgAKJAFK\x0eACG@\\O\nEENHRN!FER^!^YDRCXeOYE\x00*E*OEINRD\x00OE*T\x00G*TBOY*NONEOFn\x00Rn\x00\x00E*OEEE\x00VAAKR_N@V\\\x0e\x0eK$AZ@OAW`\x0e\\`\x0e\x0eAOHOK\t`FO_EDCOL\x0b\x0c\x0bDN\x0bDYEJDR!dEeDDUkEDLMEWSEUOEDD\x00E\x00OE\x00ORCEHESI\x00G\x00LROTU\x07\x00YWUEREONN\nOBRA\x0eHA@KC@ZJ\x0e@INJUX[Z\x0eZZ^OXXCX]A|Y^\x0b\x0b]\x0cE\x0bNEGCIEYXEDV\x7fT\x00TDR\x00RT\x00\x00KY*EE\x00UT)M\x03FM\x03KBMQPML\x03WVW\x03FZ\x03AEgXS\n\n\nOO_\r$J\x0eK@\x0eFO\x0eKOX\nO\n\nDDBM\x03FBKGRG\x0b\x0bN^^\x0b!O@L!\x0b\\\x0b]!LYERHVdG\x00GNE\x00AGYL\x00\x00RUTNU\x03)AB\x03A\x03\x03)JFLFFUMDUDMF\x03BDL\x03\x04\x06DMO\nSK![W[yA\x0e$[H OK\\DM\\M\\^JT\x03D_O\x0bDN^JC@_R\x0b^EDJ!^N!NJOE\x00dKT*WESWGSN\x00RNAGKAYUUEEJN\x03B!R\x0b\x0bXdE\x0b\x0bID\rmLDUNMQmKMAJS_\n\\^]OCK\nSFOSCEOF\nEEm@D\\EEYe_R\\J\x0bXDO\x0bEYE\x07DE\x0bDRBDRf\x00OOnC\x00O\x07I\x00*EODEEiORWE[\x0b\x0bDN\x0bDRBDYG\x0bEN_N^QV\x03X \\mFRDNTOeDX\n\nEO\nE^_\nE\nD\\B^MW)B\n\\^OeNDENDYeEY\x0b\x0bj]\x0b]^NYXENi\x00OS\x00NE\x00\x00UEnOONEO\x00\x00FRDYN_\x0b\x0cJ\x0bJNN\x0b@\x0bDXOE\n\x03\x03VO\rECDZ_G\x03\nZN_XZY\\MCXO]\nO\nEYND\x03VSDNBEGN\x0bG^_\x0bY!LF[D]^B\x0bN\\\x0bNNcAg\x00EN\x00\x00WGEONTAI\x00DRNGX[D]^B\x0b\x0bEXEJYE\x0b]^D\x0b\nBFFLD\nKD\nCOSMPU)H\n\x03KV\x03)BBB\x03GW)Z\x03FK^N!E\x0bJE\x0bCGNDBNHEEJNC\x0b\x0bR\\GEnY*\x00\x00EAUE*\x00\x00\x00NM\x00GIAOU^LLBJC\x0bLRNGJOR!\x0bDd_Lc)UZMtm\x03OF\x03L\x03GLDUBD\x03DZFOBBUFULVEBH_\x0c!YYERGN!DNONL]JL\x0bDFRJ\\BSx\x00\x00GETOOD)PQVZOE*OEDE!BFBDIN\x0cDJJJ_NNE__JD@!RX_SE_\x06EOBOdCXO\nO\ndOXYKMODDX_JD@NRFL\x0bDE\x07EEDDD^EN\x0bG\x0ceDYeJLEdNRRUGUOL\x03TFSLPW\x03N\x00\x07URNDGGeDYeJLC\x03_]DNOD\x0bMrEDF\x0b_E\x0bD\x0b\x0b^RNDGGDDBDFJ\\\\NAEYD\x0bC]oEYFE_\x0b\x0bD\x0b\x0bD\x02E[GHDD^\x0b\x0b_X\x00As\x00Qm\x03LUFMQNMW\x03OM\x03dGANEX__NR\x0b\x0bNF\x0b\x0bR^Ye\x0bD]N\\N^E\x0cY\x0bXELC\x0bE\x0bDE\x0bN\x0b\x0bXNE\x0b\x0bR^E\x03E\x0b!\x02N\x0b^!!\\\x0b]\x07LJED\x0bNE\x0bN\x0b\x0bXNE\x0bEGEMLZW\x03JE*\x04BHAFUZDUMJ\nEN\x00XR\x0c\x0bEDL\x0bNNJECNERj!\x0bJYIL][L]EBbLDLR\x0b]_L\\NE\x0bOFD\x0bNERJ!\x0bJYILDC\x0b]\x0bC!\x0bN\\N@E!INJE\x00DAOoFMZBw\x00N\x03QZBDVIOZE\x03jnE\x00U*ON!E\x0bOJDN\x0b\x0bGBNGJ\x0b@\x0bRY^JDRb\x0b\x0beR\x0bG!ON!Y\x0b\\!X\x0cLYEEY\x0b!DI\x0bRYBJLRJ\x0b\x0beR\x0bGXLCL\x0bEOOn\x00KoLGWF*LF\x03LQDAGYBOSJVLAYTOELT\x07^CYMN!OYNSEO\nDYO_\nS\x02E\x0bDN\x0b\x0bD]J_O_\x0b^_YDN!OYNRD]DDbEXDD\x0b\x0b^CXMEN]JTETNUTRO\x00*oB)SmTOMFLLFF\x03MBWQmNRVATETNSn\x00N*y\x00OU\x00tGV\x03\x00DEFV\x03FBLRnNRR\x00iENN\x00OSHNI\x00LK\x00*UO\x00\x00D\x00FV\x03FKL\x00\nQ\x03\x03PEA\x00\x00\x00NY\x00AGTLd\x00NVW\x03\x03D\x03zLDFVWSZV\x00IAEN\x00V\x00NR\x00\x00GVLN\x00MUWLQF\x00TNQLD\x03\x00U**RAON\x00VYNR\x00\x00GVEUUGE\x00N\x0cAA\x00EBM\x03B\x03UE*\x03BFM\x00VYT\x0c\x00EUE\x00\x07AaEMDD\x03BBVOM\x03BMVF)\x03BF\x03\x03W\x03BKGM)j\x03UWM\x03mDDUBDVMM\x03BM\x03VK\x03FF\x03\x03ZNOLWLT@JU)MBmADUBDVME\x0fM)VMDABB\x03JPQM\x03OLF@ZU)MBDBNIyD\x03EFjLFGLKF\x03LNJMQG\x03LLF@ZF)NMZV)VQLZKPmGQM\x03\x03F)LFJMQG\x03LF)W\x03D)WALZ)MLMFLEm\x03Qm\x03\x03F)LFFF\x03UBHBLFWMOQ\x03\x03F\x00e0\x0b\x05\x06\x1a/T\x17*\x00T\x00A*A\x04I*H?Zd,\x00\x01\x04K\x07O\x1a\x11\x00?\x0b)\x18\x0e\r*.\x1an\x07\x1b\x01\x11\x16^CB\x12\x00\x04K\x05\x16\x02\x1d\x05TKO\x1d\x02\x0fe", b'\x19\x06\x16\x1fTJ\x10H\x05\x0f\x11bD\x06\r\x18\x1b', b'\x02zLGJn\x10LLY]9\x05PN\x1d;\x1dL\rZh6FA\x12\x05XR0\x0c\x11\x1e\x1c\x0f', b'\x01U\x0c\x17\x17']
skibidi TruthWell:
bop __init__(unc):
unc.flow = unc.stream()
bop stream(unc):
sauce = fetch()
cap = len(sauce)
pulse = 223
let him cook Aura:
pulse = 0x1337 + pulse - 223
let him cook pulse >= cap:
pulse = pulse - cap
pause ord(sauce[pulse])
bop clap(unc, ohio, frfr=Cooked):
drip = ''.join(chr(w ^ y) mewing w, y diddy zip(ohio, unc.flow))
chat is this real frfr:
its giving drip.encode()
its giving drip
bop snap(unc, ohio, toilet):
its giving bytes(x ^ y mewing x, y diddy zip(ohio, toilet))
bop fetch():
GOAT mmwt
GOAT d2s
hawk:
pookie mog(sys.argv[1]) ahh scroll:
its giving scroll.read()
tuah:
mmwt = f"nah bruh, usage: lowkey_sleigh.gyat <truth scroll> <password>"
d2s = "ight imma head out"
bop strat():
GOAT mmwt
GOAT d2s
GOAT sync_off
vibe = TruthWell()
mmwt = vibe.clap(vibe_check[1])
d2s = vibe.clap(vibe_check[3])
sync_off = vibe.clap(vibe_check[0])
its giving vibe
bop reveal_the_drip(clap):
opener = clap.clap(b'Yooo Merry Skibidimas bestie, may your vibe be immaculate and your presents be absolutely goated pookie the sauce', frfr=Aura)
lock = clap.clap(sys.argv[2].encode(), frfr=Aura)
chat is this real clap.snap(opener, lock) != vibe_check[2]:
crashout
yap(sync_off)
chat is this real __name__ == "__main__":
hawk:
reveal_the_drip(strat())
tuah:
yap(mmwt)
spit on that thang:
yap(d2s)
It looks kind of like Python. __name__ == "__main__" is a classic Python pattern. Still, the keywords are all brainrot terms. Searching for python gyat, the first result is pygyat:
The example on the readme looks just like this code:
I’ll install this package with uv tool install pygyat (uv cheatsheet):
oxdf@hacky$ uv tool install pygyat
Resolved 1 package in 50ms
Installed 1 package in 3ms
+ pygyat==1.0.8
Installed 3 executables: gyat2py, py2gyat, pygyat
I can run the script with pygyat:
oxdf@hacky$ pygyat lowkey_sleigh.gyat
nah bruh, usage: lowkey_sleigh.gyat <truth scroll> <password>
ight imma head out
It shows the usage, which shows it takes a “truth scroll” and a “password”. If I give it the file that came with and a password, the output changes:
oxdf@hacky$ pygyat lowkey_sleigh.gyat oracle password
miss me with that
duces
gyat2py generates a Python script from the .gyat file:
oxdf@hacky$ gyat2py lowkey_sleigh.gyat
oxdf@hacky$ file lowkey_sleigh.py
lowkey_sleigh.py: Python script, ASCII text executable, with very long lines (6806)
Python Reverse Engineering
The structure of the Python is as follows:
import sys
vibe_check = [b",OU\x08\x1dL\n\x0cGHO\x02\t\x11D\x1cO\x19\x1d\x16\x0e\x1cyHR\x03\x0b\t\x04AO\x0b\x06\x15N\x18IB\x10\x1a^G\x00\nV\x02\x15\x15ZdUTRO\x00*EA*PnTONEOOEE\x00NATRnNRVATETNSn\x00N*y\x00OU\x00wDU\x00\x00DEEU\x00EALRnNYY\x0bbNEE\x00OSHNI\x00LK\x00*UO\x00\x00D\x00_O\x1aEAL\x1a\x1aH\x00\x00SEA\x00\x00\x00dY\x00AGTLN\x00MUT\x00\x00D\x00yOGEUTPYV\x00IAEN\x00V\x00NR\x00\x00GVLN\x00MUTORE\x00TNROG\x00\x00U*!YJDE\x0b]REY\x0b\x0bGVEUUGE\x00N\x0c[[\x1a_XT\x1a[\rX_0\x1a[EN\x00VYT\x0c*EUE\x00\x07AKEMGG\x00AAULN\x00ANUE*\x00AE\x00\x00T\x00AHDN*i\x00VTN\x00nGGVAGUNN\x00AN\x00UC\x0bNN\x0b\x0bRFGD_D\\HB]!NAnBGV[]OT\\\x16T0OT]X[[\x1aSIHT\x00LOoCYV*NAnBNIyG\x00FEiOEDOHE\x00OMINRD\x00OOECYE*MNYU*UROYHSnDRN\x00\x0bN!DNBEYO\x0bDN ^\nL!_IDR*@A@KAHt\x1aHt\x1a\x1a_0U___\x1aL[KAOE~NLR\x00\x00E*OTNAOYn\x00Rn\x00\x00OAFAE\x07nHRTNOHDG\x00\x07\x00OE\x00ORNAOY*oNnOOUJNYVCKY]K[UXOO\nO\x0bDN\x0bDHMKFK]G\x0eI\x1aVHUNO\x1d\x1aCMO_\\OEDD*EHOO\x00FONEMNTD\x00NGSWOVUT\x00TWUERSHSVJwRU\x00\x00V\x07N\x00ENLHBNRSNAX[Z\x0eZJ\\\x0e\\Z\x0e\x0eEW$KH\x0b^\\!C\x0eK@\x0eFO@\\]@U\x1aNON\x1a_C\x1a[_iRY\x00\x00*EEU\x00*D\x00EN\x00HA\x00EAE\x00E\x00\x00NNHG\x00EAALYL\x00\x00EUU\x00*DKG*\x00W\x00V*GR@WMX@I\x0eI@K\x0eOIWB\x0e\x0e\\[Z@O\x1d!XO\x0eL\x0e\x0e$GKAK_LT]L]T_\x1a[]B\t\x07\x0cNmE\x00YA*AYUwO\x00*UE*EAVNGVGVTIW\x00NTD\x00OEUAHKTY\x00UNOA*UE*KOJ@\x0e@EZ$YK]YI]@\x0e\\@OIEOW[[@AOK\x06G$W\x0e\x0eItT\x1a\x1dXU\x1dtU]LNNRnkGKAYU\x00VTVRHA\x00YLEYIOEL\x00OOnCGVNNRnTYWA\x00SOD\x00NRN\x0cO@\x0eAWGA\\B\x0eAA`M\x0eA\tG\x0e$KAJKKgA\\Y@J\x1a\x0eAK\x1aACSU_G\x0bENN_NHO\x1aR*VnoOOEIRnNR\x00\x00OE\x00OTU\x00O\x00NVHTGT*A\x00WUDnEONEORnNR\x00\x00aV\x0eX[K\\]@@Y\x1aAI\x1aT_\x1a\x1aO_`AA@KA\x0e\x0eH\\AYO_\x0b\rK\x0bJNN\nA\x0bDXYT\x1a\x1a\x1aOE\x07OIeGUD\x00\x00GDURPSVGIREW\x00E\x00OSDN\x00UPNEINLE\x00LUT\x00R*GMPOX[G\x0eKM\x1a_@G[}\x1a_T\x1a\x1aM]_UTNOG\x0eJ\\@I]JULOS\x1a\x1dEXS[HT\x1aLOU\x1a\x1a[__UN\x00kN\x00HRNGSV*K\t\x00HU\x00*AAA\x00DT*Y\x00EAUE*N\x00AN\x00HLEOIEC@@OKF\x0e\x1aCMV_YT'\x1d\r_[O_0\x1a\x1a\x1a@C\x1a]GOA[[]]S[R\x1aZRNQ[^C0\x1aUuN]s0LCNwD\x00LE\x00O\x00DOGVAG\x00GYELAAVEVOUOHCT\x07*RRNYLE*OEDEIXOI\x0eAWC[MSIH\x1a\x1a]_YBBC0^_XDB_0UKJK$SWSUXH\x1aDJ\\LNHHTNN[BF0CHUYeU\x0cOEHEnIRE\x00E\x00nERSAGENNRTAOKEYMG\x00ON\x0cNNOOOU@K\x0eB\t`A\\`OIK@@\\HXI[AU\rM_JUIN\x1aT\x1a\t[WKUVVtUOeJZR\x12NLU_^U\x1a\\cNOM\x00~N\x00O\x00\x00UYEOLLOOIOMAWWEJNRO\x00HUgMQNMW\x03\x00O\x00\x00O\tN^BMAA[\x0e\x0eZ]\x0eOW\x0e\\t\rB[HC_C@Z\x0eBT\x1a}^[@K]ZN_C\x1a\x1dNF\x1d\x1aCOHt\x1aUL_M_UN\x07R*SNGH\x00N\x00ON\x00E\x00\x00SEN\x00\x00YUN\x08N\x00*\tF\x03V))T\x03K\x02IONO\x00EN\x0eK\x0e\x0e]K@\x0eKIKiAWZ\x0eSH0\x1dOELKX^IXTS\x0eK@\x0e]W\x1d\x1aTRL\x0bXXKMCNED{0\x1a[RBGVzGVNIiGOGY\x00VTGWEN\x00DMO\x00ENYA*\nBQADLR\x0eX\x1dB \nEWKE@$LKO@\x0eJOAKK@W[m\x1aW\x1a\\WOI[GAWH\x0eg`K\x0e[$^_0S\x1aJORF\x03\x03OJFA[\x1aQ\x00YRUkOYi\x00\x00nY\x00L*DE*R\x00W*S\x07GRNNR\x00*OB\x00ZQSO]RK\n\ndS\x0bB]IFI\x0eKAA`\x0eEKAJZK$AK\x0eA\\JOIWOB^G[BOWZDNF^\r_E\\HN)GQFZEO\nEQGV\x03s\x08N\x00OE\x00\x00OVATDT\x00UTROE*DREYOVOOiOR@@\x1d\n_CXMDHXOZKZ@[Z\\A\x0e$KO$^`ZA@KAAKK\x0e@OZ\\`@\\X[^O^DYd\nD0w\x1aEV\x03tGV\x03\x03GFFV\x03oALRnNRR\x00iENN\x00OSHNI\x00LK\x00*UO\x00\x00D\x00EU\x00EAL\n\nX\n\n^KO\x0e\x0e\x0e@W\x0eOIZB@\x0eC[Z\x0e\x0eJ\x0ewAIK[Z^WX\x0eGOKC\n\\\nDX\n\nM]VS\x0bF^_DYN\x0b_EYDG*\x00U**RAON\x00VYNR\x00\x00GVEUUGE\x00N\x0cAA\x00EBN\x00A\x00UE*\nKOD\x1aXWZ\x02\x0eK[K\x0e\tOEKCII\x0eOO[B@\x0eO@[K$\x0eOK\x0e\x0eY\nKBND b\x0b]_E\x0beLL]JL^EE\x0bJN*UH\x00EE\x00\x00YMLOTOWCIV*NAnBGVAGUNF\x0cN*UNGBAA\x00IYO@\x0eBAKMWX$@O`L@GwI\x0eHKgAKJAFK\x0eACG@\\O\nEENHRN!FER^!^YDRCXeOYE\x00*E*OEINRD\x00OE*T\x00G*TBOY*NONEOFn\x00Rn\x00\x00E*OEEE\x00VAAKR_N@V\\\x0e\x0eK$AZ@OAW`\x0e\\`\x0e\x0eAOHOK\t`FO_EDCOL\x0b\x0c\x0bDN\x0bDYEJDR!dEeDDUkEDLMEWSEUOEDD\x00E\x00OE\x00ORCEHESI\x00G\x00LROTU\x07\x00YWUEREONN\nOBRA\x0eHA@KC@ZJ\x0e@INJUX[Z\x0eZZ^OXXCX]A|Y^\x0b\x0b]\x0cE\x0bNEGCIEYXEDV\x7fT\x00TDR\x00RT\x00\x00KY*EE\x00UT)M\x03FM\x03KBMQPML\x03WVW\x03FZ\x03AEgXS\n\n\nOO_\r$J\x0eK@\x0eFO\x0eKOX\nO\n\nDDBM\x03FBKGRG\x0b\x0bN^^\x0b!O@L!\x0b\\\x0b]!LYERHVdG\x00GNE\x00AGYL\x00\x00RUTNU\x03)AB\x03A\x03\x03)JFLFFUMDUDMF\x03BDL\x03\x04\x06DMO\nSK![W[yA\x0e$[H OK\\DM\\M\\^JT\x03D_O\x0bDN^JC@_R\x0b^EDJ!^N!NJOE\x00dKT*WESWGSN\x00RNAGKAYUUEEJN\x03B!R\x0b\x0bXdE\x0b\x0bID\rmLDUNMQmKMAJS_\n\\^]OCK\nSFOSCEOF\nEEm@D\\EEYe_R\\J\x0bXDO\x0bEYE\x07DE\x0bDRBDRf\x00OOnC\x00O\x07I\x00*EODEEiORWE[\x0b\x0bDN\x0bDRBDYG\x0bEN_N^QV\x03X \\mFRDNTOeDX\n\nEO\nE^_\nE\nD\\B^MW)B\n\\^OeNDENDYeEY\x0b\x0bj]\x0b]^NYXENi\x00OS\x00NE\x00\x00UEnOONEO\x00\x00FRDYN_\x0b\x0cJ\x0bJNN\x0b@\x0bDXOE\n\x03\x03VO\rECDZ_G\x03\nZN_XZY\\MCXO]\nO\nEYND\x03VSDNBEGN\x0bG^_\x0bY!LF[D]^B\x0bN\\\x0bNNcAg\x00EN\x00\x00WGEONTAI\x00DRNGX[D]^B\x0b\x0bEXEJYE\x0b]^D\x0b\nBFFLD\nKD\nCOSMPU)H\n\x03KV\x03)BBB\x03GW)Z\x03FK^N!E\x0bJE\x0bCGNDBNHEEJNC\x0b\x0bR\\GEnY*\x00\x00EAUE*\x00\x00\x00NM\x00GIAOU^LLBJC\x0bLRNGJOR!\x0bDd_Lc)UZMtm\x03OF\x03L\x03GLDUBD\x03DZFOBBUFULVEBH_\x0c!YYERGN!DNONL]JL\x0bDFRJ\\BSx\x00\x00GETOOD)PQVZOE*OEDE!BFBDIN\x0cDJJJ_NNE__JD@!RX_SE_\x06EOBOdCXO\nO\ndOXYKMODDX_JD@NRFL\x0bDE\x07EEDDD^EN\x0bG\x0ceDYeJLEdNRRUGUOL\x03TFSLPW\x03N\x00\x07URNDGGeDYeJLC\x03_]DNOD\x0bMrEDF\x0b_E\x0bD\x0b\x0b^RNDGGDDBDFJ\\\\NAEYD\x0bC]oEYFE_\x0b\x0bD\x0b\x0bD\x02E[GHDD^\x0b\x0b_X\x00As\x00Qm\x03LUFMQNMW\x03OM\x03dGANEX__NR\x0b\x0bNF\x0b\x0bR^Ye\x0bD]N\\N^E\x0cY\x0bXELC\x0bE\x0bDE\x0bN\x0b\x0bXNE\x0b\x0bR^E\x03E\x0b!\x02N\x0b^!!\\\x0b]\x07LJED\x0bNE\x0bN\x0b\x0bXNE\x0bEGEMLZW\x03JE*\x04BHAFUZDUMJ\nEN\x00XR\x0c\x0bEDL\x0bNNJECNERj!\x0bJYIL][L]EBbLDLR\x0b]_L\\NE\x0bOFD\x0bNERJ!\x0bJYILDC\x0b]\x0bC!\x0bN\\N@E!INJE\x00DAOoFMZBw\x00N\x03QZBDVIOZE\x03jnE\x00U*ON!E\x0bOJDN\x0b\x0bGBNGJ\x0b@\x0bRY^JDRb\x0b\x0beR\x0bG!ON!Y\x0b\\!X\x0cLYEEY\x0b!DI\x0bRYBJLRJ\x0b\x0beR\x0bGXLCL\x0bEOOn\x00KoLGWF*LF\x03LQDAGYBOSJVLAYTOELT\x07^CYMN!OYNSEO\nDYO_\nS\x02E\x0bDN\x0b\x0bD]J_O_\x0b^_YDN!OYNRD]DDbEXDD\x0b\x0b^CXMEN]JTETNUTRO\x00*oB)SmTOMFLLFF\x03MBWQmNRVATETNSn\x00N*y\x00OU\x00tGV\x03\x00DEFV\x03FBLRnNRR\x00iENN\x00OSHNI\x00LK\x00*UO\x00\x00D\x00FV\x03FKL\x00\nQ\x03\x03PEA\x00\x00\x00NY\x00AGTLd\x00NVW\x03\x03D\x03zLDFVWSZV\x00IAEN\x00V\x00NR\x00\x00GVLN\x00MUWLQF\x00TNQLD\x03\x00U**RAON\x00VYNR\x00\x00GVEUUGE\x00N\x0cAA\x00EBM\x03B\x03UE*\x03BFM\x00VYT\x0c\x00EUE\x00\x07AaEMDD\x03BBVOM\x03BMVF)\x03BF\x03\x03W\x03BKGM)j\x03UWM\x03mDDUBDVMM\x03BM\x03VK\x03FF\x03\x03ZNOLWLT@JU)MBmADUBDVME\x0fM)VMDABB\x03JPQM\x03OLF@ZU)MBDBNIyD\x03EFjLFGLKF\x03LNJMQG\x03LLF@ZF)NMZV)VQLZKPmGQM\x03\x03F)LFJMQG\x03LF)W\x03D)WALZ)MLMFLEm\x03Qm\x03\x03F)LFFF\x03UBHBLFWMOQ\x03\x03F\x00e0\x0b\x05\x06\x1a/T\x17*\x00T\x00A*A\x04I*H?Zd,\x00\x01\x04K\x07O\x1a\x11\x00?\x0b)\x18\x0e\r*.\x1an\x07\x1b\x01\x11\x16^CB\x12\x00\x04K\x05\x16\x02\x1d\x05TKO\x1d\x02\x0fe", b'\x19\x06\x16\x1fTJ\x10H\x05\x0f\x11bD\x06\r\x18\x1b', b'\x02zLGJn\x10LLY]9\x05PN\x1d;\x1dL\rZh6FA\x12\x05XR0\x0c\x11\x1e\x1c\x0f', b'\x01U\x0c\x17\x17']
class TruthWell:
def __init__(self):
self.flow = self.stream()
def stream(self):
...[snip]...
def clap(self, ohio, frfr=False):
...[snip]...
def snap(self, ohio, toilet):
return bytes(x ^ y for x, y in zip(ohio, toilet))
def fetch():
...[snip]...
def strat():
...[snip]...
def reveal_the_drip(clap):
...[snip]...
if __name__ == "__main__":
try:
reveal_the_drip(strat())
except:
print(mmwt)
finally:
print(d2s)
vibe_check is a list at the top of four binary strings. TruthWell is a class with four methods. fetch, strat, and reveal_the_drip are functions. And at the bottom, if __name__ is __main__, the it calls reveal_the_drip(strat()). On any exception, it prints mmwt, and then always prints d2s.
Those two variables are defined in strat, which does an initialization:
def strat():
global mmwt
global d2s
global sync_off
vibe = TruthWell()
mmwt = vibe.clap(vibe_check[1])
d2s = vibe.clap(vibe_check[3])
sync_off = vibe.clap(vibe_check[0])
return vibe
It creates a TruthWell instance, and uses it to decrypt the 2nd, 4th, and 1st items in vibe_check.
TruthWell has an __init__ function:
def __init__(self):
self.flow = self.stream()
def stream(self):
sauce = fetch()
cap = len(sauce)
pulse = 223
while True:
pulse = 0x1337 + pulse - 223
while pulse >= cap:
pulse = pulse - cap
yield ord(sauce[pulse])
Because stream is a generator function (it uses the yield keyword to return a value and then continue on the next call), this allows it to get values from self.flow over and over.
fetch also does some initiation:
def fetch():
global mmwt
global d2s
try:
with open(sys.argv[1]) as scroll:
return scroll.read()
except:
mmwt = f"nah bruh, usage: lowkey_sleigh.gyat <truth scroll> <password>"
d2s = "ight imma head out"
If it fails to open in the “truth scroll” value, it will set the usage strings and return None. Then when it tries to get len(None), it’ll crash again, which is caught back in the __name__ == "__main__" block and printed.
When it actually does read the file, that is passed back and stored as sauce . The it basically starts reading at an offset of 223 into the file, and then jumps forward 0x1337 - 223 (which is 4696). If it goes beyond the end of the file, it just loops back, subtracting the length of the file. Finally, it yields the ordinal value for the current character.
clap takes a string and xors it with bytes from self.flow:
def clap(self, ohio, frfr=False):
drip = ''.join(chr(w ^ y) for w, y in zip(ohio, self.flow))
if frfr:
return drip.encode()
return drip
It’s important to note that the state in TruthWell is maintained across calls to clap. So calling the order in which strings are decrypted matters.
Recover Static Strings
Back in strat, it first decrypts the 2nd, then 4th, then 1st strings in vibe_check. The password has not yet come into play, so I can edit the function to see what they decrypt to:
def strat():
global mmwt
global d2s
global sync_off
vibe = TruthWell()
mmwt = vibe.clap(vibe_check[1])
d2s = vibe.clap(vibe_check[3])
sync_off = vibe.clap(vibe_check[0])
print(f"{mmwt=}")
print(f"{d2s=}")
print(f"{sync_off=}")
return vibe
Running now shows them:
oxdf@hacky$ python lowkey_sleigh.py oracle
mmwt='miss me with that'
d2s='duces'
sync_off='Yo, sleigh the holidays, frfr. Big vibes, no cap.\n +++++++ ::: ::: \n ++++++++++++ ::::::::--:::: \n +++++++++++++++++ ::::::::::::::::::: \n ++++++++++++***++++++ ......::::::::::::::: \n ++=:......:=++**+++++:........::::::::::::. \n .................-++++-..........:::::::::::. \n .....................:=+:..........:::::::::::. \n ....................................::::=::::::: \n .............................::...:.:::-++++:::::: \n ........::.::::::::...........+*++**++++**+++=::::: \n .....:::..:::::::::::::........::::::=++=::::::::::::: \n ......:::::=--=-::::::::..::.....::::::=++=::::::::::::: \n ......:::::::::::----:---=-:::....:::::-=++=-:--::::--::: \n ...............:----:-:::::::::....:::::=++=::::::::::: \n ######## ...............:------.....:::::....::::=++=::::::::::: \n #######=... ................:-::........::......:::=++==*#+++=:::: \n *#####:..=*** ................:::::................:::=:..=######-::: \n ##:.:+*****+....................................++****-..+#####****####\n ..=******-..................................:********:.:*############\n *****-..................................-********+:=++++++++++++ \n ****:.................................-******+++++++++++++++++ \n *=.................................+***++++++++++++++++++++ \n **=::.:.........................=++++++++++++++++++++++++ \n *******=.............==:.....-+**++++++++++++++++++++++++ \n ###################### ********-...........=********###*++++++++++++++++++++++++ \n ######################## *#*******+:.......-**********###*++++++++++++++++++++++++ \n +*+++++++++*+++++*#### ###***+*****+=+*************###*++++++++++++++++++++++++ \n ++++++++++++++++++*###***##=++==+******************###*++++++++++++++++++++++++ \n ++++++++++++++++++*###****+=*##*=******************###*++++++++++++++++++++++++ \n +++++++++++++++++++*####*****+==*####################*+++++++++++++++++++++++++ \n ++++++++++++++++++++*##############################**++++++++++++++++++++++++++ \n *##### +++++++++++++++++++++++***************************+++++++++++++++++++++++++++++ \n ############ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ \n ################# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ \n##### ######*####* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ \n#### ####### #### ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ \n#### ##### ##### +++++++++****+++****++++++++++++++++++++++++++++****+++***++++ \n#### ############ #### ##### ####* *#### \n ##### ######### #### #### #### #### \n ##################################################################################################\n ################################################################################################\n\nDedicated to L and M.\nCheck out PyGyat at https://www.pygyat.org/\n'
miss me with that
duces
I’ve already seen the first two. The last one is some ASCII art, with a nod to pygyat.org (if I hadn’t already found that).
Recover Flag
After strat, the TruthWell instance is passed into reveal_the_drip as clap:
def reveal_the_drip(clap):
opener = clap.clap(b'Yooo Merry Skibidimas bestie, may your vibe be immaculate and your presents be absolutely goated with the sauce', frfr=True)
lock = clap.clap(sys.argv[2].encode(), frfr=True)
if clap.snap(opener, lock) != vibe_check[2]:
raise
print(sync_off)
This encrypts the static string and saves it as opener. Then it encrypts the input password and saves it as lock. Then it passes the two results to clap.snap, which is a simple xor, and checks if the result is the final vibe_check string.
So the password is encrypted and then xored with opener and compared to vibe_check[2]. Both the encryptions are xor, which means both are reversible.
So if I take opener xor vibe_check[2], that’ll make the target encrypted password. Then if I clap.clap that, it’ll make the decrypted flag:
def reveal_the_drip(clap):
opener = clap.clap(b'Yooo Merry Skibidimas bestie, may your vibe be immaculate and your presents be absolutely goated with the sauce', frfr=True)
enc_pass = clap.snap(opener, vibe_check[2])
pt_pass = clap.clap(enc_pass)
print(pt_pass)
sys.exit()
lock = clap.clap(sys.argv[2].encode(), frfr=True)
if clap.snap(opener, lock) != vibe_check[2]:
raise
print(sync_off)
Running this prints the flag:
oxdf@hacky$ python lowkey_sleigh.py oracle
FV25{pygyat_s4ys_h3ll0_f3ll0w_k1ds}
miss me with that
duces
If I comment out the changes and run it, or go back to the original .gyat file, it prints a nice ASCII message:
oxdf@hacky$ pygyat lowkey_sleigh.gyat oracle FV25{pygyat_s4ys_h3ll0_f3ll0w_k1ds}
Yo, sleigh the holidays, frfr. Big vibes, no cap.
+++++++ ::: :::
++++++++++++ ::::::::--::::
+++++++++++++++++ :::::::::::::::::::
++++++++++++***++++++ ......:::::::::::::::
++=:......:=++**+++++:........::::::::::::.
.................-++++-..........:::::::::::.
.....................:=+:..........:::::::::::.
....................................::::=:::::::
.............................::...:.:::-++++::::::
........::.::::::::...........+*++**++++**+++=:::::
.....:::..:::::::::::::........::::::=++=:::::::::::::
......:::::=--=-::::::::..::.....::::::=++=:::::::::::::
......:::::::::::----:---=-:::....:::::-=++=-:--::::--:::
...............:----:-:::::::::....:::::=++=:::::::::::
######## ...............:------.....:::::....::::=++=:::::::::::
#######=... ................:-::........::......:::=++==*#+++=::::
*#####:..=*** ................:::::................:::=:..=######-:::
##:.:+*****+....................................++****-..+#####****####
..=******-..................................:********:.:*############
*****-..................................-********+:=++++++++++++
****:.................................-******+++++++++++++++++
*=.................................+***++++++++++++++++++++
**=::.:.........................=++++++++++++++++++++++++
*******=.............==:.....-+**++++++++++++++++++++++++
###################### ********-...........=********###*++++++++++++++++++++++++
######################## *#*******+:.......-**********###*++++++++++++++++++++++++
+*+++++++++*+++++*#### ###***+*****+=+*************###*++++++++++++++++++++++++
++++++++++++++++++*###***##=++==+******************###*++++++++++++++++++++++++
++++++++++++++++++*###****+=*##*=******************###*++++++++++++++++++++++++
+++++++++++++++++++*####*****+==*####################*+++++++++++++++++++++++++
++++++++++++++++++++*##############################**++++++++++++++++++++++++++
*##### +++++++++++++++++++++++***************************+++++++++++++++++++++++++++++
############ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
################# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
##### ######*####* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#### ####### #### ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#### ##### ##### +++++++++****+++****++++++++++++++++++++++++++++****+++***++++
#### ############ #### ##### ####* *####
##### ######### #### #### #### ####
##################################################################################################
################################################################################################
Dedicated to L and M.
Check out PyGyat at https://www.pygyat.org/
duces
Flag: FV25{pygyat_s4ys_h3ll0_f3ll0w_k1ds}
FV25.03
Challenge
It’s time to load the presents into Santa’s Slegih at the North Pole, a task assigned to Jinglebell this year. Jinglebell keeps the password for the warehouse safely in a file in his home directory. Unfortunately, the other elves have played a prank—they’ve hidden the warehouse password in a tangled maze throughout the workshop. Things are falling behind schedule, and Jinglebell needs your help. Can you navigate the labyrinth and find the password? |
|
| Categories: |
|
| Level: | easy |
| Author: | 0xdf |
| Spawnable Instance: |
|
It also gives a button to start a container as well as instructions for connecting to SSH:
export COMMAND='ncat --ssl <...>.challs.flagvent.org 31337'
ssh -o "ProxyCommand=$COMMAND" elf@localhost
Starting the container gives a home name of the format <GUID>.challs.flagvent.org.
Solution
Enumeration
I’ll connect to the container using the given command:
oxdf@hacky$ ssh -o "ProxyCommand=ncat --ssl fdfb4c86-5b0b-4d5f-8ee1-11c6fe46e5ca.challs.flagvent.or
g 31337" elf@localhost
elf@localhost's password:
Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.12.45-talos x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
This system has been minimized by removing packages and content that are
not required on a system that users do not log into.
To restore this content, you can run the 'unminimize' command.
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
elf@host:~$
The elf user’s home directory is pretty empty other than some standard config files and a symbolic link:
elf@host:~$ la -la
total 12
drwxr-x---. 1 elf elf 46 Dec 3 18:22 .
drwxr-xr-x. 1 root root 17 Nov 24 20:52 ..
-rw-r--r--. 1 elf elf 220 Mar 31 2024 .bash_logout
-rw-r--r--. 1 elf elf 3771 Mar 31 2024 .bashrc
drwx------. 2 elf elf 34 Dec 3 18:22 .cache
-rw-r--r--. 1 elf elf 807 Mar 31 2024 .profile
lrwxrwxrwx. 1 elf elf 16 Dec 3 18:17 warehouse_password -> /workshop/rf0eoz
The link shows as red, as it’s broken. Trying to read it fails:
elf@host:~$ cat warehouse_password
cat: warehouse_password: Permission denied
elf doesn’t have permissions to read it. At the system root, there’s the standard stuff, along with entrypoint.sh and workshop:
elf@host:/$ ls -l
total 48
lrwxrwxrwx. 1 root root 7 Apr 22 2024 bin -> usr/bin
drwxr-xr-x. 2 root root 6 Apr 22 2024 boot
drwxr-xr-x. 5 root root 340 Dec 3 18:16 dev
-rwx------. 1 root root 629 Nov 24 20:52 entrypoint.sh
drwxr-xr-x. 1 root root 23 Nov 24 20:52 etc
drwxr-xr-x. 1 root root 17 Nov 24 20:52 home
lrwxrwxrwx. 1 root root 7 Apr 22 2024 lib -> usr/lib
drwxr-xr-x. 2 root root 6 Nov 14 2024 lib.usr-is-merged
lrwxrwxrwx. 1 root root 9 Apr 22 2024 lib64 -> usr/lib64
drwxr-xr-x. 2 root root 6 Oct 13 14:02 media
drwxr-xr-x. 2 root root 6 Oct 13 14:02 mnt
drwxr-xr-x. 2 root root 6 Oct 13 14:02 opt
dr-xr-xr-x. 311 root root 0 Dec 3 18:16 proc
drwx------. 1 root root 18 Nov 24 20:52 root
drwxr-xr-x. 1 root root 42 Dec 3 18:22 run
lrwxrwxrwx. 1 root root 8 Apr 22 2024 sbin -> usr/sbin
drwxr-xr-x. 2 root root 6 Oct 13 14:02 srv
dr-xr-xr-x. 13 root root 0 Dec 3 17:02 sys
drwxrwxrwt. 2 root root 6 Oct 13 14:09 tmp
drwxr-xr-x. 1 root root 96 Oct 13 14:02 usr
drwxr-xr-x. 1 root root 17 Oct 13 14:09 var
drwx------. 1 root root 24576 Dec 3 18:17 workshop
Both of those are owned by root and not accessible to elf.
elf can run cat and stat as the root user using sudo:
elf@host:/$ sudo -l
Matching Defaults entries for elf on host:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User elf may run the following commands on host:
(root) NOPASSWD: /bin/cat /workshop/*
(root) NOPASSWD: /usr/bin/stat /workshop/*
I can use that to check out the file pointed to by warehouse_password:
elf@host:/$ sudo stat /workshop/rf0eoz
File: /workshop/rf0eoz -> /workshop/jce5dp
Size: 16 Blocks: 0 IO Block: 4096 symbolic link
Device: 0,274 Inode: 269949556 Links: 1
Access: (0777/lrwxrwxrwx) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2025-12-03 18:26:18.348035639 +0000
Modify: 2025-12-03 18:17:15.169017543 +0000
Change: 2025-12-03 18:17:15.169017543 +0000
Birth: 2025-12-03 18:17:15.169017543 +0000
If I try to read this link, it still fails:
elf@host:/$ sudo cat /workshop/rf0eoz
cat: /workshop/rf0eoz: Too many levels of symbolic links
On Linux systems, the default is typically 40 links and then the OS throws an ELOOP error. This exists to prevent loops and being stuck forever, but this error doesn’t necessarily mean there’s a loop.
Intended Solution
The intended solution is to write a Bash loop that uses stat to get the target of the link, and then continues until there’s no more target. Something like this will get the target:
elf@host:~$ stat warehouse_password | head -1 | awk -F ' -> ' '{print $2}'
/workshop/0q08pi
elf@host:~$ sudo stat /workshop/0q08pi | head -1 | awk -F ' -> ' '{print $2}'
/workshop/5aqx7v
I’ll wrap this in a loop:
elf@host:~$ fn=$(stat warehouse_password | head -1 | awk -F' -> ' '{print $2}')
count=0
while true; do
s=$(sudo stat "$fn")
if echo "$s" | grep ' -> ' -q; then
fn=$(echo "$s" | head -1 | awk -F' -> ' '{print $2}')
count=$((count + 1))
printf "\rLink %d: %s" "$count" "$fn"
else
printf "\r"
sudo cat "$fn"
break
fi
done
FV25{ELOOP_ELOOP_ELOOP_ELOOP}
I’m always a bit of a sucker for live updates on a single line, which this does with printf and \r. Once it gets a stat that doesn’t have a -> in it, it will cat that file, giving the flag.
Unintended Solution
Wildcards in sudo rules are very difficult to do securely, and it break here as well. The rules allow for /bin/cat /workshop/*. * can match on the string “ /etc/shadow”:
elf@host:~$ sudo cat /workshop/ /etc/shadow
cat: /workshop/: Is a directory
root:*:20374:0:99999:7:::
daemon:*:20374:0:99999:7:::
bin:*:20374:0:99999:7:::
sys:*:20374:0:99999:7:::
sync:*:20374:0:99999:7:::
games:*:20374:0:99999:7:::
man:*:20374:0:99999:7:::
lp:*:20374:0:99999:7:::
mail:*:20374:0:99999:7:::
news:*:20374:0:99999:7:::
uucp:*:20374:0:99999:7:::
proxy:*:20374:0:99999:7:::
www-data:*:20374:0:99999:7:::
backup:*:20374:0:99999:7:::
list:*:20374:0:99999:7:::
irc:*:20374:0:99999:7:::
_apt:*:20374:0:99999:7:::
nobody:*:20374:0:99999:7:::
ubuntu:!:20374:0:99999:7:::
systemd-network:!*:20416::::::
systemd-timesync:!*:20416::::::
messagebus:!:20416::::::
systemd-resolve:!*:20416::::::
sshd:!:20416::::::
elf:$y$j9T$F13DWqYoOjyhwx.N0NEGK/$nxQzNohOSbsRYyWhB1WiatGfXQbrll/C6Y5YibbvEb9:20416:0:99999:7:::
I’ll read the entrypoint.sh script:
#!/bin/bash
rm -f /workshop/* /home/elf/warehouse_password
cd /workshop
names=()
for i in {1..1000}; do
name=$(tr -dc a-z0-9 </dev/urandom | head -c 6)
names+=("$name")
done
echo "FV25{ELOOP_ELOOP_ELOOP_ELOOP}" > "${names[999]}"
chmod 600 "${names[999]}"
for i in {998..0}; do
ln -s "/workshop/${names[$((i+1))]}" "${names[$i]}"
done
echo "${names[0]}" > /workshop/.start
chmod 644 /workshop/.start
ln -s "/workshop/${names[0]}" /home/elf/warehouse_password
chown -h elf:elf /home/elf/warehouse_password
exec /usr/sbin/sshd -D
It’s generating 1000 random names, and then putting the flag in the last one. Then it loops from 998 to 0, creating links between successive names
Flag: FV25{ELOOP_ELOOP_ELOOP_ELOOP}
FV25.04
Challenge
The damn Grinch messed around again with Santas files! And now he can’t watch his favorite video. Can you help me? |
|
| Categories: |
|
| Level: | easy |
| Author: | W1nd5h13ldV1p3r |
| Attachments: |
📦 our-house.tar.gz
|
The archive contains a single file, chal.mp4.
Solution
Repair Video
The video isn’t recognized as such:
oxdf@hacky$ file chal.mp4
chal.mp4: data
If I try to open it, it fails:
Looking at the list of file signatures page, it should have “ftypisom” or “ftypMSNV” at an offset of four bytes. This medium post has a nice breakdown of the MP4 file format, and shows what the header should look like:
I’ll use a hex editor to update the first 32 bytes to match that, and now file shows it’s an MP4:
oxdf@hacky$ file chal-fixed.mp4
chal-fixed.mp4: ISO Media, MP4 Base Media v1 [ISO 14496-12:2003]
oxdf@hacky$ xxd chal-fixed.mp4 | head -n 4
00000000: 0000 0020 6674 7970 6973 6f6d 0000 0200 ... ftypisom....
00000010: 6973 6f6d 6973 6f32 6176 6331 6d70 3431 isomiso2avc1mp41
00000020: 0000 0008 6672 6565 0073 040a 6d64 6174 ....free.s..mdat
00000030: 0000 0058 0605 2edc 45e9 bde6 d948 b796 ...X....E....H..
And it plays:
It starts by saying “Our House”, and then there’s a picture of a house. The perspective zooms into the house, and then the door opens and we go through to find the house again, on a loop.
Around 1:30 in, it changes to a different house:
Identify Audio Streams
ffprobe will show information about the video, including it’s streams:
oxdf@hacky$ ffprobe chal-fixed.mp4 2>&1 | grep -A2 "Stream"
Stream #0:0[0x1](und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 640x360 [SAR 1:1 DAR 16:9], 297 kb/s, 30 fps, 30 tbr, 16k tbn (default)
Metadata:
handler_name : ISO Media file produced by Google Inc. Created on: 01/30/2025.
--
Stream #0:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 129 kb/s (default)
Metadata:
handler_name : ISO Media file produced by Google Inc. Created on: 01/30/2025.
--
Stream #0:2[0x3](und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, mono, fltp, 70 kb/s
Metadata:
handler_name : SoundHandler
There’s one video stream, but two audio streams. I’ll extract both audio streams to look at them independently:
oxdf@hacky$ ffmpeg -i chal-fixed.mp4 -map 0:1 -acodec pcm_s16le audio1.wav
ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 the FFmpeg developers
built with gcc 13 (Ubuntu 13.2.0-23ubuntu3)
configuration: --prefix=/usr --extra-version=3ubuntu5 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --disable-omx --enable-gnutls --enable-libaom --enable-libass --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libglslang --enable-libgme --enable-libgsm --enable-libharfbuzz --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-openal --enable-opencl --enable-opengl --disable-sndio --enable-libvpl --disable-libmfx --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-ladspa --enable-libbluray --enable-libjack --enable-libpulse --enable-librabbitmq --enable-librist --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libx264 --enable-libzmq --enable-libzvbi --enable-lv2 --enable-sdl2 --enable-libplacebo --enable-librav1e --enable-pocketsphinx --enable-librsvg --enable-libjxl --enable-shared
libavutil 58. 29.100 / 58. 29.100
libavcodec 60. 31.102 / 60. 31.102
libavformat 60. 16.100 / 60. 16.100
libavdevice 60. 3.100 / 60. 3.100
libavfilter 9. 12.100 / 9. 12.100
libswscale 7. 5.100 / 7. 5.100
libswresample 4. 12.100 / 4. 12.100
libpostproc 57. 3.100 / 57. 3.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'chal-fixed.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf62.3.100
Duration: 00:02:07.87, start: 0.000000, bitrate: 483 kb/s
Stream #0:0[0x1](und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 640x360 [SAR 1:1 DAR 16:9], 297 kb/s, 30 fps, 30 tbr, 16k tbn (default)
Metadata:
handler_name : ISO Media file produced by Google Inc. Created on: 01/30/2025.
vendor_id : [0][0][0][0]
Stream #0:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 129 kb/s (default)
Metadata:
handler_name : ISO Media file produced by Google Inc. Created on: 01/30/2025.
vendor_id : [0][0][0][0]
Stream #0:2[0x3](und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, mono, fltp, 70 kb/s
Metadata:
handler_name : SoundHandler
vendor_id : [0][0][0][0]
Stream mapping:
Stream #0:1 -> #0:0 (aac (native) -> pcm_s16le (native))
Press [q] to stop, [?] for help
Output #0, wav, to 'audio1.wav':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
ISFT : Lavf60.16.100
Stream #0:0(eng): Audio: pcm_s16le ([1][0][0][0] / 0x0001), 44100 Hz, stereo, s16, 1411 kb/s (default)
Metadata:
handler_name : ISO Media file produced by Google Inc. Created on: 01/30/2025.
vendor_id : [0][0][0][0]
encoder : Lavc60.31.102 pcm_s16le
[out#0/wav @ 0x56139dbce140] video:0kB audio:19149kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.000398%
size= 19149kB time=00:01:51.13 bitrate=1411.5kbits/s speed= 177x
oxdf@hacky$ ffmpeg -i chal-fixed.mp4 -map 0:2 -acodec pcm_s16le audio2.wav
ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 the FFmpeg developers
built with gcc 13 (Ubuntu 13.2.0-23ubuntu3)
configuration: --prefix=/usr --extra-version=3ubuntu5 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --disable-omx --enable-gnutls --enable-libaom --enable-libass --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libglslang --enable-libgme --enable-libgsm --enable-libharfbuzz --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-openal --enable-opencl --enable-opengl --disable-sndio --enable-libvpl --disable-libmfx --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-ladspa --enable-libbluray --enable-libjack --enable-libpulse --enable-librabbitmq --enable-librist --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libx264 --enable-libzmq --enable-libzvbi --enable-lv2 --enable-sdl2 --enable-libplacebo --enable-librav1e --enable-pocketsphinx --enable-librsvg --enable-libjxl --enable-shared
libavutil 58. 29.100 / 58. 29.100
libavcodec 60. 31.102 / 60. 31.102
libavformat 60. 16.100 / 60. 16.100
libavdevice 60. 3.100 / 60. 3.100
libavfilter 9. 12.100 / 9. 12.100
libswscale 7. 5.100 / 7. 5.100
libswresample 4. 12.100 / 4. 12.100
libpostproc 57. 3.100 / 57. 3.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'chal-fixed.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf62.3.100
Duration: 00:02:07.87, start: 0.000000, bitrate: 483 kb/s
Stream #0:0[0x1](und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 640x360 [SAR 1:1 DAR 16:9], 297 kb/s, 30 fps, 30 tbr, 16k tbn (default)
Metadata:
handler_name : ISO Media file produced by Google Inc. Created on: 01/30/2025.
vendor_id : [0][0][0][0]
Stream #0:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 129 kb/s (default)
Metadata:
handler_name : ISO Media file produced by Google Inc. Created on: 01/30/2025.
vendor_id : [0][0][0][0]
Stream #0:2[0x3](und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, mono, fltp, 70 kb/s
Metadata:
handler_name : SoundHandler
vendor_id : [0][0][0][0]
Stream mapping:
Stream #0:2 -> #0:0 (aac (native) -> pcm_s16le (native))
Press [q] to stop, [?] for help
Output #0, wav, to 'audio2.wav':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
ISFT : Lavf60.16.100
Stream #0:0(und): Audio: pcm_s16le ([1][0][0][0] / 0x0001), 44100 Hz, mono, s16, 705 kb/s
Metadata:
handler_name : SoundHandler
vendor_id : [0][0][0][0]
encoder : Lavc60.31.102 pcm_s16le
[out#0/wav @ 0x56af5e186140] video:0kB audio:9592kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.000794%
size= 9592kB time=00:01:51.33 bitrate= 705.8kbits/s speed= 248x
This results in two files:
oxdf@hacky$ file audio*.wav
audio1.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, stereo 44100 Hz
audio2.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 44100 Hz
audio2.wav
Listening to the file, it starts with what sounds like a dial-up modem, then breaks into steady beats / tones. That’s characteristic of Slow-scan Television (SSTV). It’s a picture transmissions method used mainly be amateur radio operators to send pictures via radio.
I’ll upload the audio file to an online decoder and it generates the flag:
Flag: FV25{0ur_h0u53}
FV25.22
Challenge
When the aliens left the earth a long time ago, they left us a message in the genome. Our high-tech instruments have extracted the encoded message from the genome of an average human. Interestingly, they used a kind of 8-bit ASCII encoding, but since the Rendlesham Forest incident, this should not come as a huge surprise. It also seems that they used the expression “DNA encoding” in the message. |
|
| Categories: |
|
| Level: | easy |
| Author: | brp64 |
| Attachments: |
📦 the-crib-in-the-genome.tar.gz
|
The download is a single file, enc.text.
Solution
The file contains a string of A, C, G, and T:
GCCTGGATAGATGTATACGTTCATCTGTTTGTCGATTCATCTGTTAATGGATAGATGCGTCGACCCACGTATGGATGGATCTATCCACTTGTGCGTTTATCCACGGATATATCCACCTGTCAATTTATCCACGCATACGTTAATACATCCACCTCTAGCTTCCTCCACTTATAGATGCATGGATCTATTAATAGATGTATAGACCCACCCACCACTTTATACGTTTATCCACTAATGCGTCCACTAGTGGATTTGTACGTCCACATATCGATTCATGTATCCACATCTATTTACGCTTGCGAGTCTCTAGCTTCCTGGTTGCGCAGATGCATCCGCCTATGCGCCTATGGTTATATTCGCCTGCATGCTGGT
These are the four letters typically used to represent gene sequences, though that’s not super important here.
With four possibilities at each character, that means each one represents two bits. That means each character represents some combination of ‘00’, ‘01’, ‘10’, and ‘11’. I’ll use the Python function permutations from the itertools package to try every possible mapping. I’ll also try the resulting bit strings forward and backwards.
Given the scenario, I can look for “DNA encoding” in the result:
import itertools
def bits_to_data(bitstring, byteorder, bits_rev=False):
if bits_rev:
bitstring = bitstring[::-1]
return int(bitstring, 2).to_bytes((len(bitstring) + 7) // 8, byteorder=byteorder)
with open("the-crib-in-the-genome/enc.text", "r") as f:
dna = f.read().strip()
bases = ["A", "C", "G", "T"]
bit_values = ["00", "01", "10", "11"]
for perm in itertools.permutations(bit_values):
mapping = dict(zip(bases, perm))
bitstring = "".join(mapping[base] for base in dna)
for byteorder in ["big", "little"]:
for reversed in [True, False]:
data = bits_to_data(bitstring, byteorder, reversed)
if b"DNA encoding" in data:
print(f"Found flag with {mapping=}, {byteorder=}, {reversed=}")
print(data.decode())
This finds the flag:
oxdf@hacky$ uv run solve.py
Found flag with mapping={'A': '01', 'C': '00', 'G': '11', 'T': '10'}, byteorder='little', reversed=True
Congratulations, good use of the crib DNA encoding. Here is your flag FV25{DNA_3nc0d3d_f146}
Flag: FV25{DNA_3nc0d3d_f146}
FV25.23
Challenge
santa got more and more interested in composing music himself and he decided to release his first song, which the elves deemed “lit” and a “vibe”. unfortunately, he left the sheet on the piano and the grinch scrambled all the notes! the music doesn’t sound good anymore, it’s just black and white keys. santa is trying to give a Quick Response and although he’s squaring his shoulders, he needs your help! |
|
| Categories: |
|
| Level: | easy |
| Author: | kuyaya |
| Attachments: |
📦 santas-tune.tar.gz
|
The download archive contains a single file with the .musicxml extension:
oxdf@hacky$ file santas-tune/flagvent.musicxml
santas-tune/flagvent.musicxml: XML 1.0 document, ASCII text
Solution
The musicXML file format is “the standard open format for exchanging digital sheet music”. soundslice.com has a viewer for this file format:
I’ll note that there are a lot of sharp notes (marked by #).
There are 650 notes in the music:
oxdf@hacky$ grep -c "<note>" flagvent.musicxml
650
650 is 25 x 26, which could be a QR code (though I would expect 25 x 25). I’ll try looking at the music as a square, using sharps as black and others as white:
import xml.etree.ElementTree as ET
from PIL import Image
tree = ET.parse('flagvent.musicxml')
root = tree.getroot()
pixels = []
for note in root.iter('note'):
alter = note.find('.//alter')
if alter is not None and alter.text == '1':
pixels.append(0) # Sharp (black key) = black pixel
else:
pixels.append(255) # Natural (white key) = white pixel
# Create 26x25 image
img = Image.new('L', (26, 25))
img.putdata(pixels)
img = img.resize((260, 250), Image.NEAREST)
img.save('qr_26x25.png')
The resulting image is a QRcode:
It contains the flag:
oxdf@hacky$ zbarimg qr_26x25.png
QR-Code:FV25{wh4t_4_l0v3ly_tun3}
scanned 1 barcode symbols from 1 images in 0.05 seconds
Flag: FV25{wh4t_4_l0v3ly_tun3}
FV25.24
Challenge
Santa’s elves are busy making presents, but one of their most important tools is locked away in a safe. The only elf who knows the combination is on a mission delivering presents to the astronauts on the ISS. The elves know that the combination is in that elf’s notes, but the notes are encrypted. The connection between the North Pole and the ISS is very slow, so the key is arriving only one byte per day, so it will take 24 days for the full key to arrive. At this rate, the it won’t arrive before Christmas Eve, and so the presents won’t be ready. Help the elves decrypt the notes. |
|
| Categories: |
|
| Level: | easy |
| Author: | villambarna |
| Attachments: |
📦 xormaas.tar.gz
|
The download archive contains a single file, data, that isn’t recognized as anything by file:
oxdf@hacky$ ls xormaas
data
oxdf@hacky$ file xormaas/data
xormaas/data: data
oxdf@hacky$ wc xormaas/data
27 70 936 xormaas/data
Solution
Decryption Round 1
The clue makes it clear that this data is using a 24-byte XOR key for encryption. I’ll take every 24th character and see if any of the 256 possible byte generates an XOR result that’s entirely valid ASCII:
from collections import defaultdict
from string import printable
key_len = 24
with open('xormaas/data', 'rb') as f:
data = f.read()
keys = defaultdict(list)
for i in range(key_len):
group = bytes(data[j] for j in range(i, len(data), key_len))
for x in range(256):
decrypted = bytes([b ^ x for b in group])
if all(chr(b) in printable for b in decrypted):
keys[i].append(x)
print(keys)
Unfortunately, that leaves a lot of possible keys:
oxdf@hacky$ uv run .\solve.py
defaultdict(<class 'list'>, {0: [112, 114, 115, 116], 1: [96, 97, 103, 105, 107, 108, 109, 112, 113, 114, 115, 116, 117, 119, 122, 123, 125], 2: [96, 97, 98, 102, 103], 6: [115, 117, 119], 7: [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 55, 57, 59, 60, 61, 62, 63, 102, 103, 104, 105, 106, 107, 108, 109, 110, 112, 115, 116, 118, 123, 124, 125], 8: [58, 59, 61, 96, 97, 98, 99, 100, 102, 103, 105, 106, 107, 108, 109, 115, 116, 118, 119, 123, 124, 126, 127], 9: [96, 97, 98, 99, 100, 101, 102, 103, 105, 110, 111, 112, 115, 116, 117, 120, 121, 126], 10: [32, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 50, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 98, 100, 101, 102, 104, 105, 107, 108, 109, 110, 111, 113, 115, 116, 120, 121, 122], 11: [32, 33, 34, 35, 36, 37, 38, 40, 41, 42, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 60, 61, 62, 63, 98, 100, 101, 103, 108, 113, 114, 116, 118, 119, 120, 122, 123, 124, 125, 127], 18: [97, 99, 100, 101, 104, 105, 106, 107, 108, 110, 111, 112, 116, 120, 126, 127], 19: [32, 33, 34, 35, 36, 37, 39, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 55, 56, 59, 60, 61, 62, 63, 97, 102, 103, 104, 105, 106, 107, 108, 109, 110, 113, 116, 118, 121, 123, 124], 20: [32, 33, 36, 38, 39, 104, 105, 109, 110, 111], 21: [96, 97, 98, 99, 100, 101, 102, 103, 105, 106, 110, 114, 115, 116, 117, 120, 122, 123, 127], 22: [96, 98, 99, 100, 101, 102, 103, 105, 108, 110, 114, 115, 117, 120], 23: [32, 33, 34, 35, 36, 37, 38, 40, 41, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 55, 57, 58, 59, 60, 61, 62, 63, 96, 97, 98, 99, 100, 101, 102, 103, 104, 110, 111, 114, 115, 118, 120, 123, 126]})
If I add a print for each time it finds a key, I’ll see that it’s a lot of symbols:
oxdf@hacky$ uv run .\solve.py
b'Nmwf#zflwt/wfjqv\tbg#fjlfqwmwpbp#qqw#T X'
b'Loud!xdnuv-udhst\x0b`e!dhndsuour`r!ssu!V"Z'
b'Mnte yeotw,teiru\nad eioertntsas rrt W#['
b"Jisb'~bhsp+sbnur\rfc'bnhbusistft'uus'P$\\"
b'U444cyfa:u4{4`u4[g{]pwfzq|y4`z|`:y}ui01'
b'T555bxg`;t5z5at5Zfz\\qvg{p}x5a{}a;x|th10'
b'R333d~af=r3|3gr3\\`|Zwpa}v{~3g}{g=~zrn76'
b'\\===jpoh3|=r=i|=RnrTy~osxup=isui3pt|`98'
b'^???hrmj1~?p?k~?PlpV{|mqzwr?kqwk1rv~b;:'
b'Y888oujm6y8w8ly8WkwQ|{jv}pu8lvpl6uqye<='
b'X999ntkl7x9v9mx9VjvP}zkw|qt9mwqm7tpxd=<'
b'E$$$sivq*e$k$pe$KwkM`gvjali$pjlp*imey !'
b'D%%%rhwp+d%j%qd%JvjLafwk`mh%qkmq+hldx! '
b'G&&&qkts(g&i&rg&IuiObethcnk&rhnr(kog{"#'
b'F\'\'\'pjur)f\'h\'sf\'HthNcduiboj\'sios)jnfz#"'
b'A wmru.a o ta OsoIdcrnehm tnht.mia}$%'
b'@!!!vlst/`!n!u`!NrnHebsodil!uoiu/lh`|%$'
b"B###tnqv-b#l#wb#LplJg`qmfkn#wmkw-njb~'&"
b'O...yc|{ o.a.zo.A}aGjm|`kfc.z`fz cgos*+'
b'N///xb}z!n/`/{n/@|`Fkl}ajgb/{ag{!bfnr+*'
b"H)))~d{|'h)f)}h)Fzf@mj{glad)}ga}'d`ht-,"
b'Svvs``us@rg!oruum!o!ft,\x0bfdnune`i!hbrv+]'
b'RwwraatrAsf nsttl n gu-\ngeotodah icsw*\\'
b'QttqbbwqBpe#mpwwo#m#dv.\tdflwlgbk#j`pt)_'
b"UppuffsuFta'itssk'i'`r*\r`bhshcfo'ndtp-["
b'TqqtggrtGu`&hurrj&h&as+\x0caciribgn&oeuq,Z'
b"')btnsu)Thf\r'kedfhsppkkk'fupn'ih'b~fexG"
b'!/drhus/Rn`\x0b!mcb`nuvvmmm!`svh!on!dx`c~A'
b'#-fpjwq-Plb\t#oa`blwttooo#bqtj#ml#fzba|C'
b">i?=.!ei*-'!&%0&;&ie!0g (-0 :<&<>ie- *?"
b"?h></ dh+,& '$1':'hd 1f!),1!;='=?hd,!+>"
b'<k=?,#gk(/%#$\'2$9$kg#2e"*/2"8>$><kg/"(='
b'=j<>-"fj).$"%&3%8%jf"3d#+.3#9?%?=jf.#)<'
b':m;9*%am.)#%"!4"?"ma%4c$,)4$>8"8:ma)$.;'
b';l:8+$`l/("$# 5#>#l`$5b%-(5%?9#9;l`(%/:'
b"8o9;('co,+!' #6 = oc'6a&.+6&<: :8oc+&,9"
b'9n8:)&bn-* &!"7!<!nb&7`\'/*7\'=;!;9nb*\'-8'
b'6a75&)ma"%/).-8.3.am)8o( %8(24.46am%("7'
b"7`64'(l`#$.(/,9/2/`l(9n)!$9)35/57`l$)#6"
b'4c57$+oc \'-+,/:,1,co+:m*"\':*06,64co\'* 5'
b'5b46%*nb!&,*-.;-0-bn*;l+#&;+17-75bn&+!4'
b'2e31"-ie&!+-*)<*7*ei-<k,$!<,60*02ei!,&3'
b"3d20#,hd' *,+(=+6+dh,=j-% =-71+13dh -'2"
b'0g13 /kg$#)/(+>(5(gk/>i.&#>.42(20gk#.$1'
b'1f02!.jf%"(.)*?)4)fj.?h/\'"?/53)31fj"/%0'
b'.y/->1uy:=7165 6+6yu1 w08= 0*,6,.yu=0:/'
b'/x.,?0tx;<6074!7*7xt0!v19<!1+-7-/xt<1;.'
b',{-/<3w{8?5347"4)4{w3"u2:?"2(.4.,{w?28-'
b'-z,.=2vz9>4256#5(5zv2#t3;>#3)/5/-zv>39,'
b'*}+):5q}>93521$2/2}q5$s4<9$4.(2(*}q94>+'
b'+|*(;4p|?82430%3.3|p4%r5=8%5/)3)+|p85?*'
b")~(*96r~=:0612'1,1~r6'p7?:'7-+1+)~r:7=("
b'\'p&$78|p34>8?<)?"?p|8)~914)9#%?%\'p|493&'
b"%r$&5:~r16<:=>+= =r~:+|;36+;!'='%r~6;1$"
b'"u#!2=yu61;=:9,:\':uy=,{<41,<& : "uy1<6#'
b'#t" 3<xt70:<;8-;&;tx<-z=50-=\'!;!#tx0=7"'
b' w!#0?{w439?8;.8%8w{?.y>63.>$"8" w{3>4!'
b'!v "1>zv528>9:/9$9vz>/x?72/?%#9#!vz2?5 '
b'x/y{hg#/lkag`cv`}`/#gv!fnkvf|z`zx/#kfly'
b'y.xzif".mj`fabwa|a."fw gojwg}{a{y."jgmx'
b'v!wufi-!beoinmxnsn!-ix/h`exhrtntv!-ehbw'
b'w vtgh, cdnholyoro ,hy.iadyisuouw ,dicv'
b't#uwdk/#`gmklozlql#/kz-jbgzjpvlvt#/gj`u'
b'u"tvej."afljmn{mpm".j{,kcf{kqwmwu".fkat'
b'r%sqbm)%fakmji|jwj%)m|+lda|lvpjpr%)alfs'
b's$rpcl($g`jlkh}kvk$(l}*me`}mwqkqs$(`mgr'
b"p'qs`o+'dciohk~huh'+o~)nfc~ntrhrp'+cndq"
b'n9om~q59z}wqvu`vkv95q`7px}`pjlvln95}pzo'
b'm:ln}r6:y~truvcuhu:6rc4s{~csiouom:6~syl'
b'j=kizu1=~ysurqdror=1ud3t|ydtnhrhj=1yt~k'
b'h?ikxw3?|{qwpsfpmp?3wf1v~{fvljpjh?3{v|i'
b'e2dfuz>2qv|z}~k}`}2>zk<{svk{ag}ge2>v{qd'
b'b5car}95vq{}zylzgz59}l;|tql|f`z`b59q|vc'
b'c4b`s|84wpz|{xm{f{48|m:}upm}ga{ac48p}wb'
b'8\r<<7<y\n+y 0?yy=5+4y8yy7y<y*y7-70-yyi<-'
b'9\x0c==6=x\x0b*x!1>xx<4*5x9xx6x=x+x6,61,xxh=,'
b"?\n;;0;~\r,~'78~~:2,3~?~~0~;~-~0*07*~~n;*"
b'bWffmf#Pq#zje##goqn#b##m#f#p#mwmjw##3fw'
b'cVgglg"Qp"{kd""fnpo"c""l"g"q"lvlkv""2gv'
b'`Uddod!Rs!xhg!!emsl!`!!o!d!r!ouohu!!1du'
b'aTeene Sr yif dlrm a n e s ntnit 0et'
b"fSbbib'Tu'~na''ckuj'f''i'b't'isins''7bs"
b'dQ``k`%Vw%|lc%%aiwh%d%%k%`%v%kqklq%%5`q'
b'ePaaja$Wv$}mb$$`hvi$e$$j$a$w$jpjmp$$4ap'
b'k^oodo*Yx*scl**nfxg*k**d*o*y*d~dc~**:o~'
b'h]llgl)Z{)p`o))me{d)h))g)l)z)g}g`}))9l}'
b'i\\mmfm([z(qan((ldze(i((f(m({(f|fa|((8m|'
b'n[jjaj/\\}/vfi//kc}b/n//a/j/|/a{af{//?j{'
b'oZkk`k.]|.wgh..jb|c.o..`.k.}.`z`gz..>kz'
b'qDuu~u0Cb0iyv00t|b}0q00~0u0c0~d~yd00 ud'
b"vCrryr7De7n~q77s{ez7v77y7r7d7ycy~c77'rc"
b'tApp{p5Fg5l|s55qygx5t55{5p5f5{a{|a55%pa'
b'u@qqzq4Gf4m}r44pxfy4u44z4q4g4z`z}`44$q`'
b'yL}}v}8Kj8aq~88|tju8y88v8}8k8vlvql88(}l'
b'~Kzzqz?Lm?fvy??{smr?~??q?z?l?qkqvk??/zk'
b'|Ixxsx=No=dt{==yqop=|==s=x=n=sisti==-xi'
b'}Hyyry<On<euz<<xpnq<}<<r<y<o<rhruh<<,yh'
b'rissd!`bntur!mbdd,dnueHdbrhehi!umnu`cwB'
b'shrre acouts lcee-eotdIecsidih tlotabvC'
b'pkqqf#b`lvwp#o`ff.flwgJf`pjgjk#wolwbau@'
b'qjppg"camwvq"nagg/gmvfKgaqkfkj"vnmvc`tA'
b'vmww`%dfjpqv%if``(`jqaL`fvlalm%qijqdgsF'
b'wlvva$egkqpw$hgaa)akp`Magwm`ml$phkpefrG'
b"touub'fdhrst'kdbb*bhscNbdtncno'skhsfeqD"
b'unttc&geisru&jecc+cirbOceuobon&rjirgdpE'
b'{`zzm(ikg}|{(dkmm%mg|lAmk{ala`(|dg|ij~K'
b'|g}}j/nl`z{|/cljj"j`{kFjl|fkfg/{c`{nmyL'
b'}f||k.oma{z}.bmkk#kazjGkm}gjgf.zbazolxM'
b'bycct1pr~deb1}rtt<t~euXtrbxuxy1e}~epsgR'
b'az``w2sq}gfa2~qww?w}fv[wqa{v{z2f~}fspdQ'
b'f}ggp5tvz`af5yvpp8pzaq\\pvf|q|}5ayzatwcV'
b'g|ffq4uw{a`g4xwqq9q{`p]qwg}p}|4`x{`uvbW'
b'jqkk|9xzvlmj9uz||4|vm}P|zjp}pq9muvmx{oZ'
b'kpjj}8y{wmlk8t{}}5}wl|Q}{kq|qp8ltwlyzn['
b'lwmmz?~|pjkl?s|zz2zpk{Vz|lv{vw?kspk~}i\\'
b'l)ll(/">#<$l8##l5"-*l)l(#8"#"-(> l$?!n~'
b'n+nn*- <!>&n:!!n7 /(n+n*!: ! /*<"n&=#l|'
b'o*oo+,!= ?\'o; o6!.)o*o+ ;! !.+=#o\'<"m}'
b"h-hh,+&:'8 h<''h1&).h-h,'<&'&),:$h ;%jz"
b"i,ii-*';&9!i=&&i0'(/i,i-&='&'(-;%i!:$k{"
b'j/jj.)$8%:"j>%%j3$+,j/j.%>$%$+.8&j"9\'hx'
b"k.kk/(%9$;#k?$$k2%*-k.k/$?%$%*/9'k#8&iy"
b'd!dd \'*6+4,d0++d=*%"d!d +0*+*% 6(d,7)fv'
b'e ee!&+7*5-e1**e<+$#e e!*1+*+$!7)e-6(gw'
b'f#ff"%(4)6.f2))f?(\' f#f")2()(\'"4*f.5+dt'
b'g"gg#$)5(7/g3((g>)&!g"g#(3)()+g/4*eu'
b'`%``$#.2/0(`4//`9.!&`%`$/4./.!$2,`(3-br'
b'a$aa%"/3.1)a5..a8/ \'a$a%.5/./ %3-a)2,cs'
b"b'bb&!,0-2*b6--b;,#$b'b&-6,-,#&0.b*1/`p"
b'c&cc\' -1,3+c7,,c:-"%c&c\',7-,-"\'1/c+0.aq'
b'|9||8?2.3,4|(33|%2=:|9|83(232=8.0|4/1~n'
b"~;~~:=0,1.6~*11~'0?8~;~:1*010?:,2~6-3|l"
b'x=xx<;6*7(0x,77x!69>x=x<7,6769<*4x0+5zj'
b'y<yy=:7+6)1y-66y 78?y<y=6-7678=+5y1*4{k'
b'z?zz>94(5*2z.55z#4;<z?z>5.454;>(6z2)7xh'
b'{>{{?85)4+3{/44{"5:={>{?4/545:?)7{3(6yi'
b"t1tt07:&;$<t ;;t-:52t1t0; :;:50&8t<'9vf"
b"u0uu16;':%=u!::u,;43u0u1:!;:;41'9u=&8wg"
b'v3vv258$9&>v"99v/870v3v29"89872$:v>%;td'
b"w2ww349%8'?w#88w.961w2w38#98963%;w?$:ue"
b'p5pp43>"? 8p$??p)>16p5p4?$>?>14"<p8#=rb'
b'q4qq52?#>!9q%>>q(?07q4q5>%?>?05#=q9"<sc'
b'r7rr61< =":r&==r+<34r7r6=&<=<36 >r:!?p`'
b"s6ss70=!<#;s'<<s*=25s6s7<'=<=27!?s; >qa"
b'.k..jm`|a~f.zaa.w`oh.k.jaz`a`oj|b.f}c,<'
b'(m((lkfzgx`(|gg(qfin(m(lg|fgfilzd(`{e*:'
b')l))mjg{fya)}ff)pgho)l)mf}gfghm{e)azd+;'
b'*o**nidxezb*~ee*sdkl*o*ne~dedknxf*byg(8'
b'$a$$`gjvktl$pkk$}jeb$a$`kpjkje`vh$lwi&6'
b"%`%%afkwjum%qjj%|kdc%`%ajqkjkdawi%mvh'7"
b"'b''cdiuhwo'shh'~ifa'b'chsihifcuk'otj%5"
b' e dcnroph too ynaf e dotnonadrl hsm"2'
b'!d!!ebosnqi!unn!xo`g!d!enuono`esm!irl#3'
b'"g""falpmrj"vmm"{lcd"g"fmvlmlcfpn"jqo 0'
b'#f##g`mqlsk#wll#zmbe#f#glwmlmbgqo#kpn!1'
b'=x==y~sormu=irr=ds|{=x=yrisrs|yoq=unp?/'
b'?z??{|qmpow?kpp?fq~y?z?{pkqpq~{ms?wlr=-'
b'8}88|{vjwhp8lww8avy~8}8|wlvwvy|jt8pku:*'
b'4q44pwzf{d|4`{{4mzur4q4p{`z{zupfx4|gy6&'
b"5p55qv{gze}5azz5l{ts5p5qza{z{tqgy5}fx7'"
b'6s66ruxdyf~6byy6oxwp6s6rybxyxwrdz6~e{4$'
b'<*97x4<7?7100/5>x96x,95t>xx5x41!x*9x&2d'
b"=+86y5=6>6011.4?y87y-84u?yy4y50 y+8y'3e"
b'>(;5z6>5=5322-7<z;4z.;7v<zz7z63#z(;z$0f'
b'?):4{7?4<4233,6={:5{/:6w={{6{72"{):{%1g'
b'8.=3|083;3544+1:|=2|(=1p:||1|05%|.=|"6`'
b'9/<2}192:2455*0;}<3})<0q;}}0}14$}/<}#7a'
b":,?1~2:191766)38~?0~*?3r8~~3~27'~,?~ 4b"
b'4"1?p<4?7?988\'=6p1>p$1=|6pp=p<9)p"1p.:l'
b'5#0>q=5>6>899&<7q0?q%0<}7qq<q=8(q#0q/;m'
b'6 3=r>6=5=;::%?4r3<r&3?~4rr?r>;+r 3r,8n'
b'0&5;t80;3;=<<#92t5:t 59x2tt9t8=-t&5t*>h'
b'1\'4:u91:2:<=="83u4;u!48y3uu8u9<,u\'4u+?i'
b'2$79v:2919?>>!;0v78v"7;z0vv;v:?/v$7v(<j'
b'3%68w;3808>?? :1w69w#6:{1ww:w;>.w%6w)=k'
b',:)\'h$,\'/\'! ?%.h)&h<)%d.hh%h$!1h:)h6"t'
b"-;(&i%-&.& !!>$/i('i=($e/ii$i% 0i;(i7#u"
b'.8+%j&.%-%#""=\',j+$j>+\'f,jj\'jj8+j4 v'
b'/9*$k\'/$,$"##<&-k*%k?*&g-kk&k\'"2k9*k5!w'
b'(>-#l (#+#%$$;!*l-"l8-!`*ll!l %5l>-l2&p'
b')?,"m!)"*"$%%: +m,#m9, a+mm m!$4m?,m3\'q'
b'*</!n"*!)!\'&&9#(n/ n:/#b(nn#n"\'7n</n0$r'
b'+=. o#+ ( &\'\'8")o.!o;."c)oo"o#&6o=.o1%s'
b"$2!/`,$/'/)((7-&`!.`4!-l&``-`,)9`2!`>*|"
b"%3 .a-%.&.())6,'a /a5 ,m'aa,a-(8a3 a?+}"
b'&0#-b.&-%-+**5/$b#,b6#/n$bb/b.+;b0#b<(~'
b' 6%+d( +#+-,,3)"d%*d0%)h"dd)d(-=d6%d:.x'
b'!7$*e)!*"*,--2(#e$+e1$(i#ee(e),<e7$e;/y'
b'"4\')f*")!)/..1+ f\'(f2\'+j ff+f*/?f4\'f8,z'
b'#5&(g+#( (.//0*!g&)g3&*k!gg*g+.>g5&g9-{'
b'~h{u:v~u}usrrmw|:{t:n{w6|::w:vsc:h{:dp&'
b'xn}s<pxs{suttkqz<}r<h}q0z<<q<pue<n}<bv '
b'yo|r=qyrzrtuujp{=|s=i|p1{==p=qtd=o|=cw!'
b'{m~p?s{pxpvwwhry?~q?k~r3y??r?svf?m~?au#'
b'pfu{4xp{s{}||cyr4uz4`uy8r44y4x}m4fu4j~('
b'm{hf)emfnf`aa~do)hg)}hd%o))d)e`p){h)wc5'
b'nxke*fnemecbb}gl*kd*~kg&l**g*fcs*xk*t`6'
b'h~mc,`hckcedd{aj,mb,xma j,,a,`eu,~m,rf0'
b'j|oa.bjaiagffych.o`.zoc"h..c.bgw.|o.pd2'
b'k}n`/ck`h`fggxbi/na/{nb#i//b/cfv/}n/qe3'
b'drao ldogoihhwmf an tam,f m liy ra ~j<'
b'fpcm"nfmemkjjuod"cl"vco.d""o"nk{"pc"|h>'
b'gqbl#ogldljkktne#bm#wbn/e##n#ojz#qb#}i?'
b'`vek$h`kckmllsib$ej$pei(b$$i$hm}$ve$zn8'
b'awdj%iajbjlmmrhc%dk%qdh)c%%h%il|%wd%{o9'
b"cufh'kch`hnoopja'fi'sfj+a''j'kn~'uf'ym;"
b"eebh*bbmdIi$o&|**ds*y~klcekxgbcee*o'oWG"
b'gg`j(``ofKk&m$~((fq({|inagize`agg(m%mUE'
b'``gm/gghaLl!j#y//av/|{nif`n}bgf``/j"jRB'
b'aafl.ffi`Mm k"x..`w.}zohgao|cfgaa.k#kSC'
b'llka#kkdm@`-f/u##mz#pwbejlbqnkjll#f.f^N'
b'mmj`"jjelAa,g.t""l{"qvcdkmcpojkmm"g/g_O'
b'nnic!iifoBb/d-w!!ox!ru`ghn`slihnn!d,d\\L'
b'oohb hhgnCc.e,v ny stafioarmhioo e-e]M'
b"hhoe'oo`iDd)b+q''i~'tsfanhfujonhh'b*bZJ"
b'jjmg%mmbkFf+`)s%%k|%vqdcljdwhmljj%`(`XH'
b'kklf$llcjGg*a(r$$j}$wpebmkevilmkk$a)aYI'
b'ttsy;ss|uXx5~7m;;ub;hoz}rtzivsrtt;~6~FV'
b'ppw}?wwxq\\|1z3i??qf?lk~yvp~mrwvpp?z2zBR'
b'||{q3{{t}Pp=v?e33}j3`gruz|ra~{z||3v>vN^'
b'zz}w5}}r{Vv;p9c55{l5fats|ztgx}|zz5p8pHX'
b'{{|v4||szWw:q8b44zm4g`ur}{ufy|}{{4q9qIY'
b'ii(<*, \'(!!iii,:--i"ii?e%/-i (=\';,0\'fyw'
b'hh)=+-!&) hhh-;,,h#hh>d$.,h!)<&:-1&gxv'
b'kk*>(."%*##kkk.8//k kk=g\'-/k"*?%9.2%d{u'
b'jj+?)/#$+""jjj/9..j!jj<f&,.j#+>$8/3$ezt'
b'mm,8.($#,%%mmm(>))m&mm;a!+)m$,9#?(4#b}s'
b'll-9/)%"-$$lll)?((l\'ll:` *(l%-8">)5"c|r'
b'nn/;-+\' /&&nnn+=**n%nn8b"(*n\'/: <+7 a~p'
b'``!5#%).!((```%3$$`+``6l,&$`)!4.2%9.op~'
b'cc"6 &*-"++ccc&0\'\'c(cc5o/%\'c*"7-1&:-ls}'
b"bb#7!'+,#**bbb'1&&b)bb4n.$&b+#6,0';,mr|"
b'ee$0& ,+$--eee 6!!e.ee3i)#!e,$1+7 <+ju{'
b'dd%1\'!-*%,,ddd!7 d/dd2h(" d-%0*6!=*ktz'
b'gg&2$".)&//ggg"4##g,gg1k+!#g.&3)5">)hwy'
b'ff\'3%#/(\'..fff#5""f-ff0j* "f/\'2(4#?(ivx'
b'yy8,:<07811yyy<*==y2yy/u5?=y08-7+< 7vig'
b'xx9-;=16900xxx=+<<x3xx.t4><x19,6*=!6whf'
b'{{:.8>25:33{{{>(??{0{{-w7=?{2:/5)>"5tke'
b'zz;/9?34;22zzz?)>>z1zz,v6<>z3;.4(?#4ujd'
b'}}<(>843<55}}}8.99}6}}+q1;9}4<)3/8$3rmc'
b'||=)?952=44|||9/88|7||*p0:8|5=(2.9%2slb'
b"~~?+=;70?66~~~;-::~5~~(r28:~7?*0,;'0qn`"
b'qq0$248?099qqq4"55q:qq\'}=75q80%?#4(?~ao'
b"rr3'17;<3::rrr7!66r9rr$~>46r;3&< 7+<}bl"
b"uu4 60<;4==uuu0&11u>uu#y931u<4!;'0,;zek"
b'tt5!71=:5<<ttt1\'00t?tt"x820t=5 :&1-:{dj'
b'ww6"42>96??www2$33w<ww!{;13w>6#9%2.9xgi'
b'vv7#53?87>>vvv3%22v=vv z:02v?7"8$3/8yfh'
b"((i}kmafi``(((m{ll(c((~$dnl(ai|fzmqf'86"
b'//nzljfangg///j|kk/d//y#cik/fn{a}jva ?1'
b'..o{mkg`off...k}jj.e..x"bhj.goz`|kw`!>0'
b'!!`tbdho`ii!!!dree!j!!w-mge!h`uosdxo.1?'
b' auceinahh esdd k v,lfd iatnreyn/0>'
b'##bv`fjmbkk###fpgg#h##u/oeg#jbwmqfzm,3='
b'""cwagklcjj"""gqff"i""t.ndf"kcvlpg{l-2<'
b'%%dpf`lkdmm%%%`vaa%n%%s)ica%ldqkw`|k*5;'
b'$$eqgamjell$$$aw``$o$$r(hb`$mepjva}j+4:'
b"''frdbnifoo'''btcc'l''q+kac'nfsiub~i(79"
b'88ym{}qvypp888}k||8s88n4t~|8qylvj}av7(&'
b'==|h~xts|uu===xnyy=v==k1q{y=t|isoxds2-#'
b'??~j|zvq~ww???zl{{?t??i3sy{?v~kqmzfq0/!'
b'00qesuy~qxx000uctt0{00f<|vt0yqd~bui~? .'
b'22sgqw{|szz222wavv2y22d>~tv2{sf|`wk|=",'
b'55t`vp|{t}}555pfqq5~55c9ysq5|ta{gpl{:%+'
b',*:<"n++#/!n===/+o: >/+nnD+/" b++#n/&\r!'
b'-+;=#o**". o<<<.*n;!?.*ooE*.#!c**"o.\'\x0c '
b'(.>8&j//\'+%j999+/k>$:+/jj@/+&$f//\'j+"\t%'
b"*,<:$h--%)'h;;;)-i<&8)-hhB-)$&d--%h) \x0b'"
b"+-=;%i,,$(&i:::(,h='9(,iiC,(%'e,,$i(!\n&"
b"dbrtj&cckgi&uuugc'rhvgc&&\x0ccgjh*cck&gnEi"
b"ecsuk'bbjfh'tttfb&siwfb''\rbfki+bbj'foDh"
b'agwqo#ffnbl#pppbf"wmsbf##\tfbom/ffn#bk@l'
b'bdtrl eemao sssae!tnpae \nealn,eem ahCo'
b'ceusm!ddl`n!rrr`d uoq`d!!\x0bd`mo-ddl!`iBn'
b"bh)nbracbit'wb'af'ohf''sfn)ibc'''wpn/WG"
b'ci(ocs`bchu&vc&`g&nig&&rgo(hcb&&&vqo.VF'
b'`j+l`pca`kv%u`%cd%mjd%%qdl+k`a%%%url-UE'
b'ak*maqb`ajw$ta$be$lke$$pem*ja`$$$tsm,TD'
b'fl-jfvegfmp#sf#eb#klb##wbj-mfg###stj+SC'
b'gm,kgwdfglq"rg"dc"jmc""vck,lgf"""ruk*RB'
b'dn/hdtgedor!qd!g`!in`!!u`h/ode!!!qvh)QA'
b'eo.ieufdens pe fa hoa tai.ned pwi(P@'
b'ka gk{hjk`}.~k.ho.fao..zog `kj...~yg&^N'
b'hb#dhxkihc~-}h-kl-ebl--yld#chi---}zd%]M'
b"lf'`l|omlgz)yl)oh)afh))}h`'glm)))y~`!YI"
b'pz;|p`sqp{f5ep5st5}zt55at|;{pq555eb|=EU'
b'q{:}qarpqzg4dq4ru4|{u44`u}:zqp444dc}<DT'
b'v|=zvfuwv}`3cv3ur3{|r33grz=}vw333cdz;CS'
b'w}<{wgtvw|a2bw2ts2z}s22fs{<|wv222be{:BR'
b'zp1vzjy{zql?oz?y~?wp~??k~v1qz{???ohv7O_'
b'xr3txh{yxsn=mx={|=ur|==i|t3sxy===mjt5M]'
b'ys2uyizxyro<ly<z}<ts}<<h}u2ryx<<<lku4L\\'
b'}w6q}m~|}vk8h}8~y8pwy88lyq6v}|888hoq0HX'
b"`rSfui'''`bSnw~bcNfpucehtuEd<thawofkd~="
b'bpQdwk%%%b`Qlu|`aLdrwagjvwGf>vjcumdif|?'
b'cqPevj$$$caPmt}a`Mesv`fkwvFg?wkbtlehg}>'
b'dvWbqm###dfWjszfgJbtqgalpqA`8pleskbo`z9'
b'ewVcpl"""egVkr{gfKcupf`mqp@a9qmdrjcna{8'
b'ftU`so!!!fdUhqxdeH`vsecnrsCb:rngqi`mbx;'
b'guTarn geTipyedIawrdbosrBc;sofphalcy:'
b'i{Zo|`...ikZg~wkjGoy|jla}|Lm5}ah~fobmw4'
b'l~_jye+++ln_b{rnoBj|yoidxyIh0xdm{cjghr1'
b'n|]h{g)))nl]`yplm@h~{mkfz{Kj2zfoyahejp3'
b'r`Atg{555rpA|elpq\\tbgqwzfgWv.fzse}tyvl/'
b'sa@ufz444sq@}dmqp]ucfpv{gfVw/g{rd|uxwm.'
b'ugFs`|222uwF{bkwv[se`vp}a`Pq)a}tbzs~qk('
b'xjK~mq???xzKvofz{V~hm{}plm]|$lpyow~s|f%'
b'.%/+ #*.g"g/5&(igg3+3("gg(2"gg5("&4i6ux'
b'/$.*!"+/f#f.4\')hff2*2)#ff)3#ff4)#\'5h7ty'
b',\'-)"!(,e e-7$*kee1)1* ee*0 ee7* $6k4wz'
b'-&,(# )-d!d,6%+jdd0(0+!dd+1!dd6+!%7j5v{'
b'*!+/$\'.*c&c+1",mcc7/7,&cc,6&cc1,&"0m2q|'
b"+ *.%&/+b'b*0#-lbb6.6-'bb-7'bb0-'#1l3p}"
b'(#)-&%,(a$a)3 .oaa5-5.$aa.4$aa3.$ 2o0s~'
b'&-\'#(+"&o*o\'=. aoo;#; *oo :*oo= *.<a>}p'
b'\',&")*#\'n+n&</!`nn:":!+nn!;+nn<!+/=`?|q'
b'%.$ +(!%l)l$>-#bll8 8#)ll#9)ll>#)-?b=~s'
b'")#\',/&"k.k#9*$ekk?\'?$.kk$>.kk9$.*8e:yt'
b'#("&-.\'#j/j"8+%djj>&>%/jj%?/jj8%/+9d;xu'
b' +!%.-$ i,i!;(&gii=%=&,ii&<,ii;&,(:g8{v'
b"!* $/,%!h-h :)'fhh<$<'-hh'=-hh:'-);f9zw"
b'>5?;03:>w2w?%68yww#;#82ww8"2ww%826$y&eh'
b'?4>:12;?v3v>$79xvv":"93vv9#3vv$937%x\'di'
b"<7=9218<u0u='4:{uu!9!:0uu: 0uu':04&{$gj"
b"=6<8309=t1t<&5;ztt 8 ;1tt;!1tt&;15'z%fk"
b':1;?47>:s6s;!2<}ss\'?\'<6ss<&6ss!<62 }"al'
b";0:>56?;r7r: 3=|rr&>&=7rr='7rr =73!|#`m"
b'928<74=9p5p8"1?~pp$<$?5pp?%5pp"?51#~!bo'
b'7<629:37~;~6,?1p~~*2*1;~~1+;~~,1;?-p/la'
b'4?51:904}8}5/<2s}})1)28}}2(8}}/28<.s,ob'
b'5>40;815|9|4.=3r||(0(39||3)9||.39=/r-nc'
b'2937<?62{>{3):4u{{/7/4>{{4.>{{)4>:(u*id'
b'3826=>73z?z2(;5tzz.6.5?zz5/?zz(5?;)t+he'
b'0;15>=40y<y1+86wyy-5-6<yy6,<yy+6<8*w(kf'
b'1:04?<51x=x0*97vxx,4,7=xx7-=xx*7=9+v)jg'
b"neok`cjn'b'oufh)''skshb''hrb''uhbft)v58"
b'odnjabko&c&ntgi(&&rjric&&isc&&ticgu(w49'
b'lgmibahl%`%mwdj+%%qiqj`%%jp`%%wj`dv+t7:'
b'mflhc`im$a$lvek*$$phpka$$kqa$$vkaew*u6;'
b'jakodgnj#f#kqbl-##wowlf##lvf##qlfbp-r1<'
b'k`jnefok"g"jpcm,""vnvmg""mwg""pmgcq,s0='
b'hcimfelh!d!is`n/!!umund!!ntd!!snd`r/p3>'
b'ibhlgdmi e hrao. tltoe oue roeas.q2?'
b'fmgchkbf/j/g}n`!//{c{`j//`zj//}`jn|!~=0'
b"`kaenmd`)l)a{hf'))}e}fl))f|l)){flhz'x;6"
b'aj`dolea(m(`zig&((|d|gm((g}m((zgmi{&y:7'
b"|w}yrqx|5p5}gtz;55ayazp55z`p55gzptf;d'*"
b'}v|xspy}4q4|fu{:44`x`{q44{aq44f{qug:e&+'
b'xsy}vu|x1t1ycp~?11e}e~t11~dt11c~tpb?`#.'
b'v}wsx{rv?z?wm~p1??kskpz??pjz??mpz~l1n- '
b'u~tp{xqu<y<tn}s2<<hphsy<<siy<<nsy}o2m.#'
b'p{qu~}tp9|9qkxv799mumv|99vl|99kv|xj7h+&'
defaultdict(<class 'list'>, {0: [112, 114, 115, 116], 1: [96, 97, 103, 105, 107, 108, 109, 112, 113, 114, 115, 116, 117, 119, 122, 123, 125], 2: [96, 97, 98, 102, 103], 6: [115, 117, 119], 7: [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 55, 57, 59, 60, 61, 62, 63, 102, 103, 104, 105, 106, 107, 108, 109, 110, 112, 115, 116, 118, 123, 124, 125], 8: [58, 59, 61, 96, 97, 98, 99, 100, 102, 103, 105, 106, 107, 108, 109, 115, 116, 118, 119, 123, 124, 126, 127], 9: [96, 97, 98, 99, 100, 101, 102, 103, 105, 110, 111, 112, 115, 116, 117, 120, 121, 126], 10: [32, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 50, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 98, 100, 101, 102, 104, 105, 107, 108, 109, 110, 111, 113, 115, 116, 120, 121, 122], 11: [32, 33, 34, 35, 36, 37, 38, 40, 41, 42, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 60, 61, 62, 63, 98, 100, 101, 103, 108, 113, 114, 116, 118, 119, 120, 122, 123, 124, 125, 127], 18: [97, 99, 100, 101, 104, 105, 106, 107, 108, 110, 111, 112, 116, 120, 126, 127], 19: [32, 33, 34, 35, 36, 37, 39, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 55, 56, 59, 60, 61, 62, 63, 97, 102, 103, 104, 105, 106, 107, 108, 109, 110, 113, 116, 118, 121, 123, 124], 20: [32, 33, 36, 38, 39, 104, 105, 109, 110, 111], 21: [96, 97, 98, 99, 100, 101, 102, 103, 105, 106, 110, 114, 115, 116, 117, 120, 122, 123, 127], 22: [96, 98, 99, 100, 101, 102, 103, 105, 108, 110, 114, 115, 117, 120], 23: [32, 33, 34, 35, 36, 37, 38, 40, 41, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 55, 57, 58, 59, 60, 61, 62, 63, 96, 97, 98, 99, 100, 101, 102, 103, 104, 110, 111, 114, 115, 118, 120, 123, 126]})
Given the length of the message, one idea would be to score characters and spaces higher than symbols. I’ll make a new loop that scores the resulting character, giving a full point for characters and spaces, and a half point for symbols:
from string import ascii_letters, digits
key_len = 24
with open('xormaas/data', 'rb') as f:
data = f.read()
keys = []
for i in range(key_len):
group = bytes(data[j] for j in range(i, len(data), key_len))
best_score = 0
best_char = ''
for x in range(256):
score = 0
decrypted = bytes([b ^ x for b in group])
for b in decrypted:
if chr(b) in ascii_letters + ' ':
score += 1
elif chr(b) in digits + '.,!?:\n':
score += 0.5
if score > best_score:
best_score = score
best_char = chr(x)
keys.append(best_char)
print(''.join(keys))
That makes a reasonable looking key:
oxdf@hacky$ uv run .\solve.py
statisticalxorbreakinggg
I’ll update the script to decrypt the full data block:
from string import ascii_letters, digits
from itertools import cycle
key_len = 24
with open('xormaas/data', 'rb') as f:
data = f.read()
keys = []
for i in range(key_len):
group = bytes(data[j] for j in range(i, len(data), key_len))
best_score = 0
best_char = ''
for x in range(256):
score = 0
decrypted = bytes([b ^ x for b in group])
for b in decrypted:
if chr(b) in ascii_letters + ' ':
score += 1
elif chr(b) in digits + '.,!?:\n':
score += 0.5
if score > best_score:
best_score = score
best_char = chr(x)
keys.append(best_char)
key = ''.join(keys)
print(f'Found key: {key}')
decrypted = bytes([d ^ ord(c) for d, c in zip(data, cycle(key))])
print(decrypted.decode('utf-8'))
This prints a nice block of mostly decrypted text:
oxdf@hacky$ uv run .\solve.py
Found key: statisticalxorbreakinggg
MARLEY was dead, to begin with. There is no doubt whatever about
that.The register of his burial was signed by the clergyman, the clerk,
the undertaker, and the chief mourner. Scrooge signed it.And
Scrooge’s name was good upon ’Change, for anything he chose to put
his hand to. The name of the three spirits, all lowercase, separated by commas gives you the code for the safe.
Old Marley was as dead as a door-nail.
Mind! I don’t mean to say that I know, of my own knowledge,
what there is particularly dead about a door-nail. I might have been
inclined, myself, to regard a coffin-nail as the deadest piece of
ironmongery in the trade.But the wisdom of our ancestors is in the
simile; and my unhallowed hands shall not disturb it, or the Country’s
done for. You will therefore permit me to repeat, emphatically, that
Marley was as dead as a door-nail.W}w &lbi0bm~cocefe/h(cq#$*4v/cev"j/j`<y]0CPy2[%\o&i@vtC2< :>B
]M>o@:?
For the most part, this passage is the start of Charle Dickens’ A Christmas Carol. But there’s a line added in “The name of the three spirits, all lowercase, separated by commas gives you the code for the safe.” There’s also a block of still encrypted-looking data at the end.
Decryption Round 2
The names of the three spirits are past, present, and yet to come. I’ll play with different ways to apply this to the stuff at the end, but it turns out that taking the raw data (without applying the first key) works:
from string import ascii_letters, digits
from itertools import cycle
key_len = 24
with open('xormaas/data', 'rb') as f:
data = f.read()
keys = []
for i in range(key_len):
group = bytes(data[j] for j in range(i, len(data), key_len))
best_score = 0
best_char = ''
for x in range(256):
score = 0
decrypted = bytes([b ^ x for b in group])
for b in decrypted:
if chr(b) in ascii_letters + ' ':
score += 1
elif chr(b) in digits + '.,!?:\n':
score += 0.5
if score > best_score:
best_score = score
best_char = chr(x)
keys.append(best_char)
key = ''.join(keys)
print(f'Found key: {key}')
decrypted = bytes([d ^ ord(c) for d, c in zip(data, cycle(key))])
key2 = 'past,present,yet to come'
search = b'that\nMarley was as dead as a door-nail.'
offset = decrypted.index(search) + len(search)
plaintext = bytes([r ^ ord(k) for r, k in zip(data, cycle(key2))])
print(plaintext[offset:].decode('utf-8'))
This prints a message with an encoded flag:
oxdf@hacky$ uv run .\solve.py
Found key: statisticalxorbreakinggg
The code for the safe is 1843,your flag:RlYyNXs0X0NocjFzdG00c19DNHIwbH0=
I’ll add a call to b64decode from base64 to get the flag:
print(b64decode(plaintext[offset:].split(b':')[1]).decode())
This gets the flag:
oxdf@hacky$ uv run .\solve.py
Found key: statisticalxorbreakinggg
The code for the safe is 1843,your flag:RlYyNXs0X0NocjFzdG00c19DNHIwbH0=
FV25{4_Chr1stm4s_C4r0l}
Flag: FV25{4_Chr1stm4s_C4r0l}
FV25.25
Challenge
Please leave some feedback for an extra gift: [Google Form URL] |
|
| Categories: |
|
| Level: | easy |
| Author: | Flagvent Team |
Solution
The link leads to a Google form about the event where players can leave feedback. At the end, it provides a link to https://flagvent.org/feedflag/:
The tree is spinning and the snowflakes are falling.
The page source has a script block that manages this, as well as a hint to where the hidden flags are throughout the event:
const hiddenFlagMapping = {
h1: '06',
h2: '10',
h3: '14',
h4: '21'
};
There’s also a flag in the tree flagpole:
Flag: FV25{thx_4_th3_f33db4ck}