EASYCTF - Remember Me

description: I’m such a klutz! I know I hid a flag in this file somewhere, but I can’t remember where I put it! Song is from sukasuka.

hint: Sometimes I can’t tell my left from my right, either.

category: Forensics

In this chall, we have a mp3 file containing an amazing japanese song.

Of course, I tried using file, exiftool and binwalk command, but there were nothing interesting…

GitHub Logo

I load the file in Audacity, and given the hint, I made the difference between the 2 channels. There are 2 ways to do it:

- split the audio channels into two tracks (Black triangle -> Splitting a Stereo Track), invert the right channel (Select track -> Effect -> invert), set it to be a left channel (Black triangle -> Join Stereo Track) and mix the two tracks together again (Track -> Mix and render).

- The simple way is to use Audacity plugin voice remover (Effect > Vocal Reduction and Isolation)

GitHub Logo

Using one of those methods, we now have no signal… Or maybe we have a signal ? Let’s try to amplificate the signal (Effect -> Amplification), we can boost the signal to the maximum threshold.

GitHub Logo

Nice ! we can hear a voice telling us the flag: easyctf{4ud10_st3g}

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!}

EASYCTF - Not OTP

It seems we’ve intercepted 2 strings that were both encrypted with what looks like OTP! Is it possible to decrypt them?

For this challenge we’ve been given a text file containing the following :

c1 = 38445d4e5311544249005351535f005d5d0c575b5e4f481155504e495740145f4c505c5c0e196044454817564d4e12515a5f4f12465c4a45431245430050154b4d4d415c560c4f54144440415f595845494c125953575513454e11525e484550424941595b5a4b c2 = 3343464b415550424b415551454b00405b4553135e5f00455f540c535750464954154a5852505a4b00455f5458004b5f430c575b58550c4e5444545e0056405d5f53101055404155145d5f0053565f59524c54574f46416c5854416e525e11506f485206554e51

The two strings have the same length, so we can think that they have been encrypted with the same key. Let K be the encryption key, C1 and C2 the two ciphers, and M1 and M2 the corresponding plain texts with:

C1 = M1 ⊕ K, C2 = M2 ⊕ K

Then, we have : R = C1 ⊕ C2 = M1 ⊕ M2

If we consider that the plain texts are made with comprehensive english language, if we xor R with a word contained in M1 at the offset x in the string, it will reveal a part of the message M2.

So we need to xor R with a known or potential substring at the offset 0 of R. If the result looks like an english word, it means that the substring may be in M1 or M2 at the offset 0. Else, we need to do the same again at offset +1.

This method is called crib dragging.

For this challenge, we can presume that the substring ‘easyctf{‘ is contained in M1 or M2. When xoring this substring with R, we find the substring “intext u” (let’s call it S) at the offset 76. In order to find out from which cipher (C1 or C2) this substring comes from, we can xor S with C1 and C2, at the offset 76. This will reveal a part of the key. With C2, it gives us :

:8+<*8t"

This looks pretty much like random text…

With C1, it gives us :

67, 182,

This one looks more like a pattern, with numbers, spaces and commas. It should be a part of the key.

At this point we know 8 characters from M1, M2 and K at the offset 76:

M1 : intext u M2 : easyctf{ K : 67, 182,

Then we have two possibilities :

  • xor R with an english word that is likely to appear in a sentence in order to find another part of M1 or M2
  • guess parts of M1 or M2, then xor it with the corresponding cipher to check if the result looks like the key pattern

If we guess a part of one of the plain texts then it is quite easy to find out a part of the other plain text at the same offset. In this challenge, the flag was at the en of the 2nd message M2 ( from offset 76 to the end ). As the process to recover the flag is quite long, we didn’t take the time to recover the full plain texts, but only enough words from the end to be able to rebuild the flag. We ended up with something like :

M1 : …can also refer to a sample of plaintext used in the breaking

M2 : ……you will never guess! flag is easyctf{otp_ttp_cr1b_dr4gz}

(the end of M1 might not be exact)

EASYCTF - Nosource

description: All you CTFers are sure getting on my nerves with your source-viewing and developer tools-ing! Alas, despite my best wishes, the experienced programmers on the wonderful website StackOverflow tell me that it’s impossible to keep you from looking at the HTML. But a disable right click script certainly won’t stop an experienced CTFer like you! So finding the flag in the source of this problem should be no trouble, right?

category: Web

When we launch the chall, a web page informs us that we have to use Chrome and enable Javascript. So I used Chrome and nothing happended. After playing with Javascript options in Chrome browser, I finally ended up to the ‘real’ chall page.

Here you can’t use ‘right click -> inspect element’ or ‘ctrl+u’ because it will redirect you to the first page and you won’t be able to get the source code like that.

The way I did it was using Wireshark: I use it to sniff the network when we get the challenge page.

Then we can get the source code by following the stream. Here is the source code:

<!-- Stop looking at the source code -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Stop looking at the source code</title>
<script class="delete">
	var timeout = setTimeout(function () {
		location.replace('/soupd?2');
	}, 1000);
</script>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/main.css" rel="stylesheet">
</head>

<body>
<main role="main" class="container">
<div class="starter-template">
	<h1>Stop looking at the source code</h1>
	<p class="lead">
		Welcome to the problem page! Please enter the flag below.
	</p>
	<form id="flag-form">
		<div class="form-group">
			<input type="text" class="form-control" id="flag" placeholder="easyctf{">
		</div>
		<button type="submit" class="btn btn-success btn-lg">Check</button>
	</form>
</div>

<script class="delete">
	// Ah, old friend, we meet again...
	function process(a, b) {
		'use strict';
		var len = Math.max(a.length, b.length);
		var out = [];
		for (var i = 0, ca, cb; i < len; i++) {
			ca = a.charCodeAt(i % a.length);
			cb = b.charCodeAt(i % b.length);
			out.push(ca ^ cb);
			
		return String.fromCharCode.apply(null, out);
	}

	(function () {
		'use strict';

		function soupd() {
			document.documentElement.innerHTML = '';
			location.replace('/soupd?2');
			setInterval(function () {
				location.replace('/soupd?12');
			}, 100);
			

		try {
			let badNodes = document.getElementsByClassName('delete');
			for (let i = 0; i < badNodes.length; i++) {
				badNodes[i].parentNode.removeChild(badNodes[i]);
			}
		} catch (e) {}

		try {
			window.history.pushState({}, 'Stop looking at the source', '/');
		} catch (e) {}

		try {
			var element = new Image('/static/img/soup.png');
			Object.defineProperty(element, 'id', { get: function () {
			  soupd();
			}});
			eval("console.log('Stop looking at the source code%c', element);");
		} catch (e) {}

		var formEl = document.getElementById('flag-form');
		var inputEl = document.getElementById('flag');

		var func = "(function (e, v) { e.preventDefault() || " +
					"alert(inputEl.value === process(this.prototype.flag, " +
					"this.prototype.key) ? 'Your flag is correct!' : " +
					"'Incorrect, try again.'); })";
		var f = 'DQ4cJgsbCVofB18sNw4wRlhfCwAbXxpTC1wwKVlcGBIaUDAGJzowYDoqTiI=';
		var p = { prototype: { flag: atob(f), key: 'heheheh!' }};

		document.addEventListener('DOMContentLoaded', function () {
			formEl.addEventListener('submit', eval(func).bind(p));
			$('.delete').remove();
		});

	})();
</script>
</main>

<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="/static/js/jquery-3.2.1.slim.min.js"></script>
<script src="/static/js/popper.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<script class="delete">clearTimeout(timeout);</script>

</body>
</html>

First let’s get rid of this html and build our javascript:

function process(a, b) {
	var len = Math.max(a.length, b.length);
	var out = [];
	for (var i = 0, ca, cb; i < len; i++) {
		ca = a.charCodeAt(i % a.length);
		cb = b.charCodeAt(i % b.length);
		out.push(ca ^ cb);
	}
	return String.fromCharCode.apply(null, out);
}

entry = "WeWriteHere"

var f = 'DQ4cJgsbCVofB18sNw4wRlhfCwAbXxpTC1wwKVlcGBIaUDAGJzowYDoqTiI=';

flag = atob(f);
key = 'heheheh!';

func()

function func() {
	alert(entry === process(flag, key) ? 'Your flag is correct!' : 'Incorrect, try again.'); 
}

It seems that all we have to do is to print process(flag, key) and write it as entry:

console.log(process(flag, key));
// -> ektCc~a{wb7I_kXg0:ces:rrc9XL19p3r5XcO_XARO&G

We try this in entry, but it didn’t work… We know the flag look like easyctf{…} And we can see: e??????{… Maybe the given key is not right. Let’s generate another one (we can see that the process methos only compute xor between entry and key):

# First character of the key is good (we want 'e' and we already have 'e')

# For the second character, we want 'a' in output, but we have 'k' and the actual second character key is 'e':
chr(ord('a')^ord('k')^ord('e'))
# -> o

# For the third character, we want 's' in output, but we have 't' and the actual third character key is 'h':
chr(ord('s')^ord('t')^ord('h'))
# -> o

# For the 4th character, we want 'y' in output, but we have 'C' and the actual 4th character key is 'e':
chr(ord('y')^ord('C')^ord('e'))
# -> _

# The 5th character of the key is good (we want 'c' and we already have 'c')

# For the 6th character, we want 't' in output, but we have '~' and the actual 6th character key is 'e':
chr(ord('t')^ord('~')^ord('e'))
# -> o

# For the 7th character, we want 'f' in output, but we have 'a' and the actual 6th character key is 'h':
chr(ord('f')^ord('a')^ord('h'))
# -> o

Okay let’s set our new key (‘hoo_hoo’) and print the flag:

function process(a, b) {
	var len = Math.max(a.length, b.length);
	var out = [];
	for (var i = 0, ca, cb; i < len; i++) {
		ca = a.charCodeAt(i % a.length);
		cb = b.charCodeAt(i % b.length);
		out.push(ca ^ cb);
	}
	return String.fromCharCode.apply(null, out);
}

var f = 'DQ4cJgsbCVofB18sNw4wRlhfCwAbXxpTC1wwKVlcGBIaUDAGJzowYDoqTiI=';
flag = atob(f);
key = 'hoo_hoo!';

console.log(process(flag, key));
// -> `easyctf{wh0s_a_g00d_s0urc3_v13w3r?_YOU_ARE!}`

We can validate the chall with this flag =)

EASYCTF - Little Language

I want root access to this special programming portal, and this file is my only clue. Maybe the password is inside? Even if it is, I’m not sure how to enter it. nc c1.easyctf.com 12480 Oh! Almost forgot… this might help.

For this challenge, we’ve been given an image: encrypted.png.

The instructions say that the password might be in it… When using strings on the picture, we get the following text:

note: the password is l7&4C&Cg

Also on the picture, we can see a pseudo mathematical expression with the followings:

  • FLAG
  • E(username) = root
  • E(password) = REDACTED

In the instructions, we can find a link to a page with the following expression written on it:

S : E { ExpS $1 } | global var '=' E { GlobalVarS $2 $4 }

Seems like a parsing expression grammar…

The goal of this challenge is to connect to the c1.easyctf.com server and get the flag. When we connect to the server, it displays the following output :

ctflang commands begin with “:” (try :help)

:help gives us a little help saying that :

  • :help show this message
  • :end stop current multi-line parse
  • :q exit
  • note: certain language features only available to root users

The goal is clear : we have to login with username root and password l7&4C&Cg. First, we can try a simple 1 + 1 which give us the output 2. Good news ! Our expression is interpreted. Then if we try username = "root", we get the following message : Could not evaluate statement or expression to a value.

So let’s try global username = "root" ! No error message… Then entering username give us the output : "root". It works the same with : global password = "l7&4C&Cg".

So we can try to print the flag with the command : flag. Unfortunately it gives us the previous error message. But if we try again username = "root", instead of an error message, we get a Yas ! Let’s try with password = "l7&4C&Cg" : and again we get a Yas ! Finally we just need to enter the command : flag to print the flag : EasyCTF{5m4ll_573p_53m4n71c5_4r3_fun_r16h7?}