The challenge
This is a web challenge involving javascript, meaning most of the solution is going to be client side. We are asked to visit the challenge page.
From here, we can view the source code of the page.
<html>
<head>
<script src="jquery-3.3.1.min.js"></script>
<script>
var bytes = [];
$.get("bytes", function(resp) {
bytes = Array.from(resp.split(" "), x => Number(x));
});
function assemble_png(u_in){
var LEN = 16;
var key = "00000000000000000000000000000000";
var shifter;
if(u_in.length == key.length){
key = u_in;
}
var result = [];
for(var i = 0; i < LEN; i++){
shifter = Number(key.slice((i*2),(i*2)+1));
for(var j = 0; j < (bytes.length / LEN); j ++){
result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
}
}
while(result[result.length-1] == 0){
result = result.slice(0,result.length-1);
}
document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
return false;
}
</script>
</head>
<body>
<center>
<form action="#" onsubmit="assemble_png(document.getElementById('user_in').value)">
<input type="text" id="user_in">
<input type="submit" value="Submit">
</form>
<img id="Area" src=""/>
</center>
</body>
</html>
Let’s break it down. We are going to begin with the contents in the script
tags. First, the script fetches a blob of whitespace separated numbers into the
variable bytes
.
var bytes = [];
$.get("bytes", function(resp) {
bytes = Array.from(resp.split(" "), x => Number(x));
});
It will be a good idea to download a copy of these bytes for ourselves.
wget http://jupiter.challenges.picoctf.org:42899/bytes
The function assemble_png
takes a 32 characters long key as an input, as is evident from the length of the variable key and the assignment of u_in
to key only when their lengths match.
var LEN = 16;
var key = "00000000000000000000000000000000";
var shifter;
if(u_in.length == key.length){
key = u_in;
}
The function then iterates over the key to store every other byte into the shifter
.
shifter = Number(key.slice((i*2),(i*2)+1));
The inner loop then fills up 16 contiguous bytes of the result
array from the index j * LEN
by a table lookup into the bytes
array initialized earlier.
for(var j = 0; j < (bytes.length / LEN); j ++) {
result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
}
Solution
Let’s write a python script to automate searching for the key. We can narrow down our key space since a PNG file has its header (first set of bytes) as \x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR
.
We will use numpy to broadcast arithmetic operations over all elements of a tensor (rank 1 here, meaning an array). Since the key has one byte from the PNG header and another unknown byte
alternatively, we can begin by using a dummy byte like ‘A’ for all those spaces.
We then try to generate an image from the resultant byte array and validate it with the python image library (PIL). If the validation succeeds, we can end the search and save the image.
import itertools
from itsdangerous import base64_encode
from PIL import Image, UnidentifiedImageError
import numpy as np
import io
LEN = 16
with open('bytes') as handle:
blob = np.array([int(x.strip()) for x in handle.read().split(',')])
BLEN = len(blob)
J = BLEN // LEN
crib = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
charset = [set()] * LEN
def is_png(key) -> bool:
result = bytearray(BLEN)
for i in range(LEN):
shifter = ord(key[i * 2])
for j in range(J):
result[(j * LEN) + i] = blob[(((j + shifter) * LEN) % BLEN) + i]
result.rstrip(b'\x00')
try:
image = Image.open(io.BytesIO(result))
image.save(base64_encode(key).decode() + ".png")
except UnidentifiedImageError:
return False
return True
def main():
# 255 is the maximum value for a u8
shifter = np.arange(256)
for i in range(LEN):
for j in range(J):
crib_index = (j * LEN) + i
if crib_index >= len(crib):
continue
interp = (((shifter + j) * LEN) % BLEN) + i
p = shifter[np.in1d(interp, np.where(blob == crib[crib_index])[0])]
charset[i] = charset[i].union(p)
for char in itertools.product(*charset):
key = "A".join(map(chr, char))
if is_png(key):
print(key)
return
if __name__ == "__main__":
main()
Running this script, we get the following image in B0EGQQFBBkEAQQdBw6BBAUEFQQBBAEEAQQJBAEEIQQU.png
.
Since this appears to be a QR code, the only thing left to do is scan the image with a tool like zbarimg
.
zbarimg B0EGQQFBBkEAQQdBw6BBAUEFQQBBAEEAQQJBAEEIQQU.png
This yields us the flag.
QR-Code:picoCTF{227c2d3465a6a4bcc8a1bc599e34f074}
scanned 1 barcode symbols from 1 images in 0.03 seconds