pepga just lost the password for her vault. Help her to get through it đź‘€
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}