FV25.01

Challenge

FV25.01 - welcome

Welcome to Flagvent

Categories: funFUN
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:

image-20251201184503378

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:

image-20251201185051308

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:

image-20251201214049139

And then filling in the numbers that fill a square:

image-20251201214121274

If I add a rule at the top that just turns everything that has any value black, this is a QR Code:

image-20251201214156333

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

FV25.02 - Lowkey Sleigh

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: reverse_engineeringREVERSE_ENGINEERING
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:

image-20251202195219538

The example on the readme looks just like this code:

image-20251202195335027

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

FV25.03 - Loop-de-Link

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: linuxLINUX
Level: easy
Author: 0xdf
Spawnable Instance: SSHSSH

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

FV25.04 - our house

The damn Grinch messed around again with Santas files! And now he can’t watch his favorite video. Can you help me?

Categories: miscMISC
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:

image-20251205122135939

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:

image-20251205122611025

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:

image-20251205163247802

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:

image-20251205163401389

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

FV25.22 - The crib in the genome

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: cryptoCRYPTO
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

FV25.23 - Santa's Tune

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: funFUN
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:

image-20251226192402586

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:

image-20251226193208287

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

FV25.24 - XorMASS

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: cryptoCRYPTO
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)()&#5+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\'j&#3j8+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

FV25.25 - Feedback

Please leave some feedback for an extra gift:

[Google Form URL]

Categories: miscMISC
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/:

image-20251228182506012

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:

alt text

Flag: FV25{thx_4_th3_f33db4ck}