zh3r0 CTF 2021: The Vault (Misc)

pepga just lost the password for her vault. Help her to get through it 👀

The Vault

Author - wh1t3r0se

This challenge provided us with a tarball. Upon unpacking it, we were presented with the following files.

❯ tree public/
public/
└── thevault.png

❯ file public/thevault.png
public/thevault.png: PNG image data, 160 x 205, 8-bit/color RGBA, non-interlaced

Upon opening thevault.png in an image viewer, we are presented with the following image.

Based on what’s written on the cartridge, it seems to be an image of a PICO-8 cartridge. Ok but what good is an image of a cartridge in helping us find the flag? Is this a steganography challenge? At this point we decided to run stegoVeritas on the image since it’s our go-to tool for steganography challenges.

Based on what we see above, it’s quite evident that there’s some LSB steganography shenanigans going on. However, extracting the two least significant bits didn’t yield us the flag. It was at this point that we decided to google "pico-8 steganography" and this led us to discover the P8PNGFileFormat.

Each PICO-8 byte is stored as the two least significant bits of each of the four color channels, ordered ARGB (E.g: the A channel stores the 2 most significant bits in the bytes). The image is 160 pixels wide and 205 pixels high, for a possible storage of 32,800 bytes. Of these, only the first 32,773 bytes are used.

- Pico-8 Wiki

The above excerpt explains our observations thus far, as it confirms that game data is stored in the two least significant bits of each of the four color channels. Upon further research we stumbled upon a blog post which provides an in-depth tutorial of how to extract said game data. Conveniently for us, the post also linked to a full-fledged decoder that we could use to extract out the p8 code.

-- the secret vault
-- wh1t3r0se

-- global+main ----------------

ins=[[
** welcome to the vault **





by wh1t3r0se
]]

asci="\1\2\3\4\5\6\7\8\9\10\11\12\13\14\15\16\17\18\19\20\21\22\23\24\25\26\27\28\29\30\31\32\33\34\35\36\37\38\39\40\41\42\43\44\45\46\47\48\49\50\51\52\53\54\55\56\57\58\59\60\61\62\63\64\65\66\67\68\69\70\71\72\73\74\75\76\77\78\79\80\81\82\83\84\85\86\87\88\89\90\91\92\93\94\95\96\97\98\99\100\101\102\103\104\105\106\107\108\109\110\111\112\113\114\115\116\117\118\119\120\121\122\123\124\125\126\127\128\129\130\131\132\133\134\135\136\137\138\139\140\141\142\143\144\145\146\147\148\149\150\151\152\153\154\155\156\157\158\159\160\161\162\163\164\165\166\167\168\169\170\171\172\173\174\175\176\177\178\179\180\181\182\183\184\185\186\187\188\189\190\191\192\193\194\195\196\197\198\199\200\201\202\203\204\205\206\207\208\209\210\211\212\213\214\215\216\217\218\219\220\221\222\223\224\225\226\227\228\229\230\231\232\233\234\235\236\237\238\239\240\241\242\243\244\245\246\247\248\249\250\251\252\253\254\255"

-- main program ---------------
function main()

    music(0)
    cls()
    print(ins,8,8,31)
    spr(2,48,20)
    spr(3,56,20)
    spr(18,48,28)
    spr(19,56,28)
    --key
    spr(32,66,18)
    spr(33,74,18)
    spr(48,66,26)
    spr(49,74,26)
    --lock

    poke(24365,1) -- mouse+key kit

    t=""
    print("type in some text:",28,100,11)
    repeat
        grect(0,108,128,5)
        print(t,64-len(t)*2,108,6)
        grect(64+len(t)*2,108,3,5,8)
        flip()
        grect(64+len(t)*2,108,3,5,0)
        if stat(30)==true then
            c=stat(31)
            if c>=" " and c<="}" then
                t=t..c
            elseif c=="\8" then
                t=fnd(t)
            elseif c=="\13" then
                cls()
                print("got you something:",30,50,7)
                print(amugeh(t),30,62,12)
                stop()
            end
        end
    until c=="\27"

end

-->8
-- functions ------------------


function grect(h,v,x,y,c)
    rectfill(h,v,h+x-1,v+y-1,c)
end

function isprime(n)
    if n == 1 then

        return false
    end
    for i = 2, n^(1/2) do
        if (n % i) == 0 then
            return false
        end
    end
    return true
end

function fnd(a)
    return sub(a,1,#a-1)
end

function encrypt(t, k)

    return chr(asc(t) + k)

end

function check(t)
    flag="congrats! u got it"

    secret   = chr(105)
    secret ..= chr(107)
    secret ..= chr(107)
    secret ..= chr(86)
    secret ..= chr(110)
    secret ..= chr(43)
    secret ..= chr(126)
    secret ..= chr(86)
    secret ..= chr(99)
    secret ..= chr(92)
    secret ..= chr(107)
    secret ..= chr(46)

    if #secret == #t then

        cunt=1
        good=true
        while (cunt<#t+1 and good) do
            if sub(t,cunt,cunt) == sub(secret,cunt,cunt) then
                cunt+=1
            else
                return "this time try real hard:/"
            end
        end

        return flag
    end

    return "1 year old baby can do this bruhh!"

end

function len(a)
    return #a
end

function encript(t,k)
    return chr(asc(t) - k )
end

function key()
    x =0
    x+=6
    x*=9
    x+=6
    x+=9
    x\=10
    return x
end

function instr(a,b)
    local r=0
    if (a==null or a=="") return 0
        if (b==null or b=="") return 0
            for i=1,#a-#b+1 do
                if sub(a,i,i+#b-1)==b then
                    r=i
                    return r
                end
            end
            return 0
        end

        function ki()
            x =0
            x+=6
            x*=9
            x+=6
            x+=9
            x%=10

            return 9

        end

        function amugeh(renbow)

            lmao=""
            local str = sub(renbow,0,6)
            if ( str == "zh3r0{" and sub(renbow,#renbow,#renbow) == "}") then

                hk_noob=1

                repeat
                    print("")
                    if (isprime(hk_noob)) == true then
                        lmao = lmao..(encrypt(sub(renbow,6+hk_noob,6+hk_noob),key()))

                    else
                        lmao = lmao..(encript(sub(renbow,6+hk_noob,6+hk_noob),ki()))
                    end

                    hk_noob += 1
                until hk_noob> (#renbow-7)
                return check(lmao)
            end

            return "try harder !"
        end

        function asc(a)
            return instr(asci,a)
        end

        main()

Running the decoder on the cartridge file provided to us yielded the code above. In summary, our input undergoes a couple of transformations before it is eventually compared against a secret string. Unfortunately (for me) the p8 language seems to based on lua which i’m not the biggest fan of, so I decided to re-implement the code in python to compute the flag.

def amugeh(inputt):
    lmao = ""

    assert inputt[:6] == "zh3r0{"
    assert inputt[-1] == "}"

    hk_noob = 1

    while True:
        if isprime(hk_noob):
            lmao += encrypt(inputt[6 + hk_noob], key())
        else:
            lmao += encript(inputt[6 + hk_noob], ki())
        hk_noob += 1

        if hk_noob > (len(inputt) - 7):
            break

    return check(lmao)


def check(t):
    secret = chr(105)
    secret += chr(107)
    secret += chr(107)
    secret += chr(86)
    secret += chr(110)
    secret += chr(43)
    secret += chr(126)
    secret += chr(86)
    secret += chr(99)
    secret += chr(92)
    secret += chr(107)
    secret += chr(46)

    assert len(t) == len(secret)

    return secret == t


def isprime(n):
    if n <= 1 or n % 1 > 0:
        return False
    for i in range(2, n // 2):
        if n % i == 0:
            return False
    return True


def encript(x, y):
    return chr(ord(x) - y)


def encrypt(x, y):
    return chr(ord(x) + y)


def key():
    return 6


def ki():
    return 9


secret = chr(105)
secret += chr(107)
secret += chr(107)
secret += chr(86)
secret += chr(110)
secret += chr(43)
secret += chr(126)
secret += chr(86)
secret += chr(99)
secret += chr(92)
secret += chr(107)
secret += chr(46)

flag = "zh3r0{"

for i, j in enumerate(secret):
    if isprime(i + 1):
        flag += encrypt(j, -key())
    else:
        flag += encript(j, -ki())

flag += "}"

print(f"Flag: {flag}")

Running the above script yields us the following.

❯ python xpl.py
Flag: zh3r0{reePh4x_lee7}

Trying to submit zh3r0{reePh4x_lee7} results in an incorrect flag, but at this point we can pretty much guess that the correct flag should be zh3r0{ree_h4x_lee7}.

Flag: zh3r0{ree_h4x_lee7}