EASYCTF - Pixelly

description: I’ve created a new ASCII art generator, and it works beautifully! But I’m worried that someone might have put a backdoor in it. Maybe you should check out the source for me…

category: Reverse Engineering

Here is the source code given with the challenge:

#!/usr/bin/env python3
# Modified from https://gist.github.com/cdiener/10491632

import sys
from PIL import Image
import numpy as np

# it's me flage!
flag = '<redacted>'

# settings
chars = np.asarray(list(' -"~rc()+=01exh%'))
SC, GCF, WCF = 1/10, 1, 7/4

# read file
img = Image.open(sys.argv[1])

# process
S = ( round(img.size[0]*SC*WCF), round(img.size[1]*SC) )
img = np.sum( np.asarray( img.resize(S) ), axis=2)
img -= img.min()
img = (1.0 - img/img.max())**GCF*(chars.size-1)

arr = chars[img.astype(int)]
arr = '\n'.join(''.join(row) for row in arr)
print(arr)

# hehehe
try:
	eval(arr)
except SyntaxError:
	pass

Obviously we know that we’re going to exloit the eval call to print the flag. Let’s have a look to the program core:

It loads the image given as argument

img = Image.open(sys.argv[1])

It computes some operations on pixels

# Resize the image
S = ( round(img.size[0]*SC*WCF), round(img.size[1]*SC) )
img = np.sum( np.asarray( img.resize(S) ), axis=2)

# Substract the min to all values
img -= img.min()

# After this line, minimum will be at 15 and maximum will be at zero
# All values between min and max will be between 0 and 15 proportionnaly
img = (1.0 - img/img.max())**GCF*(chars.size-1)

After thoses lines, we have numbers between 0 and 15. This step will convert those numbers to char in the ‘chars’ variable:

arr = chars[img.astype(int)]
arr = '\n'.join(''.join(row) for row in arr)

Then this variable ‘arr’ will be given to eval.

Now we have to find a good payload that will print the flag using only characters in the var ‘chars’. We can simply test it using this code:

flag = '<redacted>'
arr = "PAYLOAD"
eval(arr)

I tried:

flag = '<redacted>'
# chr(112)+chr(114)+chr(105)+chr(110)+chr(116)+chr(40)+chr(102)+chr(108)+chr(97)+chr(103)+chr(41) -> print(flag)
arr = "exec(chr(112)+chr(114)+chr(105)+chr(110)+chr(116)+chr(40)+chr(102)+chr(108)+chr(97)+chr(103)+chr(41))"
eval(arr)
# -> <redacted>

Good ! We have to create an image that will be tranlated by exec(chr(112)+chr(114)+chr(105)+chr(110)+chr(116)+chr(40)+chr(102)+chr(108)+chr(97)+chr(103)+chr(41))

I’ll save you some time, don’t try to write on the image on multiple lines, for me it didn’t work, so the best way is to do it on one line.

We can generate the image using PIL:

from PIL import Image

# Create the image, and init it in white
img = Image.new('RGB', (256*20 + 95,8), "white")
pixels = img.load()

# Because of the resize, we have to write on multiple pixels
length_x = 6
length_y = 8

# Starting position: where we start to write on pixels (it will be incremented in each methods)
pos_x = 0
pos_y = 0

def move_pos():
	global pos_x
	global pos_y
	pos_x += length_x
	if pos_x + length_x >= img.size[0]:
		pos_x = 0

def zero():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (85,85,85)
	move_pos()

def pourcent():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (0,0,0)
	move_pos()

def un():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (60,60,60)
	move_pos()

def plus():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (115,115,115)
	move_pos()

def c():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (170,170,170)
	move_pos()

def h():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (1,1,1)
	move_pos()

def r():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (176,176,176)
	move_pos()

def p_f():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (130,130,130)
	move_pos()

def p_o():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (150,150,150)
	move_pos()

def x():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (34,34,34)
	move_pos()

def space():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (255,255,255)
	move_pos()

def e():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (50,50,50)
	move_pos()

def equal():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (90,90,90)
	move_pos()

img.save("test.png")

Those methods will print the specified characters. I would like to just call one by one every function to print the wanted strings, but the problem is that every 19 characters, a caracter is doubled. (because of the redimension)

You can test this by entering:

pourcent()
for i in range(100	):
	zero()
	plus()
img.save("test.png")

# Using the challenge program, we will have:
# %0+0+0+0+00+0+0+0+0+0+0+0+0+0+00+0+0+0+0+0+0+0+0+0+00+0+0+0+0+0+0+0+0+0+00+0+0+0+0+0+0+0+0+0+00+0+0+0+0+0+0+0+0+0+00+0+0+0+0+0+0+0+0+0++0+0+0+0+0+0+0+0+0+0++0+0+0+0+0+0+0+0+0+0++0+0+0+0+0+0+0+0+0+0++0+0+0+0+0+0+
# As you can see, some 0 are repeated twice... 

In the hurry I just make a very ugly code (don’t judge me) but it worked:

from PIL import Image

# Create the image, and init it in white
img = Image.new('RGB', (256*20 + 95,8), "white")
pixels = img.load()

# Because of the resize, we have to write on multiple pixels
length_x = 6
length_y = 8

# Starting position: where we start to write on pixels (it will be incremented in each methods)
pos_x = 0
pos_y = 0

def move_pos():
	global pos_x
	global pos_y
	pos_x += length_x
	if pos_x + length_x >= img.size[0]:
		pos_x = 0

def zero():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (85,85,85)
	move_pos()

def pourcent():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (0,0,0)
	move_pos()

def un():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (60,60,60)
	move_pos()

def plus():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (115,115,115)
	move_pos()

def c():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (170,170,170)
	move_pos()

def h():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (1,1,1)
	move_pos()

def r():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (176,176,176)
	move_pos()

def p_f():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (130,130,130)
	move_pos()

def p_o():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (150,150,150)
	move_pos()

def x():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (34,34,34)
	move_pos()

def space():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (255,255,255)
	move_pos()

def e():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (50,50,50)
	move_pos()

def equal():
	for i in range(pos_x, pos_x+length_x):
		for j in range(pos_y, pos_y+length_y):
			pixels[i,j] = (90,90,90)
	move_pos()

e()
x()
e()
c()
p_o()

#p
c()
h()
r()
p_o()
un()
plus()
un()
zero()
zero()
plus()
un()
plus()
un()
pourcent()
un()
p_f()

plus()

#r
c()
h()
r()
p_o()
un()
un()
un()
plus()
un()
plus()
un()
plus()
un()
p_f()

plus()

#i
c()
h()
r()
p_o()
un()
zero()
un()
plus()
un()
plus()
zero()
plus()
plus()
un()
plus()
un()
plus()
un()
p_f()

plus()

#n
c()
h()
r()
p_o()
un()
un()
zero()
plus()
plus()
plus()
plus()
plus()
plus()
zero()
p_f()

plus()

#t
c()
h()
r()
p_o()
un()
un()
un()
plus()
un()
plus()
un()
plus()
un()
plus()
un()
plus()
plus()
un()
p_f()

plus()

#p_o
c()
h()
r()
p_o()
un()
zero()
plus()
un()
zero()
plus()
un()
zero()
plus()
un()
zero()
plus()
plus()
plus()
zero()
p_f()

plus()

#f
c()
h()
r()
p_o()
zero()
plus()
zero()
plus()
un()
zero()
un()
plus()
un()
plus()
plus()
zero()
p_f()

plus()

#l
c()
h()
r()
p_o()
un()
zero()
un()
plus()
un()
plus()
un()
plus()
un()
plus()
un()
plus()
plus()
un()
plus()
un()
plus()
un()
p_f()

plus()

#a
c()
h()
r()
p_o()
un()
un()
plus()
un()
un()
plus()
un()
un()
plus()
un()
un()
plus()
un()
un()
plus()
un()
un()
plus()
un()
un()
plus()
un()
zero()
plus()
un()
zero()
plus()
plus()
zero()
p_f()

plus()

#g
c()
h()
r()
p_o()
un()
zero()
un()
plus()
un()
plus()
un()
plus()
plus()
plus()
plus()
plus()
plus()
zero()
p_f()

plus()

#p_f
c()
h()
r()
p_o()
un()
zero()
plus()
un()
zero()
plus()
un()
zero()
plus()
un()
un()
p_f()

p_f()

img.save("test.png")

# This is the payload we generate:
# -> exec(chr(11+100+1+1%1)+chr(111++1+1+1)+chr(101+1+0+++1+1+1)+chr(110+++++++0)+chr(111+1+1+1+1+++1)+chr(10+10+10+10++++0)+chr(0+0+101+1+++0)+chr(101+1+1+1+1+++1+1+1)+chr(11+11+11++11+11+11+11+10+10++00)+chr(101+1+1++++++00)+chr(10+10+10+11))

Now all we have to do is to drop the generated image on the website and we get the flag: easyctf{wish_thi5_fl@g_was_1n_ASCII_@rt_t0o!}