Hacky Easter 2018 writeup

As every year hacking-lab.com carried out the annual Hacky Easter event with 27 challenges. I could not spend as much time as I would have liked to on solving the challenges, but after all I managed to collect 25 of the 27 eggs and focused on this writeup.
Easy
01 Prison Break
02 Babylon
03 Pony Coder
04 Memeory
05 Sloppy & Paste (mobile)
06 Cooking for Hackers
07 Jigsaw
08 Disco Egg
09 Dial Trial (mobile)
Medium
10 Level Two
11 De Egg you must (not solved)
12 Patience (mobile)
13 Sagittarius…
14 Same same…
15 Manila greetings
16 git cloak –hard
17 Space Invaders
18 Egg Factory
Hard
19 Virtual Hen
20 Artist: No Name Yet (not solved)
21 Hot Dog
22 Block Jane
23 Rapbid Learning
24 ELF
Hidden
25 Hidden Egg #1
26 Hidden Egg #2
27 Hidden Egg #3

01 – Prison Break

The encryption method is taken from the television serial Prison Break (see here).

The prefix of the phone numbers (555) can be ignored. The other digits determine which key on a cellphone-keyboard should be pressed and the dots on the origami determine how often the key should be pressed:

Example:

Phone Number: 7
Dots on Origami: ... (3)
--> Pressing the key 7 three times: R

Phone Number: 2
Dots on Origami: .. (2)
--> Pressing the key 2 two times: B

By combining the phone numbers and the dots on the origami this way the message can be decoded:

Link
----
Phone Numbers:	7  7   4   7    6  6  3
Origami:	. ... ... .... ... .. ..
		1  3   3   4    3  2  2
Plaintext:	P  R   I   S    O  N  E

Sara
----
Phone Numbers:	 7   4   7   5   4  6  4
Origami:	... ... .... .. ... .. .
		 3   3   4   2   3  2  1
Plaintext:	 R   I   S   K   I  N  G

The password is PRISONERISKING.

02 – Babylon

The hint walls and shelves refers to the Library of Babel (see here).

The library can be browsed (see here) by entering:

  • a hex name (the challenge’s attachment: babylon.txt)
  • a wall (the first digit: 4)
  • a shelf (the second digit: 4)
  • a volume (the third digit: 28)
  • a page (the fourth digit: 355)

The page contains the following text:

the super secret hackyeaster password is checkthedatayo

The password is checkthedatayo.

03 – Pony Coder

The title Pony Coder refers to the Punycode specified in RFC 3492 (see here: RFC 3492, wikipedia). Punycode is used to encode Unicode characters as ASCII characters in order to use them as DNS names.

When decoding the provided string gn tn-gha87be4e with an online-decoder like www.punycoder.com we need to prefix the string with xn--:

The password is gin tonic.

04 – Memeory

A single memeory card is represented by a figure-element:

<figure id="legespiel_card_0">
  <a href="#card_0">
    <img class="boxFront" src="./lib/1.jpg" />
    <img class="boxWhite" src="./lib/shadow_card.png" />
    <img class="boxBack" src="./lib/back.jpg"  />
  </a>
  <img class="boxStretch" src="./lib/shim.gif" />
</figure>

Two adjacent cards have the same picture and thus belong together:

legespiel_card_0 <--> legespiel_card_1
legespiel_card_2 <--> legespiel_card_3
legespiel_card_4 <--> legespiel_card_5
...

This means that we just have to click the cards in the order of their id (0,1,2,3,4,5…). We can simply emulate these clicks by running javascript code in the console-window of our browser (F12):

for (var i = 0; i < 100; i++) $("#legespiel_card_"+i+" a").click();

This single line clicks one card after the other finally yielding the egg:

05 – Sloppy & Paste (mobile)

When trying to copy the base64-string displayed in the app, another base64-string is loaded in the clipboard. Decoding this string yields a locked egg:

root@kali:~/Documents/he18/egg5# echo "iVBORw0KGgoAAAANSUhEUgAAAeAAAAHgCAMAAABKCk6nAAACQ1BM...pKUAAAAASUVORK5CYII=" | base64 -d > egg05_locked.png

I used the APK Editor app to extract the challenge’s html-file from the hacky easter app:

The html-file contains the actual base64-string being displayed:

...
<p>The egg is right here. Just copy it!</p>
<p><button onclick="doClip();">Copy to Clipboard</button></p>
<textarea id="area" style="width:100%; height: 240px; word-break: break-all;"> iVBORw0KGgoAAAANSUhEUgAAAeAAAAHgCAMAAABKCk6nAAACQ1BM...BwAAAABJRU5ErkJggg==</textarea>
...

Decoding this string…

root@kali:~/Documents/he18/egg5# echo "iVBORw0KGgoAAAANSUhEUgAAAeAAAAHgCAMAAABKCk6nAAACQ1BM...BwAAAABJRU5ErkJggg==" | base64 -d > egg05.png

…yields the actual egg:

06 – Cooking for Hackers

The ingredients seem to be base64-encoded:

usr@host:~$ echo "c2FsdA==" | base64 -d
salt

usr@host:~$ echo "b2ls" | base64 -d
oil

usr@host:~$ echo "dDd3Mmc=" | base64 -d
t7w2g

usr@host:~$ echo "bnRkby4=" | base64 -d
ntdo.

usr@host:~$ echo "b25pb24=" | base64 -d
onion

Because of the .onion at the end, we can assume that this is a TOR hidden service address, which can for example be accessed using the Tor Browser. The complete address simply is the concatenation of all ingredients: saltoilt7w2gntdo.onion.

07 – Jigsaw

I started by writing a python-script, which extracts all single tiles from the provided image and saves each tile in an own file:

root@kali:~/Documents/he18/egg7# cat egg7.py
#!/usr/bin/env python

import Image

img = Image.open("jigsaw.png")
pix = img.load()

img_arr = []
pix_arr = []
for i in range(1280/40):
  img_arr.append([])
  pix_arr.append([])
  for j in range(720/40):
    img_arr[i].append(Image.new("RGB", (40, 40), "white"))
    pix_arr[i].append(img_arr[i][j].load())

for w in range(img.size[0]):
  for h in range(img.size[1]):
    pix_arr[w/40][h/40][w%40,h%40] = pix[w,h]

for i in range(1280/40):
  for j in range(720/40):
    img_arr[i][j].save("title_"+str(i)+"_"+str(j)+".png")

At next I wrote another python-script, which generates an html-page, containing all image-tiles:

root@kali:~/Documents/he18/egg7# cat gen_html.py
print("<html>")
print("<style>.sel { border:1px solid red; }</style>")
print("<body>")
print("<table>")
for i in range(18):
  print("<tr>"),
  for j in range(32):
    print("<td><img id=\"img"+str(j)+"_"+str(i)+"\" onclick=\"clicked(this)\"/></td>")
  print("</tr>")
print("</table>")
print("<script src=\"jigsaw.js\"></script>")
print("</body>")
print("</html>")

The html-page includes the following javascript:

root@kali:~/Documents/he18/egg7# cat jigsaw.js
for (var i = 0; i < 32; i++) {
  for (var j = 0; j < 18; j++) {
    document.getElementById("img"+i+"_"+j).src = "title_"+i+"_"+j+".png";
  }
}

sel = null;

function clicked(elem) {
  if (sel == null) {
    sel=elem;
    sel.addClass("sel");
  }
  else {
    sel.removeClass("sel");
    tmp = sel.src;
    sel.src = elem.src;
    elem.src = tmp;
    sel = null;
  }
}

The javascript highlights a tile which has been clicked (addClass("sel")) and changes it with the next tile being clicked.

This way I puzzled the image until the password became visible:

The password is goodsheepdontalwayswearwhite.

08 – Disco Egg

The provided website displays the egg with a blinking background and each pixel of the qrcode continuously changing colors:

I started by saving the website offline in order to change the html-code.

Each pixel of the qrcode is represented by a cell (td element) within a table:

<td class="cyan black green darkgreen blue orange red darkgrey brown" style="background-color: rgb(251, 89, 3);"></td>...

Each cell either has the class black or white, which equals the actual pixel-color of the qrcode.

The qrcode can be displayed by making three changes in the html-file:

1. Remove initial background-color

The initial color of each cell is set by the style defined in the td element. By removing all occurrences of background-color there is no initial color.

Before:

<td class="cyan black green darkgreen blue orange red darkgrey brown" style="background-color: rgb(251, 89, 3);"></td>

After change:

<td class="cyan black green darkgreen blue orange red darkgrey brown" style=": rgb(251, 89, 3);"></td>

(The css within the style attribute is not valid anymore, but we don’t care for now.)

2. Comment out javascript

The colors are changed via javascript. We don’t want the colors to be changed.

Before:

<script src="./Disco Egg_files/jquery-1.12.4.js.Download"></script>
<script src="./Disco Egg_files/jquery-ui.js.Download"></script>
<script>bgcolors = [ "#1FB714", "#FBF305", "#006412", ... </script>

After change:

<!--
<script src="./Disco Egg_files/jquery-1.12.4.js.Download"></script>
<script src="./Disco Egg_files/jquery-ui.js.Download"></script>
<script>bgcolors = [ "#1FB714", "#FBF305", "#006412", ... </script>
-->

3. Add color to class black

At last only the cells which have the class black should be colored. We can achieve this by adding the background-color property within the css definition.

Before:

.black, .white, .green, ...

After change:

.black {background-color:#000000; } .white, .green, ...

After hitting F5 in order to reload the page we get the qrcode:

09 – Dial Trial (mobile)

When pushing the Dial! button in the app, the sound of dialing phone is played.

Like in challenge 05 I used the APK Editor app to extract the mp3-file res/raw/dial.mp3 from the hacky easter app:

The dialing sound is called Dual-tone multi-frequency signaling (DTMF) (see wikipedia) and can for example be decoded here.

The sound is decoded to the following digits:

4*7#2*6#1*2#2*5#2*3#3*6#2*6#2*6#3*6#2*5#3*4#1*2

Similar to challenge 01 the numbers determine which key (second digit) on a cellphone should be pressed and how often (first digit):

This results in the following password:

4*7#2*6#1*2#2*5#2*3#3*6#2*6#2*6#3*6#2*5#3*4#1*2
S   N   A   K   E   O   N   N   O   K   I   A

The password is snakeonnokia.

10 – Level Two

My solution is quite lame, because I did not actually play the game. I rather extracted the game files using rgssad:

root@kali:~/Documents/he18/egg10# git clone https://github.com/luxrck/rgssad
Cloning into 'rgssad'...
remote: Counting objects: 46, done.
remote: Compressing objects: 100% (19/19), done.
remote: Total 46 (delta 15), reused 46 (delta 15), pack-reused 0
Unpacking objects: 100% (46/46), done.

cargo is required to install rgssad:

root@kali:~/Documents/he18/egg10# cd rgssad/
root@kali:~/Documents/he18/egg10/rgssad# apt-get install cargo
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  libgit2-26 libstd-rust-1.24 libstd-rust-dev rust-gdb rustc
  ...

After installing cargo rgssad can be installed:

root@kali:~/Documents/he18/egg10/rgssad# cargo install
  Installing rgssad v0.1.3 (file:///root/Documents/he18/egg10/rgssad)
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading regex v1.0.0
 Downloading utf8-ranges v1.0.0
 Downloading aho-corasick v0.6.4
 ...

Now the game files can be extracted from the file Game.rgss3a (this file will be created when installing the game on windows):

root@kali:~/Documents/he18/egg10# mkdir out
root@kali:~/Documents/he18/egg10# rgssad unpack Game.rgss3a out/ .
Extracting: Data/System.rvdata2
Extracting: Data/Map009.rvdata2
Extracting: Data/Tilesets.rvdata2
Extracting: Data/Animations.rvdata2
Extracting: Data/Map015.rvdata2
Extracting: Data/Armors.rvdata2
Extracting: Data/Map006.rvdata2
...

Let’s search for relevant information within the extracted files:

root@kali:~/Documents/he18/egg10# cd out
root@kali:~/Documents/he18/egg10/out# grep -r "password"
Binary file Data/Map024.rvdata2 matches
root@kali:~/Documents/he18/egg10/out# strings Data/Map024.rvdata2 | less
...
I"9puts "[!] Delete this in production: PW is 13371337"
...
I")7034353577307264355f3406033b5749114c
...

The password 13371337 is not the right one yet. But there is a hex-encoded ASCII string equal to the password for the teaser-challenge.

root@kali:~/Documents/he18/egg10/out# python
>>> s = "7034353577307264355f3406033b5749114c"
>>> s.decode("hex")
'p455w0rd5_4\x06\x03;WI\x11L'

This also does not seem right yet. Let’s browse through all values like this (beginning with I"):

root@kali:~/Documents/he18/egg10/out# cd Data
root@kali:~/Documents/he18/egg10/out/Data# strings -f * | grep 'I"' | less
...
Items.rvdata2: I"+7034353577307264355f3472335f6330306c
...
Map014.rvdata2: I")7034353577307264355f052d066b15035433
...
Map015.rvdata2: I")70343535773072105d6c6b05032d0f546f4c
...
Map024.rvdata2: I")7034353577307264355f3406033b5749114c
...

Including the last value, which we have already discovered, there are 4 hex-encoded ASCII strings:

root@kali:~/Documents/he18/egg10/out/Data# python
>>> s1 = "7034353577307264355f3472335f6330306c"
>>> s2 = "7034353577307264355f052d066b15035433"
>>> s3 = "70343535773072105d6c6b05032d0f546f4c"
>>> s4 = "7034353577307264355f3406033b5749114c"
>>> s1.decode("hex")
'p455w0rd5_4r3_c00l'
>>> s2.decode("hex")
'p455w0rd5_\x05-\x06k\x15\x03T3'
>>> s3.decode("hex")
'p455w0r\x10]lk\x05\x03-\x0fToL'
>>> s4.decode("hex")
'p455w0rd5_4\x06\x03;WI\x11L'

The first string is a valid ASCII string: p455w0rd5_4r3_c00l. The three other values contain non-ASCII-characters but begin with the same characters as the first string.

After trying different things, I figured out, that each of the three later strings is supposed to be XORed with the first string:

>>> s1 = 0x7034353577307264355f3472335f6330306c
>>> s2 = 0x7034353577307264355f052d066b15035433
>>> s3 = 0x70343535773072105d6c6b05032d0f546f4c
>>> s4 = 0x7034353577307264355f3406033b5749114c
>>> hex(s1^s2)[2:-1].decode("hex")
'1_54v3d_'
>>> hex(s1^s3)[2:-1].decode("hex")
'th3_w0rld_ '
>>> hex(s1^s4)[2:-1].decode("hex")
't0d4y! '

That’s it. The password is 1_54v3d_th3_w0rld_t0d4y!:

11 – De Egg you must

Unfortunately I could not spend as much time as I would have needed to solve this challenge. Nevertheless, here is what I got so far:

The provided zip-archive basket.zip contains 6 files (egg1 – egg6) and is password-protected:

root@kali:~/Documents/he18/egg11# unzip -l basket.zip
Archive:  basket.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
  1433600  2018-02-09 03:57   egg1
  1433600  2018-02-09 03:57   egg2
  1433600  2018-02-09 03:57   egg3
  1433600  2018-02-09 03:57   egg4
  1433600  2018-02-09 03:57   egg5
   384584  2018-02-09 03:57   egg6
---------                     -------
  7552584                     6 files

root@kali:~/Documents/he18/egg11# unzip basket.zip
Archive:  basket.zip
[basket.zip] egg1 password:

The password can be cracked using fcrackzip and a wordlist like the rockyou.txt:

root@kali:~/Documents/he18/egg11# fcrackzip -u -D -p /usr/share/wordlists/rockyou.txt basket.zip


PASSWORD FOUND!!!!: pw == thumper

Now the files can be extracted:

root@kali:~/Documents/he18/egg11/a# unzip basket.zip
Archive:  basket.zip
[basket.zip] egg1 password: (thumper)
  inflating: egg1
  inflating: egg2
  inflating: egg3
  inflating: egg4
  inflating: egg5
  inflating: egg6

The first file egg1 seems to be an m4v-file:

root@kali:~/Documents/he18/egg11/a# file *
basket.zip: Zip archive data, at least v2.0 to extract
egg1:       ISO Media, Apple iTunes Video (.M4V) Video
egg2:       data
egg3:       data
egg4:       data
egg5:       data
egg6:       data

After searching through the other files, I recognized that all files seem to be part of the m4v-file:

root@kali:~/Documents/he18/egg11/a# cat egg* > total.m4v

This file is a valid movie which can be played. The movie has also been posted on twitter.

I recognized that there are 14926 bytes at the end of the file (in this case part of the original file egg6), which do not belong to the actual movie:

root@kali:~/Documents/he18/egg11# tail -c 14926 total.m4v | hexdump -C | head -n30
00000000  26 29 28 24 23 5e 40 2a  23 5e 28 00 76 af b1 b8  |&)($#^@*#^(.v...|
00000010  f2 f5 e5 f5 ff ff ff f2  b6 b7 bb ad ff ff fe 1f  |................|
00000020  ff ff fe 1f f7 fc ff ff  ff b5 f5 b1 58 ff ff fd  |............X...|
00000030  02 af b3 ab ba ff ff ff  cc c6 c9 cc c6 c9 d0 d0  |................|
00000040  d0 cc c7 c9 cb c8 c9 cc  c8 c9 ca c8 c8 cc c7 c9  |................|
00000050  cb c9 c8 ca c9 c8 cb c9  c9 ca c8 c8 cc c7 ca c6  |................|
00000060  c6 c6 cb c8 c8 ca c9 c9  cb c9 c9 c8 c7 c5 c8 c8  |................|
00000070  c4 cb c8 c8 cc c8 c9 cc  c7 ca cc c8 c9 cb c8 c9  |................|
00000080  cb c8 c8 cb c9 c9 cb ca  c9 cb c8 c8 cb c8 c9 cb  |................|
00000090  c8 c8 ca c8 c7 ca c8 c7  cb c8 c8 ca c9 c8 cb ca  |................|
000000a0  ca cb c8 c8 ca c9 c7 c9  c9 c9 00 05 04 06 1b 17  |................|
000000b0  00 00 00 cc c5 c9 cc c9  c5 63 50 ab ca ca ca 39  |.........cP....9|
000000c0  48 84 ce cb c7 d1 ce c9  0e 0e 0d a2 a0 9d 38 37  |H.............87|
000000d0  36 10 10 0f 2c 2b 2a 21  21 20 b4 b1 ae ad aa a7  |6...,+*!! ......|
...

My assumption is that this is an encrypted image, but I did not figure out in time.

12 – Patience (mobile)

I used the APK Editor app to edit the challenge html-file challenge12.html

… and added an output for the hash, which is being calculated 100000 times before being used as the image-name of the egg:

The first hash, which is calculated with the start value genesis and the count 100000 is deff00cf98ca4019d94ccfe99a5c35c81b39c917:

Since the hash is 160 bit (=20 byte) long, it is probably sha1. Thus I tried a few inputs for the hash calculation:

root@kali:~/Documents/he18/egg12# echo -n "genesis" | sha1sum
fe10566e2adeece8faf585a8fbd5db896e4a60f7  -
root@kali:~/Documents/he18/egg12# echo -n "genesis,100000" | sha1sum
45b76c441161c736e3d81154ede7addfa2a91dd5  -
root@kali:~/Documents/he18/egg12# echo -n "genesis100000" | sha1sum
deff00cf98ca4019d94ccfe99a5c35c81b39c917  -

The last one is a hit. The hash-input is the previous hash and the count-value.

I wrote the following python-script to calculate the final hash, which is the image-name of the egg:

root@kali:~/Documents/he18/egg12# cat egg12.py
#!/usr/bin/env python

import hashlib

c = 100000
h = "genesis"

while c > 0:
  m = hashlib.sha1()
  m.update(h+str(c))
  h = m.hexdigest()
  c -= 1

print("https://hackyeaster.hacking-lab.com/hackyeaster/images/eggs/" + h + ".png")

Running the script …

root@kali:~/Documents/he18/egg12# ./egg12.py
https://hackyeaster.hacking-lab.com/hackyeaster/images/eggs/dd6f1596ab39b463ebecc2158e3a0a2ceed76ec8.png

… yields the egg:

13 – Sagittarius…

The provided file pila.kmz is a Keyhole Markup Language Zipped file, which contains some coordinates and five images:

root@kali:~/Documents/he18/egg13# file pila.kmz
pila.kmz: Zip archive data, at least v2.0 to extract
root@kali:~/Documents/he18/egg13# unzip pila.kmz
Archive:  pila.kmz
  inflating: pila.kml
  inflating: star_b_20.png
  inflating: star_w_20.png
  inflating: star_ya_20.png
  inflating: star_yb_20.png
  inflating: star_yc_20.png
root@kali:~/Documents/he18/egg13# cat pila.kml
<kml xmlns="http://www.opengis.net/kml/2.2" hint="target=sky"><Document>
<Style id="b20"><IconStyle><scale>1</scale><Icon><href>star_b_20.png</href></Icon></IconStyle></Style>
<Style id="b15"><IconStyle><scale>0.75</scale><Icon><href>star_b_20.png</href></Icon></IconStyle></Style>
<Style id="b10"><IconStyle><scale>0.5</scale><Icon><href>star_b_20.png</href></Icon></IconStyle></Style>
<Style id="w20"><IconStyle><scale>1</scale><Icon><href>star_w_20.png</href></Icon></IconStyle></Style>
...
<Placemark>
<styleUrl>#yc10</styleUrl><Point><coordinates>120.70710678118655,-44.792893218813454,0</coordinates></Point>
</Placemark>

<Placemark>
<styleUrl>#b10</styleUrl><Point><coordinates>120.67572462851734,-44.762845859799256,0</coordinates></Point>
</Placemark>
                        
<Placemark>
<styleUrl>#yc20</styleUrl><Point><coordinates>120.64018439966448,-44.73177872040262,0</coordinates></Point>
</Placemark>
...

The kmz-file represent coordinates in the sky and can be viewed using Google Earth:

After changing the icon for the coordinates, we can see that the coordinates look similar to a qrcode:

The coordinates are actually located on a round circle but are contorted when viewed in Google Earth. The coordinates seem to be located on 13 different circles. This means that we are looking for a 25×25 qrcode:

The coordinates located on the circles must be transformed to the appropriate squares in order to form a valid qrcode:

I started by determining which radius each of the 13 circles has using the following python-script:

root@kali:~/Documents/he18/egg13# cat radius.py
#!/usr/bin/python

import math
from PIL import Image

f = open("pila.kml", "r")

tagOpen = "<coordinates>"
tagClose = "</coordinates>"
coords = []
max_x = 0.0
min_x = 10000.0
max_y = -1000.0
min_y = 0.0
center_x = 0.0
center_y = 0.0

# parse coordinates and determine min/max coordinate
for l in f:
  if (tagOpen in l):
    coord = [float(x) for x in l[l.index(tagOpen)+len(tagOpen):l.index(tagClose)].split(",")]
    coords.append(coord)
    if (coord[0] > max_x): max_x = coord[0]
    if (coord[0] < min_x): min_x = coord[0]
    if (coord[1] > max_y): max_y = coord[1]
    if (coord[1] < min_y): min_y = coord[1]

center_x = min_x + (max_x - min_x) / 2
center_y = min_y + (max_y - min_y) / 2

print(max_x)
print(min_x)
print(max_y)
print(min_y)
print("")
print(center_x)
print(center_y)

me = []

for coord in coords:
  u = coord[0] - center_x
  v = coord[1] - center_y
  d = math.degrees(math.atan2(u,v))
  r = float(round(math.sqrt(u**2+v**2),2))
  if (r not in me): me.append(r)

print(me)
print(len(me))

I rounded off the radius to two decimals:

root@kali:~/Documents/he18/egg13# ./radius.py
121.0
119.0
-44.5034542418
-46.5

120.0
-45.5017271209
[1.0, 0.92, 0.83, 0.84, 0.75, 0.67, 0.58, 0.59, 0.5, 0.42, 0.33, 0.34, 0.25, 0.17, 0.08, 0.66, 0.91]
17

17 different radius-values have been found, but there are only 13 circles. There are four pairs which only differ in the last decimal number. These can be perceived to be on the same circle:

  • 0.84 / 0.83
  • 0.59 / 0.58
  • 0.67 / 0.66
  • 0.34 / 0.33

Based on this I wrote a python-script which transforms the coordinates on the circles to a valid qrcode. The script does:

  1. calculate the radius and degree of a coordinate
  2. determine on which of the 13 circles/squares the coordinate is located based on the radius
  3. determine the concrete x/y-square-value of the coordinate based on the degree

The following image shows an example:

The python-script:

root@kali:~/Documents/he18/egg13# cat transform.py
#!/usr/bin/env python

from __future__ import division, print_function
import math
from PIL import Image

def getRdIdx(r):
  r = float(round(r,2))
  if (r == 1.0): return 0
  elif (r <= 0.92 and r >= 0.91): return 1
  elif (r <= 0.84 and r >= 0.83): return 2
  elif (r == 0.75): return 3
  elif (r <= 0.67 and r >= 0.66): return 4
  elif (r <= 0.59 and r >= 0.58): return 5
  elif (r == 0.5): return 6
  elif (r == 0.42): return 7
  elif (r <= 0.34 and r >= 0.33): return 8
  elif (r == 0.25): return 9
  elif (r == 0.17): return 10
  elif (r == 0.08): return 11
  else: return 12


# y,x = 0..24
# center = (12,12)
def degreeBlocks(y, x):
  y_tmp = 12 - y
  x_tmp = x - 12
  return math.degrees(math.atan2(x_tmp, y_tmp))



def getDiff(a1, a2):
  r = (a2-a1) % 360.0
  if r >= 180.0: r -= 360.0
  return abs(r)



f = open("pila.kml", "r")

tagOpen = "<coordinates>"
tagClose = "</coordinates>"
coords = []
max_x = 0.0
min_x = 10000.0
max_y = -1000.0
min_y = 0.0
center_x = 0.0
center_y = 0.0

for l in f:
  if (tagOpen in l):
    coord = [float(x) for x in l[l.index(tagOpen)+len(tagOpen):l.index(tagClose)].split(",")]
    coords.append(coord)
    if (coord[0] > max_x): max_x = coord[0]
    if (coord[0] < min_x): min_x = coord[0]
    if (coord[1] > max_y): max_y = coord[1]
    if (coord[1] < min_y): min_y = coord[1]

center_x = min_x + (max_x - min_x) / 2
center_y = min_y + (max_y - min_y) / 2

me = {}
for i in range(13):
  me[i] = 0

bestAll = []

for coord in coords:
  u = center_x - coord[0]
  v = -1*(center_y - coord[1])
  d = math.degrees(math.atan2(u,v))
  r = math.sqrt(u**2+v**2)
  rd_idx = getRdIdx(r)
  me[rd_idx] += 1

  best_degree_diff = 360
  best = None

  for y in range(25):
    for x in range(25):
      if ((y>=rd_idx and y<=24-rd_idx and x>=rd_idx and x<=24-rd_idx) and (y == rd_idx or y == (24-rd_idx) or x == rd_idx or x == (24-rd_idx))):
        degree_blocks = degreeBlocks(y,x)
        diff = getDiff(d, degree_blocks)
        if (diff < best_degree_diff):
          best_degree_diff = diff
          best = (y,x)

  bestAll.append(best)


im = Image.new("RGB", (25,25), "white")
pix = im.load()

for y in range(25):
  for x in range(25):
    if ((y,x) in bestAll):
      pix[x,y] = (0,0,0)

im.save("out.png")

Running the script …

root@kali:~/Documents/he18/egg13# ./transform.py

… generates the qrcode:

14 – Same same…

The provided file upload.php.txt contains the source-code of the challenge-website (http://whale.hacking-lab.com:4444):

<?php
require __DIR__ . "/vendor/autoload.php"; // QR decoder library from https://github.com/khanamiryan/php-qrcode-detector-decoder

try {
    $qrcode1 = new QrReader($_FILES["file1"]["tmp_name"]);
    $answer1 = $qrcode1->text();
} catch(Exception $e) {
    exit("Error while reading the first QR.");
}

try {
    $qrcode2 = new QrReader($_FILES["file2"]["tmp_name"]);
    $answer2 = $qrcode2->text(); 
} catch(Exception $e) {
    exit("Error while reading the second QR.");
}

if(($answer1 == "Hackvent" && $answer2 == "Hacky Easter" or $answer1 == "Hacky Easter" && $answer2 == "Hackvent") && sha1_file($_FILES["file1"]["tmp_name"]) == sha1_file($_FILES["file2"]["tmp_name"])) {
    [SURPRISE]
}
else {
    echo ":-(";
}
?>

The website expects two files to be uploaded:

The uploaded files are treated as images containing a qrcode. The parsed strings of the qrcodes are stored in $answer1 and $answer2. In order to fulfill the crucial comparison on line 18 to get the egg, the qrcodes must contain both strings Hackvent and Hacky Easter. Also, the sha1 hash of both files must be the same.

The qrcode images can be generated using qrencode:

usr@host:~/pdf$ qrencode "Hackvent" -o qr1.png
usr@host:~/pdf$ qrencode "Hacky Easter" -o qr2.png

Of course the sha1 hash of these images are not the same:

usr@host:~/pdf$ sha1sum qr1.png 
1b7a5b35b69d924a7203e740e4b364e3fcbf6fb9  qr1.png
usr@host:~/pdf$ sha1sum qr2.png 
a95574beb9b4ffd4f67c2d4aeb53f0b8c95f55c8  qr2.png

On git there is a python-script (sha1collider) which uses the SHAttered pdf prologue from https://shattered.io/ to generate two pdf files with the same sha1 hash.

We start by converting the png files to pdf:

usr@host:~/pdf$ convert qr1.png qr1.pdf
usr@host:~/pdf$ convert qr2.png qr2.pdf

Git clone sha1collider:

usr@host:~/pdf$ git clone https://github.com/nneonneo/sha1collider
Klone nach 'sha1collider' ...
remote: Counting objects: 18, done.
remote: Total 18 (delta 0), reused 0 (delta 0), pack-reused 18
Entpacke Objekte: 100% (18/18), Fertig.

And run the script on both pdf files:

usr@host:~/pdf$ ./sha1collider/collide.py --progressive qr1.pdf qr2.pdf 
[12:39:08] INFO: rendering file 1...
GPL Ghostscript 9.21 (2017-03-16)
Copyright (C) 2017 Artifex Software, Inc.  All rights reserved.
This software comes with NO WARRANTY: see the file PUBLIC for details.
Processing pages 1 through 1.
Page 1
[12:39:08] INFO: rendering file 2...
...
363x363 RGB Targa image
Independent JPEG Group's CJPEG, version 9b  17-Jan-2016
Copyright (C) 2016, Thomas G. Lane, Guido Vollbeding
363x363 RGB Targa image
[12:39:08] INFO: producing final PDFs

Notice: As stated on git, the script should be called with the option --progressive.

The sha1 hash of both files is the same now:

usr@host:~/pdf$ sha1sum out-*
bdece875ca36c6505b0728cbeca7495db1a30246  out-qr1.pdf
bdece875ca36c6505b0728cbeca7495db1a30246  out-qr2.pdf

After uploading the files we get the egg:

15 – Manila greetings

The provided file deck contains a list of playing-cards:

root@kali:~/Documents/he18/egg15# cat deck
d8
s3
d7
d3
c2
s5
da
c6
s7
d6
jr
...

The secret message GTIFL RVLEJ TAVEY ULDJO KCCOK P seems to be somehow encrypted using the deck of cards.

When googling for encryption methods based on playing cards, I stumbled upon the following: The Solitaire Encryption Algorithm.

The article describes in detail how the cryptosystem code-named Pontifex from Neal Stephenson’s novel Cryptonomicon works. Within the novel the characters Enoch Root and Randy Waterhouse (mentioned in the challenge) communicate using this cryptosystem.

I followed the description in the article and wrote a python-script to decode the message (see comments in code):

root@kali:~/Documents/he18/egg15# cat solitaire.py
#!/usr/bin/env python

deck = []

def cardToNum(card):
  # ordering:
  #    1..13 : clubs A,2,3,...,9,10,J,Q,K
  #   14..26 : diamonds A,2,3,...,9,10,J,Q,K
  #   27..39 : hearts A,2,3,...,9,10,J,Q,K
  #   40..52 : spades A,2,3,...,9,10,J,Q,K
  #   53..54 : joker A, joker B
  if (card == "jr"): return 53
  if (card == "jb"): return 54
  arr_clr = {"c":0,"d":1,"h":2,"s":3}
  arr_val = {"a":1,"2":2,"3":3,"4":4,"5":5,"6":6,"7":7,"8":8,"9":9,"10":10,"j":11,"q":12,"k":13}
  clr = card[0]
  val = card[1:]
  return arr_clr[clr]*13 + arr_val[val]


def streamChar():
  global deck
  # step 1: Find the A joker
  for i in range(len(deck)):
    if (deck[i] == 53):
      if (i == len(deck)-1):
        deck = deck[0:1] + [53] + deck[1:len(deck)-1]
        break
      else:
        tmp = deck[i]
        deck[i] = deck[i+1]
        deck[i+1] = tmp
        break

  # step 2: Find the B joker
  for i in range(len(deck)):
    if (deck[i] == 54):
      if (i == len(deck)-1):
        deck = deck[0:2] + [54] + deck[2:len(deck)-1]
        break
      elif (i == len(deck)-2):
        deck = deck[0:1] + [54] + deck[1:len(deck)-2] + [deck[len(deck)-1]]
        break
      else:
        deck = deck[:i] + deck[i+1:i+3] + [54] + deck[i+3:]
        break

  # step 3: Perform a triple cut
  a = -1
  b = -1
  for i in range(len(deck)):
    if (deck[i] == 53 or deck[i]== 54):
      if (a < 0): a = i
      else:
        b = i
        break
  deck = deck[b+1:] + deck[a:b+1] + deck[:a]

  # step 4: Perform a count cut
  bottom = deck[len(deck)-1]
  if (bottom <= 52): deck = deck[bottom:-1] + deck[:bottom] + [bottom]


  # step 5: Find the output card
  top = deck[0]
  if (top == 54): top = 53
  top = deck[top]

  if (top <= 52): return top%26
  return streamChar()


# key = secret deck
f = open("deck")
for card in f: deck.append(cardToNum(card.strip()))

encrypted = "GTIFLRVLEJTAVEYULDJOKCCOKP"
decrypted = ""

for c in encrypted:
  decr = ord(c)-65
  decr -= streamChar()
  decr = decr % 26
  decr = chr(decr+65)
  decrypted += decr

print(decrypted)

Running the script yields the password:

root@kali:~/Documents/he18/egg15# ./solitaire.py
THEPASSWORDISCRYPTONOMICON

The password is CRYPTONOMICON.

16 – git cloak –hard

The provided zip-archive repo.zip contains a few images and a .git folder:

root@kali:~/Documents/he18/egg16# unzip repo.zip
Archive:  repo.zip
   creating: .git/
   creating: .git/branches/
 extracting: .git/COMMIT_EDITMSG
  inflating: .git/config
  inflating: .git/description
 extracting: .git/HEAD
   creating: .git/hooks/
  inflating: .git/hooks/applypatch-msg.sample
  inflating: .git/hooks/commit-msg.sample
...
root@kali:~/Documents/he18/egg16# ls -al
total 3432
drwxr-xr-x 3 root root    4096 May  7 08:58 .
drwxr-xr-x 4 root root    4096 May  7 08:58 ..
-rwxrwx--- 1 root root  131816 Jan 23 05:43 01.jpg
-rwxrwx--- 1 root root   14089 Jan 23 05:43 02.png
-rwxrwx--- 1 root root   46846 Jan 23 05:43 03.jpg
-rwxrwx--- 1 root root   44226 Jan 23 05:43 05.jpg
-rwxrwx--- 1 root root   40417 Jan 23 05:43 06.jpg
-rwxrwx--- 1 root root  348680 Jan 23 05:43 07.png
-rwxrwx--- 1 root root    9260 Jan 23 05:43 08.png
-rwxrwx--- 1 root root   35368 Jan 23 05:43 09.jpg
-rwxrwx--- 1 root root   30336 Jan 23 05:43 10.jpg
-rwxrwx--- 1 root root  282284 Jan 23 05:43 11.png
drwxrwx--- 8 root root    4096 Jan 23 05:43 .git
-rw-r--r-- 1 root root 2497086 May  7 08:58 repo.zip

The egg in 02.png is not yet what we are looking for. When browsing through the different versions using git another egg can be found:

This is also not yet what we are looking for. When I did not find any other files browsing the different versions, I thought that if there is another egg, it must be stored in ./git/objects:

root@kali:~/Documents/he18/egg16# find .git/objects -type f
.git/objects/b9/e860f47fe6990cbda4ac5bb3d2829d2191f1eb
.git/objects/b9/820d55ce59799992648672a5a43fff4effd56b
.git/objects/22/8b603ed45ddaf1b1d3fe502e168fa2508ee5ed
.git/objects/38/39c14d2863fd850794661677352305ea798eb6
.git/objects/bc/83275bcea7da814743f1c478cb3a8771e0f1ac
.git/objects/9a/29769663d029f1b3ad83fec7e7f19ca1cf8e78
.git/objects/04/93a710296b7a684a46eed377029f7077622768
.git/objects/d0/c6562ce74c54358445fc3b6cc0584e32057ad5
.git/objects/c5/9568e4b945199366ad7ea486efbda76a07887c
.git/objects/e7/237971df563c82e85eb74a50ca41a218dc85ed
.git/objects/17/9ec0d76ecdc903ac12c4f3971efefbfb02aacb
.git/objects/9d/69bfb2dc3b3fd28389c7f709c3656e5c78c8c4
.git/objects/9d/7c9b5a1c8773ea48caac90d05401679b0a8897
.git/objects/be/98f627fa5d3251be77bbb7a64f5a34b6baf709
.git/objects/24/5735e32ed8174bdabe9655f00f1deb4ebaa3ad
.git/objects/d7/995a259ff0dfa28da06618d06e78b346738a6b
.git/objects/db/ab6618f6dc00a18b4195fb1bec5353c51b256f
.git/objects/da/cecafa877bd2346bd3a2c0a8a4026418491ccd
.git/objects/34/41837df545268c05da59d6d280a62a21343680
.git/objects/8d/9faa7ebaac3b61cc29cc309d11b923e9bcd5ab
.git/objects/f7/d946ebee06ee65e422530355c08ff2f06d456b
.git/objects/57/a17c1a44414c5973a7d967f2ca07eccf530ff4
.git/objects/5e/5e98caaa4ed1f6edee5aced3ff0b92457d6549
.git/objects/03/ed59cca1ea7ea0922d6fcbdb98c52931a8d3b0
.git/objects/0e/45662a541fee91d4652c5ba57300276eb7fa29
.git/objects/69/ee0b67f2701bc83cd64eec1e01c045c8a53bd3
.git/objects/74/1583e168e0723aef4ca0253f875a1f50144567
.git/objects/6f/5568ed00eb893db28616497f18749efd4bfd89

So I started using git cat-file to extract each of these files and see what is in there:

root@kali:~/Documents/he18/egg16# git cat-file -p 3839c14d2863fd850794661677352305ea798eb6 > ../tmp
root@kali:~/Documents/he18/egg16# file ../tmp
../tmp: ASCII text
root@kali:~/Documents/he18/egg16# cat ../tmp
tree bc83275bcea7da814743f1c478cb3a8771e0f1ac
parent 228b603ed45ddaf1b1d3fe502e168fa2508ee5ed
author PS <ps@hacking-lab.com> 1516704195 -0500
committer PS <ps@hacking-lab.com> 1516704195 -0500

more funny images added

root@kali:~/Documents/he18/egg16# git cat-file -p 9a29769663d029f1b3ad83fec7e7f19ca1cf8e78 > ../tmp
root@kali:~/Documents/he18/egg16# file ../tmp
../tmp: ASCII text
root@kali:~/Documents/he18/egg16# cat ../tmp
tree 5e5e98caaa4ed1f6edee5aced3ff0b92457d6549
parent 3839c14d2863fd850794661677352305ea798eb6
author PS <ps@hacking-lab.com> 1516704195 -0500
committer PS <ps@hacking-lab.com> 1516704195 -0500

branch created

...

Only text so far …

root@kali:~/Documents/he18/egg16# git cat-file -p 0493a710296b7a684a46eed377029f7077622768 > ../tmp
root@kali:~/Documents/he18/egg16# file ../tmp
../tmp: PNG image data, 480 x 480, 8-bit/color RGBA, non-interlaced

Not yet what we are looking for.

root@kali:~/Documents/he18/egg16# git cat-file -p d0c6562ce74c54358445fc3b6cc0584e32057ad5 > ../tmp
root@kali:~/Documents/he18/egg16# file ../tmp
../tmp: PNG image data, 480 x 480, 8-bit/color RGBA, non-interlaced

Keep on going …

root@kali:~/Documents/he18/egg16# git cat-file -p dbab6618f6dc00a18b4195fb1bec5353c51b256f > ../tmp
root@kali:~/Documents/he18/egg16# file ../tmp
../tmp: PNG image data, 480 x 480, 8-bit colormap, non-interlaced

There it is! 🙂

17 – Space Invaders

The challenge provides a text-file invaders_msg.txt containing unicode-encoded smileys:

Also, there is a hint that the message encoded in the text-file has been created using codemoji.org.

On the website a message can be entered, which is encrypted by selecting one of a few hundred smileys. The ciphertext is a series of smileys just like the provided invaders_msg.txt.

I was a little bit lucky solving this challenge, because before actually starting to understand the encryption-mechanism I decided to test a few smileys with the text abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 looking for smileys of the provided ciphertext.

As there are a few pixel-invaders on the image of the challenge, I also tried the following smiley and noticed the smileys from the encrypted message on the right side:

Since the mapping from characters to smileys is one-to-one, the only thing left doing was to see which smiley equals which character:

The password is invad3rsmustd13.

18 – Egg Factory

The provided file A.8xp is a program for the TI-83+ Graphing Calculator:

root@kali:~/Documents/he18/egg18# file A.8xp
A.8xp: TI-83+ Graphing Calculator (program)

I used a TI-83+ program (.8xp) Interpreter to disassemble the file:

The program seems to ask for a username and a password. But the interesting part is at the end of the output:

ClrDraw:AxesOff:expr(Str5)*0.01->A:Line(-1.7067137809187278*A,1.1201413427561837*A,-1.6042402826855124*A,0.7667844522968198*A):Line(-4.54,2.17,-4.08,2.57):Line(-1.441696113074205*A,0.9081272084805653*A,-1.2720848056537102*A,0.7526501766784451*A):Line(-3.6,2.13,-3.36,3.35):Circle(-1.96,2.67,0.5):Line(-0.56,3.41,-0.48,1.77):Line(-0.16961130742049468*A,0.6254416961130742*A,0.13427561837455831*A,0.8586572438162544*A):Line(0.38,2.43,1,2):Line(0.35335689045936397*A,0.7067137809187279*A,0.3886925795053004*A,1.1413427561837455*A):Line(0.5653710247349824*A,0.7455830388692579*A,1.1307420494699647*A,0.724381625441696*A):Line(1.2932862190812722*A,0.7526501766784451*A,1.2650176678445229*A,1.0848056537102473*A):Line(3.58,3.07,4.66,2.21)...

Obviously some lines and a circle are drawn here. Some of the coordinates are multiplied with the variable A, which has been initialized with Str5*0.01 (expr(Str5)*0.01->A). Str5 seems to be the entered password:

...
Disp "ENTER PASSWORD"
Input "",Str5
...

I decided to adapt the program for javascript in order to draw the lines and try different values for the value A:

<html>

<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;"></canvas>

<script>

function toPixel(x, min) {
  var r = x;
  if (min) r = -r;
  r = (r + 14)*8;
  return r;
}

var A = 280 * 0.01;

var arr = [
[-1.7067137809187278*A,1.1201413427561837*A,-1.6042402826855124*A,0.7667844522968198*A],
[-4.54,2.17,-4.08,2.57],
[-1.441696113074205*A,0.9081272084805653*A,-1.2720848056537102*A,0.7526501766784451*A],
[-3.6,2.13,-3.36,3.35],
[-0.56,3.41,-0.48,1.77],
[-0.16961130742049468*A,0.6254416961130742*A,0.13427561837455831*A,0.8586572438162544*A],
[0.38,2.43,1,2],
[0.35335689045936397*A,0.7067137809187279*A,0.3886925795053004*A,1.1413427561837455*A],
[0.5653710247349824*A,0.7455830388692579*A,1.1307420494699647*A,0.724381625441696*A],
[1.2932862190812722*A,0.7526501766784451*A,1.2650176678445229*A,1.0848056537102473*A],
[3.58,3.07,4.66,2.21],
[1.646643109540636*A,0.7420494699646644*A,1.7102473498233215*A,1.049469964664311*A],
[5.28,2.75,5.82,3.13],
[2.056537102473498*A,1.106007067137809*A,2.035335689045936*A,0.7879858657243816*A],
[7.12,3.17,6.28,2.85],
[2.219081272084806*A,1.0070671378091873*A,2.204946996466431*A,0.8091872791519434*A],
[6.24,2.29,7.18,2.15],
[9.5,3.69,8.3,3.61],
[2.932862190812721*A,1.2756183745583038*A,2.904593639575972*A,1.0353356890459364*A],
[8.22,2.93,9,3],
[8.22,2.93,8.12,2.15],
[2.869257950530035*A,0.7597173144876325*A,3.095406360424028*A,0.773851590106007*A],
[9.74,2.11,11.34,2.13],
[11.98,3.81,12,3],
[4.240282685512367*A,1.0600706713780919*A,4.240282685512367*A,0.7067137809187279*A],
[4.240282685512367*A,1.0600706713780919*A,4.515901060070671*A,1.0636042402826855*A],
[12.76,3.75,12.78,3.01],
[12.78,3.01,12.78,2.07],
[13.36,2.13,13.68,3.79],
[13.68,3.79,14.6,2.19],
[4.7773851590106*A,1.0141342756183747*A,4.946996466431095*A,1.0600706713780919*A],
[5.300353356890459*A,0.7067137809187279*A,5.795053003533568*A,1.226148409893993*A],
[15,3.81,16.4,2.07],
];

var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
for (var i = 0; i < arr.length; i++) {
  console.log(toPixel(arr[i][0]));
  ctx.moveTo(toPixel(arr[i][0]),toPixel(arr[i][1], true));
  ctx.lineTo(toPixel(arr[i][2]),toPixel(arr[i][3], true));
}
ctx.stroke();

ctx.beginPath();
ctx.arc(toPixel(-1.96), toPixel(2.67, true), 6, 0, 2 * Math.PI);
ctx.stroke();
</script>
</body>
</html>

It turned out to be quite easy, because the value for A can be adjusted gradually until a clear text is visible:

The password is WOW_N1CE_HAX.

19 – Virtual Hen

The provided file is a ELF 64-bit binary:

root@kali:~/Documents/he18/egg19# file create_egg
create_egg: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=ffe41b0a0fccc1f712e981f592ad454e929a681a, not stripped

When running the program a password is supposed to be entered:

root@kali:~/Documents/he18/egg19# ./create_egg
Enter password: test

After the password is entered, the program terminates without any further output. Within the program’s directory a new file called egg is created:

root@kali:~/Documents/he18/egg19# ls -al
total 52
drwxr-xr-x 2 root root  4096 May  7 06:45 .
drwxr-xr-x 5 root root  4096 May  7 06:45 ..
-rwxr-xr-x 1 root root 25032 May  7 06:45 create_egg
-rw-r--r-- 1 root root 15624 May  7 06:45 egg

The file does not seem to be in a valid format:

root@kali:~/Documents/he18/egg19# file egg
egg: data
root@kali:~/Documents/he18/egg19# hexdump -C egg | head
00000000  2c 4a 6e fb 0d 98 6e ee  00 d3 28 0f 32 23 7c 63  |,Jn...n...(.2#|c|
00000010  01 1e 69 ef 67 5a de 6c  a3 b8 ca 71 6f b2 bd 29  |..i.gZ.l...qo..)|
00000020  51 d3 84 2f d4 e8 01 24  4f fd 89 9b 24 bc 3a 3b  |Q../...$O...$.:;|
00000030  ee 14 65 a1 22 ba b9 a7  8d 70 6d a7 91 b4 9e 04  |..e."....pm.....|
00000040  cf ca 39 39 0f 03 d0 f5  4a 80 36 7a 9b c8 40 b4  |..99....J.6z..@.|
00000050  8a 3c 00 ff 3a 5a c5 08  76 60 9a a6 55 0b 91 73  |.<..:Z..v`..U..s|
00000060  77 79 f3 e7 50 3f b9 59  33 ee 28 a5 80 27 50 62  |wy..P?.Y3.(..'Pb|
00000070  bd 3e a4 52 fd e9 54 cf  3a 4d 0c 81 f9 da 10 20  |.>.R..T.:M..... |
00000080  5d 25 15 51 be 34 bb 9f  45 cd e2 ce b8 68 47 cf  |]%.Q.4..E....hG.|
00000090  cd d5 44 e8 5e 15 d2 9c  a6 0b fa de 97 0a 2b 01  |..D.^.........+.|

Let’s start reversing the binary using r2:

root@kali:~/Documents/he18/egg19/bla# r2 -A create_egg
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[0x00400820]>

List all functions:

[0x00400820]> afl
0x00400000    3 73   -> 75   sym.imp.__libc_start_main
0x004005f8    3 23           sym._init
0x00400620    2 16   -> 32   sym.imp.fclose
0x00400630    2 16   -> 48   sym.imp.__stack_chk_fail
0x00400640    2 16   -> 48   sym.imp.memcpy
0x00400650    2 16   -> 48   sym.imp.malloc
0x00400660    2 16   -> 48   sym.imp.__printf_chk
0x00400670    2 16   -> 48   sym.imp.fopen
0x00400680    2 16   -> 48   sym.imp.getline
0x00400690    2 16   -> 48   sym.imp.fwrite
0x004006a0   13 381          main
0x00400820    1 43           entry0
0x00400850    3 35           sym.deregister_tm_clones
0x00400880    3 53           sym.register_tm_clones
0x004008c0    3 34   -> 29   sym.__do_global_dtors_aux
0x004008f0    1 7            entry1.init
0x00400900    3 97           sym.d
0x00400970    4 101          sym.__libc_csu_init
0x004009e0    1 2            sym.__libc_csu_fini
0x004009e4    1 9            sym._fini

Disassemble the main function:

[0x00400820]> pdf @ main
            ;-- section_end..plt:
            ;-- section..text:
            ;-- main:
/ (fcn) main 381
|   main ();
|           ; var int local_8h @ rsp+0x8
|           ; var int local_10h @ rsp+0x10
|           ; var int local_14h @ rsp+0x14
|           ; var int local_18h @ rsp+0x18
|           ; var int local_1ch @ rsp+0x1c
|           ; var int local_28h @ rsp+0x28
|              ; DATA XREF from 0x0040083d (entry0)
|           0x004006a0      4154           push r12                    ; section 13 va=0x004006a0 pa=0x000006a0 sz=834 vsz=834 rwx=--r-x .text
|           0x004006a2      55             push rbp
|           0x004006a3      bf083d0000     mov edi, 0x3d08
|           0x004006a8      53             push rbx
|           0x004006a9      4883ec30       sub rsp, 0x30               ; '0'
...

Before the password is read, the content which is later written in the file egg is copied using memcpy. This content is stored in obj.c:

...
|           0x004006c2      ba083d0000     mov edx, 0x3d08
|           0x004006c7      be200a4000     mov esi, obj.c              ; 0x400a20
|           0x004006cc      4889c7         mov rdi, rax
|           0x004006cf      4989c4         mov r12, rax
|           0x004006d2      e869ffffff     call sym.imp.memcpy         ; void *memcpy(void *s1, const void *s2, size_t n)
...
[0x00400820]> px @ obj.c
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x00400a20  50cb b5d5 6483 4ffe e791 fc42 3fb9 118e  P...d.O....B?...
0x00400a30  6c0a 5c98 12c8 f4b9 0acd b1eb ea80 2757  l.\...........'W
0x00400a40  5092 c5b5 af6c fea2 565b 428e 7b22 a1f5  P....l..V[B.{"..
0x00400a50  b8c6 9077 03e3 0d65 6655 8c94 5254 2f8e  ...w...efU..RT/.
0x00400a60  0031 284a b249 1257 122e 70b3 1b0a 233a  .1(J.I.W..p...#:
0x00400a70  113d 6cd0 e389 b3aa 37ed f67c e7d2 070a  .=l.....7..|....
0x00400a80  1f7a 2108 e19b 1448 2fac ffa4 998b a1c1  .z!....H/.......
...

The password is read using the function getline:

...
|           0x00400708      e873ffffff     call sym.imp.getline
...

Only the first 8 characters of the password are processed. Each of these characters is ANDed with 0xffffffdf and ORed with 0x40:

|       |   0x0040071c      48c744240808.  mov qword [local_8h], 8
|       |      ; JMP XREF from 0x0040080d (main)
|      .--> 0x00400725      31d2           xor edx, edx
|      :|   0x00400727      660f1f840000.  nop word [rax + rax]
|      :|      ; JMP XREF from 0x0040074e (main)
|     .---> 0x00400730      488b0c24       mov rcx, qword [rsp]
|     ::|   0x00400734      4801d1         add rcx, rdx                ; '('
|     ::|   0x00400737      4883c201       add rdx, 1
|     ::|   0x0040073b      0fb601         movzx eax, byte [rcx]
|     ::|   0x0040073e      83e0df         and eax, 0xffffffdf
|     ::|   0x00400741      83c840         or eax, 0x40                ; '@'
|     ::|   0x00400744      8801           mov byte [rcx], al
|     ::|   0x00400746      488b4c2408     mov rcx, qword [local_8h]   ; [0x8:8]=-1 ; 8
|     ::|   0x0040074b      4839d1         cmp rcx, rdx
|     `===< 0x0040074e      77e0           ja 0x400730

This means that the third MSB is always 0 and the second MSB is always 1:

            01234567
character = ????????
        AND 11011111 (0xdf)
        OR  01000000 (0x40)
        -------------------
            ?10?????

Since the password is probably ASCII, which means that the MSB is 0, there are 5 unknown bits in each character.

By single-stepping through the main function using gdb we can see that 8 characters of the password are processed, even if the actual password entered is smaller:

[-------------------------------------code-------------------------------------]
   0x400741 <main+161>: or     eax,0x40
   0x400744 <main+164>: mov    BYTE PTR [rcx],al
   0x400746 <main+166>: mov    rcx,QWORD PTR [rsp+0x8]
=> 0x40074b <main+171>: cmp    rcx,rdx
   0x40074e <main+174>: ja     0x400730 <main+144>
   0x400750 <main+176>: mov    rsi,QWORD PTR [rsp]
   0x400754 <main+180>: xor    edx,edx
   0x400756 <main+182>: lea    rbp,[r12+0x3d08]
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe470 --> 0x60a380 ("TESTJ@@@")
0008| 0x7fffffffe478 --> 0x8
0016| 0x7fffffffe480 --> 0x7ffff7de70e0 (<_dl_fini>:    push   rbp)
0024| 0x7fffffffe488 --> 0x0
0032| 0x7fffffffe490 --> 0x400970 (<__libc_csu_init>:   push   r15)
0040| 0x7fffffffe498 --> 0xca0b6a3337f88000
0048| 0x7fffffffe4a0 --> 0x0
0056| 0x7fffffffe4a8 --> 0x400970 (<__libc_csu_init>:   push   r15)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000000000040074b in main ()
gdb-peda$

I entered test as the password, which is converted to TESTJ@@@ because of the newline at the end of the input (0x0a & 0xdf | 0x40 = 0x4a ('J')) and the following three zero-bytes (0x00 & 0xdf | 0x40 = 0x40 ('@')).

The 8 converted characters are copied at the end of the password, resulting in a total amount of 16 bytes:

[-------------------------------------code-------------------------------------]
   0x40078e <main+238>: div    rcx
   0x400791 <main+241>: mov    eax,DWORD PTR [rsi+rdx*1]
   0x400794 <main+244>: mov    DWORD PTR [rsp+0x1c],eax
=> 0x400798 <main+248>: nop    DWORD PTR [rax+rax*1+0x0]
   0x4007a0 <main+256>: lea    rsi,[rsp+0x10]
   0x4007a5 <main+261>: mov    rdi,rbx
   0x4007a8 <main+264>: add    rbx,0x8
   0x4007ac <main+268>: call   0x400900 <d>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe470 --> 0x60a380 ("TESTJ@@@")
0008| 0x7fffffffe478 --> 0x8
0016| 0x7fffffffe480 ("TESTJ@@@TESTJ@@@p\t@")
0024| 0x7fffffffe488 ("TESTJ@@@p\t@")
0032| 0x7fffffffe490 --> 0x400970 (<__libc_csu_init>:   push   r15)
0040| 0x7fffffffe498 --> 0x828407aaaa32e100
0048| 0x7fffffffe4a0 --> 0x0
0056| 0x7fffffffe4a8 --> 0x400970 (<__libc_csu_init>:   push   r15)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0000000000400798 in main ()
gdb-peda$

In this case the converted password is TESTJ@@@TESTJ@@@.

At next the function d is called passing the converted password as well as the first block of the encoded content from obj.c as arguments:

[-------------------------------------code-------------------------------------]
   0x4007a0 <main+256>: lea    rsi,[rsp+0x10]
   0x4007a5 <main+261>: mov    rdi,rbx
   0x4007a8 <main+264>: add    rbx,0x8
=> 0x4007ac <main+268>: call   0x400900 <d>
   0x4007b1 <main+273>: cmp    rbx,rbp
   0x4007b4 <main+276>: jne    0x4007a0 <main+256>
   0x4007b6 <main+278>: mov    esi,0x400a15
   0x4007bb <main+283>: mov    edi,0x400a17
Guessed arguments:
arg[0]: 0x606260 --> 0xfe4f8364d5b5cb50
arg[1]: 0x7fffffffe480 ("TESTJ@@@TESTJ@@@p\t@")
arg[2]: 0x4
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe470 --> 0x60a380 ("TESTJ@@@")
0008| 0x7fffffffe478 --> 0x8
0016| 0x7fffffffe480 ("TESTJ@@@TESTJ@@@p\t@")
0024| 0x7fffffffe488 ("TESTJ@@@p\t@")
0032| 0x7fffffffe490 --> 0x400970 (<__libc_csu_init>:   push   r15)
0040| 0x7fffffffe498 --> 0x828407aaaa32e100
0048| 0x7fffffffe4a0 --> 0x0
0056| 0x7fffffffe4a8 --> 0x400970 (<__libc_csu_init>:   push   r15)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00000000004007ac in main ()
gdb-peda$

I started by reversing the function in python-like pseudocode:

function d(data, copied_pwd_new) {

  ecx = [data];
  edx = [data+4];
  r11d = copied_pwd_new;
  r10d = &copied_pwd_new[4];
  r9d = &copied_pwd_new[8];
  r8d = &copied_pwd_new[0xc];
  
  esi = 0xc6ef3720;
 _400920:
  eax = ((ecx<<4)+r9d) ^ ((ecx>>5)+r8d);
  ebx = ecx+esi;
  eax ^= ebx;
  edx -= eax;
  
  eax = ((edx<<4)+r11d) ^ ((ecx>>5)+r10d);
  ebx = edx+esi;
  eax ^= ebx;
  ecx -= eax;
  
  esi += 0x61c88647
  
  if(esi != 0) jmp 400920;
  [data] = ecx;
  [data+4] = edx;
}

Googling for the constant 0xc6ef3720 I figured out, that the algorithm is a modified version of the Tiny Encryption Algorithm (TEA).

The algorithm uses a 128-bit (=16 byte) key (4*4 bytes in r8d-r11d). Bruteforcing a 128-bit key would not be an option. But we know that the key is the converted password we found. This password is limited to 8 characters with 5 unknown bits each. This results in the total amount of 2^(5*8) = 2^40 = 1.099.511.627.776 possible passwords. Still a lot of passwords, but bruteforcing this is definitely an option.

Before we can start to bruteforce the password, we need to know what we are actually looking for. Since the egg file, which is created by the program, is probably an image, I decided to look for a valid PNG-header …

PNG-header
89 50 4e 47 0d 0a 1a 0a

… and wrote a bruteforce-program in C:

root@kali:~/Documents/he18/egg19# cat bruteforce.c
#include <stdio.h>
#include <stdint.h>

void decrypt (uint32_t* v, uint32_t* k) {
  uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i;
  uint32_t delta=0x61c88647;
  for (i=0; i<32; i++) {
      v1 -= ((v0<<4) + k[0]) ^ (v0 + sum) ^ ((v0>>5) + k[1]);
      v0 -= ((v1<<4) + k[0]) ^ (v1 + sum) ^ ((v1>>5) + k[1]);
      sum += delta;
  }
  v[0]=v0; v[1]=v1;
}


int main() {
  uint32_t data[] = {0xd5b5cb50, 0xfe4f8364};
  uint32_t key[]  = {0x41414141, 0x41414141};
  uint64_t x = 0;
  for (x = 0; x < 1099511627776; x++) {

    key[0] = 0x40404040; key[1] = 0x40404040;
    data[0] = 0xd5b5cb50; data[1] = 0xfe4f8364;

    int i;
    uint32_t lastLetter = 0;
    char bSet = 0x00;
    for (i = 7; i >= 0; i--) {
      uint32_t tmp = ((x>>(i*5)) & 0x1f) << ((i%4)*8);
      if (tmp > 0 && !bSet) {
        bSet = 0x01;
        lastLetter = i+1;
      }
      key[(i>=4)] += tmp;
    }
    key[(lastLetter>=4)] += (0x0a << (lastLetter*8));

    decrypt(data,key);
    // PNG --> 89 50 4e 47 0d 0a 1a 0a
    if (data[0] == 0x474e5089 && data[1] == 0x0a1a0a0d) {
      printf("got png!\n");
      printf("%.8x %.8x\n", key[1], key[0]);
      printf("%.8x %.8x\n", data[1], data[0]);
      printf("x=%lld\n", x);
      return 1;
    }
    if (x%5000000 == 0) printf("%.8x %.8x (%lld)\n", key[1], key[0], x);
  }

  printf("%.8x %.8x\n", data[0], data[1]);
  return 0;
}

The program takes the first 16 bytes of data from obj.c, generates the next possible password based on the loop-counter x, decrypts the data and tests if the decrypted data is a valid PNG-header.

Running the program:

root@kali:~/Documents/he18/egg19# gcc bruteforce.c
root@kali:~/Documents/he18/egg19# ./a.out
40404040 4040404a (0)
40404a44 58525a40 (5000000)
40404a49 51455440 (10000000)
40404a4e 49584e40 (15000000)
40404a53 424b4840 (20000000)
40404a57 5a5e4240 (25000000)
40404a5c 53505c40 (30000000)
404a4141 4c435640 (35000000)
404a4146 44565040 (40000000)
404a414a 5d494a40 (45000000)
404a414f 555c4440 (50000000)
404a4154 4e4e5e40 (55000000)
404a4159 47415840 (60000000)
404a415d 5f545240 (65000000)
404a4242 58474c40 (70000000)
404a4247 505a4640 (75000000)
404a424c 494d4040 (80000000)
404a4251 415f5a40 (85000000)
404a4255 5a525440 (90000000)
...

After running for about two days, the program finally stopped:

...
47474467 5e5c5040 (248200000000)
4747454c 574f4a40 (248205000000)
47474551 50424440 (248210000000)
47474556 48545e40 (248215000000)
got png!
47474559 4b434048
0a1a0a0d 474e5089
x=248218225672

Converting the hex-values to ASCII:

root@kali:~/Documents/he18/egg19# python
>>> x = "474745594b434048"
>>> x.decode("hex")[::-1]
'H@CKYEGG'

Using this password, the egg is a valid PNG-file:

root@kali:~/Documents/he18/egg19# ./create_egg
Enter password: H@CKYEGG
root@kali:~/Documents/he18/egg19# file egg
egg: PNG image data, 480 x 480, 8-bit colormap, non-interlaced

Done:

20 – Artist: No Name Yet

Unfortunately I could not spend as much time as I would have needed to solve this challenge. Nevertheless, here is what I got so far:

The provided zip-archive artist.zip contains a pdf- and midi-file:

root@kali:~/Documents/he18/egg20# unzip artist.zip
Archive:  artist.zip
  inflating: nonameyet.mid
  inflating: sheet.pdf

The pdf-file sheet.pdf contains some notes, which are actually a hidden text, which can be revealed using pdftotext:

root@kali:~/Documents/he18/egg20# pdftotext sheet.pdf
root@kali:~/Documents/he18/egg20# cat sheet.txt
Composition

No Name Yet

�Okay, let’s do the information exchange as we coordinated. First let me
tell you: hiding informations in a MIDI file will be popular soon! We should
only do it this way to stay covered. MIDI hiding is just next level – wow! So,
here are all informations you need to find the secret: Trackline: Can’t remember now,
but you’ll find it. It’s kinda quiet this time, because of the doubled protection
algorithm! Characters: 0 - 127 (by the way: we won‘t need the higher ones
ever…)Let’s go!�
I‘m very exited for the lyrics that you will create
for this masterpiece.
Best wishes, your friend

LuckyTail

Thus the secret message is hidden in the midi-file nonameyet.mid.

I started by parsing the midi-messages using the mido python module:

root@kali:~/Documents/he18/egg20# cat midi.py
#!/usr/bin/env python

import sys
from mido import MidiFile

mid = MidiFile(sys.argv[1])

for i, track in enumerate(mid.tracks):
    print('Track {}: {}'.format(i, track.name))
    cc = ""
    for msg in track:
        print(msg)

Running the script displays all midi-messages within the given file:

root@kali:~/Documents/he18/egg20# ./midi.py nonameyet.mid | head -n200
Track 0: test
<meta message track_name name=u'test' time=0>
<meta message set_tempo tempo=666666 time=0>
<meta message time_signature numerator=4 denominator=4 clocks_per_click=24 notated_32nd_notes_per_beat=8 time=0>
<meta message end_of_track time=0>
Track 1: MIDI 01
<meta message track_name name=u'MIDI 01' time=0>
<meta message end_of_track time=0>
Track 2: MIDI 02
<meta message track_name name=u'MIDI 02' time=0>
<meta message end_of_track time=0>
Track 3: Synth Brass 1
<meta message track_name name=u'Synth Brass 1' time=0>
program_change channel=1 program=62 time=1016
control_change channel=1 control=7 value=111 time=8
control_change channel=1 control=120 value=0 time=0
control_change channel=1 control=121 value=0 time=0
control_change channel=1 control=7 value=111 time=0
note_on channel=1 note=66 velocity=81 time=72992
control_change channel=1 control=0 value=22 time=48
control_change channel=1 control=0 value=3 time=8
control_change channel=1 control=0 value=0 time=16
note_off channel=1 note=66 velocity=64 time=1680
note_on channel=1 note=59 velocity=81 time=40
control_change channel=1 control=0 value=14 time=16
control_change channel=1 control=0 value=0 time=8
note_off channel=1 note=59 velocity=64 time=408
...

The secret message must be hidden in some of the message-values (velocity, control_change, …?).

21 – Hot Dog

The provided zip-archive contains a TIFF-image called hotdog.jpg:

root@kali:~/Documents/he18/egg21# unzip hotdog.zip
Archive:  hotdog.zip
  inflating: hotdog.jpg
  
root@kali:~/Documents/he18/egg21# file hotdog.jpg
hotdog.jpg: TIFF image data, little-endian, direntries=27, height=2067, bps=338, compression=none, PhotometricIntepretation=RGB, description=*Don't forget to delete this*, manufacturer=Panasonic, model=DMC-FZ18, orientation=upper-left, width=2700

Using exiftool we can see that there is a RSA public key with the comment *Don't forget to delete this* in the image description, as well as in a layer’s description and caption-abstract:

root@kali:~/Documents/he18/egg21# exiftool hotdog.jpg
ExifTool Version Number         : 10.80
File Name                       : hotdog.jpg
Directory                       : .
File Size                       : 33 MB
File Modification Date/Time     : 2018:02:28 08:26:44-05:00
File Access Date/Time           : 2018:05:06 13:09:39-04:00
File Inode Change Date/Time     : 2018:05:06 13:09:38-04:00
File Permissions                : rw-r--r--
File Type                       : TIFF
File Type Extension             : tif
MIME Type                       : image/tiff
Exif Byte Order                 : Little-endian (Intel, II)
Subfile Type                    : Full-resolution Image
Image Width                     : 2700
Image Height                    : 2067
Bits Per Sample                 : 8 8 8
Compression                     : Uncompressed
Photometric Interpretation      : RGB
Image Description               : *Don't forget to delete this*..-----BEGIN PUBLIC KEY-----.MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKBgQTMleqB9nvRKhTnR4/2BDDU.g5hkjbRQygvrZWDATbC9rXxCAqaegim2XUlD8yVxYkyzJZxmAYba7qLTe3bctocM.L7GXdMf3kQiVLPigN2auEiPFreWZvZ/b4FzcvOhh+SprypAkYn9SapTyGzLdpYdD.TyoWFRT7QgEhIsDGcncsXQKBgQCVbdUZa5uQ7O9bgu2WPvUwwvuI+ZK5gOZCF299.1QRa/rdDHKyYiUxxZXjemxGICxvoC698wVvmVqzG/sCT+iLArIh4OmSHgyd1yjcA.CWmsffHYLvsl3tnN9Jiu5qzN6aGthHjK/424NK0RkfjUdmnQydYN/MqfS7c+AkfJ.QWV/9w==.-----END PUBLIC KEY-----.
Make                            : Panasonic
Camera Model Name               : DMC-FZ18
Strip Offsets                   : 28836
...

Opening the file in gimp raises multiple errors and shows an image displaying some sausages:

When inspecting the file with tiffinfo, we can see that there is an unknown field with tag 0x935c and that there seem to be a photoshop text-layer containing the text That‘s the flag :) For real! Wasn‘t that simple?. The end of the file is not correctly parsed and the raw bytes are printed as hex-values.

root@kali:~/Documents/he18/egg21# tiffinfo hotdog.jpg | head -c 8000
TIFFReadDirectory: Warning, Unknown field with tag 37724 (0x935c) encountered.
TIFFFetchNormalTag: Warning, Incompatible type for "RichTIFFIPTC"; tag ignored.
TIFF Directory at offset 0x8 (8)
  Subfile Type: (0 = 0x0)
  Image Width: 2700 Image Length: 2067
  Resolution: 72, 72 pixels/inch
  ...
  Make: Panasonic
  Model: DMC-FZ18
  Software: Adobe Photoshop CS6 (Macintosh)
  DateTime: 2018:02:27 19:19:33
  Artist: xorkiwi
  XMLPacket (XMP Metadata):
  ...
      <rdf:Description rdf:about=""
            xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
         <photoshop:LegacyIPTCDigest>00000000000000000000000000000001</photoshop:LegacyIPTCDigest>
         <photoshop:DateCreated>2009-12-16T15:58:44</photoshop:DateCreated>
         <photoshop:ColorMode>3</photoshop:ColorMode>
         <photoshop:TextLayers>
            <rdf:Bag>
               <rdf:li rdf:parseType="Resource">
                  <photoshop:LayerName>That‘s the flag :) For real! Wasn‘t that simple?</photoshop:LayerName>
                  <photoshop:LayerText>That‘s the flag :) For real! Wasn‘t that simple?</photoshop:LayerText>
               </rdf:li>
            </rdf:Bag>
         </photoshop:TextLayers>
         ...

Googling for the unknown field tag 0x935c revealed that this tag is created by photoshop. Thus I decided to try converting the file to a psd file:

root@kali:~/Documents/he18/egg21# convert hotdog.jpg hotdog.psd
convert-im6.q16: Incompatible type for "RichTIFFIPTC"; tag ignored. `TIFFFetchNormalTag' @ warning/tiff.c/TIFFWarnings/912.
convert-im6.q16: Incompatible type for "FileSource"; tag ignored. `TIFFFetchNormalTag' @ warning/tiff.c/TIFFWarnings/912.
convert-im6.q16: Incompatible type for "SceneType"; tag ignored. `TIFFFetchNormalTag' @ warning/tiff.c/TIFFWarnings/912.
convert-im6.q16: Wrong data type 3 for "GainControl"; tag ignored. `TIFFReadCustomDirectory' @ warning/tiff.c/TIFFWarnings/912.
convert-im6.q16: Incompatible type for "RichTIFFIPTC"; tag ignored. `TIFFFetchNormalTag' @ warning/tiff.c/TIFFWarnings/912.

Yet again some errors are raised, but the file can be opened in gimp:

Now we can see that there are three layers:

  1. the sausages (Wow)
  2. a text layer: That‘s the flag 🙂 For real! Wasn‘t that simple?
  3. a red egg with a qrcode

Using the online decoder zxing.org we can see that the qrcode contains the following string:

Arf3ThIY8VQg2GUd249wzDYi7CXqTST+9g4Q7bbT2eF+mD2KB+6oi3rVSY/eZ6/onNBNYPo2BPqIVEbL35G62pIHvabGcrYosGCpYhiz6EYnamnNPrHdzmEOs8lCRw1c2Pe8kl41FH0ud7tBn6qD/stnZfGkcbeIrjaSiIYSveHS

base64 🙂

root@kali:~/Documents/he18/egg21# echo "Arf3ThIY8VQg2GUd249wzDYi7CXqTST+9g4Q7bbT2eF+mD2KB+6oi3rVSY/eZ6/onNBNYPo2BPqIVEbL35G62pIHvabGcrYosGCpYhiz6EYnamnNPrHdzmEOs8lCRw1c2Pe8kl41FH0ud7tBn6qD/stnZfGkcbeIrjaSiIYSveHS" | base64 -d
\â–’â–’^5}.wâ–’Aâ–’â–’â–’â–’â–’geâ–’qâ–’â–’â–’6â–’â–’â–’â–’â–’â–’

Does not seem to be ASCII. The text layer states that this should be the egg. So it can maybe be decrypted with the RSA public key, we already found. Let’s store the key in a file rsa.pub:

root@kali:~/Documents/he18/egg21# echo "-----BEGIN PUBLIC KEY-----.MIIBIDANBgkqhkiG9...qfS7c+AkfJ.QWV/9w==.-----END PUBLIC KEY-----" | tr '.' '\n' > rsa.pub

The RSA public key parameters (N and e) can be parsed using openssl:

root@kali:~/Documents/he18/egg21# openssl asn1parse -i -in rsa.pub
    0:d=0  hl=4 l= 288 cons: SEQUENCE
    4:d=1  hl=2 l=  13 cons:  SEQUENCE
    6:d=2  hl=2 l=   9 prim:   OBJECT            :rsaEncryption
   17:d=2  hl=2 l=   0 prim:   NULL
   19:d=1  hl=4 l= 269 prim:  BIT STRING
   
root@kali:~/Documents/he18/egg21# openssl asn1parse -i -in rsa.pub -strparse 19
    0:d=0  hl=4 l= 264 cons: SEQUENCE
    4:d=1  hl=3 l= 129 prim:  INTEGER           :04CC95EA81F67BD12A14E7478FF60430D48398648DB450CA0BEB6560C04DB0BDAD7C4202A69E8229B65D4943F32571624CB3259C660186DAEEA2D37B76DCB6870C2FB19774C7F79108952CF8A03766AE1223C5ADE599BD9FDBE05CDCBCE861F92A6BCA9024627F526A94F21B32DDA587434F2A161514FB42012122C0C672772C5D
  136:d=1  hl=3 l= 129 prim:  INTEGER           :956DD5196B9B90ECEF5B82ED963EF530C2FB88F992B980E642176F7DD5045AFEB7431CAC98894C716578DE9B11880B1BE80BAF7CC15BE656ACC6FEC093FA22C0AC88783A6487832775CA37000969AC7DF1D82EFB25DED9CDF498AEE6ACCDE9A1AD8478CAFF8DB834AD1191F8D47669D0C9D60DFCCA9F4BB73E0247C941657FF7

The first integer (04CC95...) is N and the second one (956DD5..) is e.

At next we convert N to a decimal number …

root@kali:~/Documents/he18/egg21# python -c 'print(0x04CC95EA81F67...2C5D)'
8627421546422318392454906523054...995677

… and see if there is a factorization on factordb.com:

Great! We have got the two primes p and q (N = p * q). With those two values we can calculate phi(N) = (p-1) * (q-1), the multiple inverse d and try to decrypt the data from the qrcode:

root@kali:~/Documents/he18/egg21# cat decrypt.py
#!/usr/bin/env python

from Crypto.PublicKey import RSA
from fractions import gcd
import gmpy2
import base64

# N and e from openssl output
N = 0x04CC95EA81F67BD12A14E7478FF60430D48398648DB450CA0BEB6560C04DB0BDAD7C4202A69E8229B65D4943F32571624CB3259C660186DAEEA2D37B76DCB6870C2FB19774C7F79108952CF8A03766AE1223C5ADE599BD9FDBE05CDCBCE861F92A6BCA9024627F526A94F21B32DDA587434F2A161514FB42012122C0C672772C5D
e = 0x956DD5196B9B90ECEF5B82ED963EF530C2FB88F992B980E642176F7DD5045AFEB7431CAC98894C716578DE9B11880B1BE80BAF7CC15BE656ACC6FEC093FA22C0AC88783A6487832775CA37000969AC7DF1D82EFB25DED9CDF498AEE6ACCDE9A1AD8478CAFF8DB834AD1191F8D47669D0C9D60DFCCA9F4BB73E0247C941657FF7

# p and q factors for N from factordb.com
p = 21787995226958172829467888206490681114003213044856067031128998135742112625134255635772352085743308949466567934785458002652816217408595135233580400606278413
q = 39597133451487334277950950530003861952885112404500618298702299905566831117666470098035890477572068210683971280104304184580469417440656443567196733216950929

# phi(N)
phi=(p-1)*(q-1)

# ciphertext from qrcode
ct = 'Arf3ThIY8VQg2GUd249wzDYi7CXqTST+9g4Q7bbT2eF+mD2KB+6oi3rVSY/eZ6/onNBNYPo2BPqIVEbL35G62pIHvabGcrYosGCpYhiz6EYnamnNPrHdzmEOs8lCRw1c2Pe8kl41FH0ud7tBn6qD/stnZfGkcbeIrjaSiIYSveHS'
ct = base64.b64decode(ct)

# multiple invers
d   = long(gmpy2.divm(1, e, phi))

rsa = RSA.construct((N,e,d,p,q))
pt  = rsa.decrypt(ct)
print pt

Running the script yields the password:

root@kali:~/Documents/he18/egg21# ./decrypt.py
xâ–’e1â–’CAâ–’â–’sZJâ–’â–’â–’â–’ffâ–’â–’â–’â–’Fâ–’:"0â–’9;
▒▒▒̬▒5▒▒g~Great job haxxor, here's your flag: {b3w4r3_0f_c0n71nu3d_fr4c710n5}

The password is b3w4r3_0f_c0n71nu3d_fr4c710n5.

22 – Block Jane

The challenge provides an encrypted message …

root@kali:~/Documents/he18/egg22# hexdump -C secret.enc
00000000  e3 43 f4 26 04 ca 58 a7  31 ad bf 10 b3 76 ee 33  |.C.&..X.1....v.3|
00000010  aa 94 49 26 cd f9 54 40  0d 86 ee 4f 6e 35 77 4e  |..I&..T@...On5wN|
00000020  c5 10 fe 57 67 ba ba 99  a3 ed 28 fa 26 dc 99 b6  |...Wg.....(.&...|
00000030  c1 da dd 08 7e 4c ee 27  e4 55 07 00 52 76 c1 0f  |....~L.'.U..Rv..|
00000040  d9 c1 5f 27 d3 48 1a 92  f3 4d d4 64 77 f7 be 3c  |.._'.H...M.dw..<|
00000050

… and a server which accepts these messages:

root@kali:~/Documents/he18/egg22# cat secret.enc | nc whale.hacking-lab.com 5555
ok

Other messages are not accepted:

root@kali:~/Documents/he18/egg22# head /dev/urandom -c 80 | nc whale.hacking-lab.com 5555
error

The challenge description states that AES was used to encrypt the message. Also the title of the challenge Block Jane suggests that AES was used in Cipher Block Chaining (CBC) mode. After a little bit of googling I stumbled upon the Padding oracle attack:


In cryptography, a padding oracle attack is an attack which uses the padding validation of a cryptographic message to decrypt the ciphertext. In cryptography, variable-length plaintext messages often have to be padded (expanded) to be compatible with the underlying cryptographic primitive. The attack relies on having a “padding oracle” who freely responds to queries about whether a message is correctly padded or not. Padding oracle attacks are mostly associated with CBC mode decryption used within block ciphers.

AES CBC encrypts a message in blocks of 16 bytes. If the length of the message is not aligned to 16 (len(msg) % 16 != 0) it must be padded with fill-bytes. The attack leverages the fact that the server raises an error, if the padding of the message sent is not correct. The default padding PKCS7 described in RFC 2315 is quite simple. The bytes which are added for padding are set to the count of total bytes added:

Padding 1 byte:
XX XX XX XX   XX XX XX XX   XX XX XX XX   XX XX XX 01

Padding 3 bytes:
XX XX XX XX   XX XX XX XX   XX XX XX XX   XX 03 03 03

Padding 10 bytes:
XX XX XX XX   XX XX 0a 0a   0a 0a 0a 0a   0a 0a 0a 0a

In CBC mode a decrypted ciphertext block is XOR-ed with the previous ciphertext-block in order to restore the plaintext of the block:

To carry out the padding oracle attack we take the first two 16 byte-blocks (c1 and c2) of the encrypted message and change the last byte of the first block. If the server accepts the message, we know that the last byte was decrypted to 01, because it was padded correctly:

root@kali:~/Documents/he18/egg22# head secret.enc -c32 | hexdump -C
00000000  e3 43 f4 26 04 ca 58 a7  31 ad bf 10 b3 76 ee 33  |.C.&..X.1....v.3|  <-- c1
00000010  aa 94 49 26 cd f9 54 40  0d 86 ee 4f 6e 35 77 4e  |..I&..T@...On5wN|  <-- c2

root@kali:~/Documents/he18/egg22# echo -en $(head secret.enc -c15)"\x00"$(head secret.enc -c32 | tail -c16) | hexdump -C
00000000  e3 43 f4 26 04 ca 58 a7  31 ad bf 10 b3 76 ee 00  |.C.&..X.1....v..|
00000010  aa 94 49 26 cd f9 54 40  0d 86 ee 4f 6e 35 77 4e  |..I&..T@...On5wN|
00000020

root@kali:~/Documents/he18/egg22# echo -en $(head secret.enc -c15)"\x00"$(head secret.enc -c32 | tail -c16) | nc whale.hacking-lab.com 5555
error

...

root@kali:~/Documents/he18/egg22# echo -en $(head secret.enc -c15)"\x51"$(head secret.enc -c32 | tail -c16) | hexdump -C
00000000  e3 43 f4 26 04 ca 58 a7  31 ad bf 10 b3 76 ee 51  |.C.&..X.1....v.Q|
00000010  aa 94 49 26 cd f9 54 40  0d 86 ee 4f 6e 35 77 4e  |..I&..T@...On5wN|
00000020

root@kali:~/Documents/he18/egg22# echo -en $(head secret.enc -c15)"\x51"$(head secret.enc -c32 | tail -c16) | nc whale.hacking-lab.com 5555
ok

When the last byte of c1 is 0x51, the server accepts the message. If you review the picture of the CBC mode above, the plaintext of c2 (p2) is p2 = decrypt(c2) ^ c1. Since the server accepted our message, the padding must be correct and thus the last byte of p2 must be 0x01 (1 byte padding): p2[15] = decrypt(c2)[15] ^ c1[15] = decrypt(c2)[15] ^ 0x51 = 0x01. This means that the last byte of plaintext is p2[15] = 0x51 ^ 0x33 ^ 0x01 = 0x63 ('c') (0x33 is the last byte of the original c1).

Since we now know the last byte of p2, we can set it to 0x02 and continue the attack for the second-to-last byte (padding: 2 byte). If the server accepts our message, we know that p2[14] = 0x02.

We proceed until we have decrypted all bytes of the message. Notice that we cannot decrypt the first 16 byte-block since it is XORed with an initialization vector (iv), which we do not know.

The following python script iterates over all 16-byte blocks in secrect.enc and carries out the attack to decrypt the message:

root@kali:~/Documents/he18/egg22# cat oracle.py
#!/usr/bin/env python

import socket
import pwn

HOST = 'whale.hacking-lab.com'
PORT = 5555

def decrypt(c1, c2):
  c1_orig = c1
  c1_new = []
  for i in reversed(range(16)):
    print("i = " + str(i))
    for j in range(15,i,-1):
      c1 = c1[:j] + chr(c1_new[15-j]^(16-j)^(16-i)) + c1[j+1:]
    for char in range(256):
      c1_tmp = c1[:i] + chr(char) + c1[i+1:]
      s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      s.connect((HOST, PORT))
      s.send(c1_tmp+c2)
      data = s.recv(1024)
      s.close()
      if (data[:2] == "ok"):
        print("--> " + hex(char))
        c1_new.append(char)
        break

  print(c1_new)

  p2 = ""
  for i in range(16):
    p2 += chr(c1_new[15-i]^ord(c1_orig[i])^(16-i))
  print("=> " + p2)


f = open("secret.enc", "rb")
a = f.read()
f.close()

for b in range(0,4):
  print("b = " + str(b))
  c1 = a[0x10*b:0x10*b+0x10]
  c2 = a[0x10*b+0x10:0x10*b+0x20]
  print(pwn.hexdump(c1+c2))
  decrypt(c1,c2)

Luckily the password is not stored in the first 16 bytes 😉 The decrypted message:

assword is: oracl3in3delphi

See you soon!

Jane

The password is oracl3in3delphi.

23 – Rapbid Learning

The provided website (http://whale.hacking-lab.com:2222) offers four functions:

  1. Train
  2. Assignment
  3. Submit
  4. Reward

1. Train
The train-endpoint returns a single rabbit in json-format:

usr@host:~/he18/egg23$ curl 'http://whale.hacking-lab.com:2222/train'
{"sp00n": 10, "n4m3": "Carmen", "t41l": 10, "w31ght": 2, "c0l0r": "red", "ag3": 5, "l3ngth": 53, "g00d": false, "g3nd3r": "female"}

This data should be used to determine which rabbit is good ("good": true) and which is not ("good": false) based on the value of the other attributes.

2. Assignment
The assignment-endpoint returns a list of rabbits, which should be classified in "g00d": true and "good": false (this property is missing):

usr@host:~/he18/egg23$ curl http://whale.hacking-lab.com:2222/gate 2>/dev/null | head -c 400
{"attributes": ["bjRtMw==", "ZzNuZDNy", "NGcz", "YzBsMHI=", "dzMxZ2h0", "bDNuZ3Ro", "c3AwMG4=", "dDQxbA=="], "data": [["Willie", "male", 4, "brown", 2, 40, 12, 10], ["Ruthie", "female", 1, "red", 2, 42, 14, 11], ["Nellie", "female", 0, "brown", 4, 49, 8, 8], ["Randy", "male", 2, "red", 2, 47, 14, 10], ["Marcella", "female", 6, "red", 5, 50, 8, 10], ["Melissa", "female", 2, "white", 5, 45, 7, 10], 

The attributes are base64-encoded but they are the same as within the train-data expect the missing g00d attribute:

root@kali:~/he18/egg23$ echo "bjRtMw==" | base64 -d
n4m3
root@kali:~/he18/egg23$ echo "ZzNuZDNy" | base64 -d
g3nd3r
root@kali:~/he18/egg23$ echo "NGcz" | base64 -d
4g3
...

3. Submit

The submit-endpoint should be used to send the categorization of the rabbits retrieved from the assignment-endpoint to the webserver.

The solution is supposed to be submitted as a simple json integer array (these words are displayed in phonetic spelling).

This means that we have to iterate over the data-array containing all rabbits retrieved from the assignment-endpoint, determine if the rabbit is good or not and append a 1 (good) or 0 (not good) in an array, which we will send to the submit-endpoint.

4. Reward

The reward-endpoint can be used after a successful solution submission to retrieve the egg. The current session is identified by a session-id.

Solution

I started by writing a python-script which retrieves a few training-rabbits and saves them in a local file:

root@kali:~/he18/egg23$ cat train.py
#!/usr/bin/env python
# train.py

import urllib2
import json
import time
import os

def train():

  j = []
  if (os.path.isfile("train.txt")):
    f = open("train.txt", "r")
    tc = f.read()
    f.close()
    if (len(tc) > 0): j = json.loads(tc)

  url = "http://whale.hacking-lab.com:2222/train"
  resp = urllib2.urlopen(url)
  content = resp.read()

  j.append(json.loads(content))

  f = open("train.txt", "w")
  f.write(json.JSONEncoder().encode(j))
  f.close()


for i in range(1000):
  train()
  time.sleep(0.1)

At next I wrote a script which can be used to evaluate the collected training-data:

root@kali:~/Documents/he18/egg23# cat eval.py
#!/usr/bin/env python
# eval.py

import json
from sets import Set

j = None

def printSet(attr):
  global j
  s = Set()
  for obj in j:
    s.add(obj[attr])
  print(attr),
  print(s)

def countGood(attr, val):
  global j
  g = 0
  b = 0
  for obj in j:
    if (str(obj[attr]) == val and obj["g00d"] == True): g +=1
    elif (str(obj[attr]) == val and obj["g00d"] == False): b += 1

  print("g00d: " + str(g) + "\t{:.1%}".format(float(g)/float(g+b)))
  print("b4d : " + str(b) + "\t{:.1%}".format(float(b)/float(g+b)))


f = open("train.txt", "r")
tc = f.read()
f.close()
j = json.loads(tc)

printSet("sp00n")
printSet("t41l")
printSet("w31ght")
printSet("c0l0r")
printSet("ag3")
printSet("l3ngth")
printSet("g3nd3r")


while True:
  a = raw_input("Enter attribute: ")
  v = raw_input("Enter value: ")
  countGood(a,v)

The script prints all possible values found for each attribute …

root@kali:~/Documents/he18/egg23# ./eval.py
sp00n Set([7, 8, 9, 10, 11, 12, 13, 14])
t41l Set([8, 9, 10, 11])
w31ght Set([2, 3, 4, 5])
c0l0r Set([u'blue', u'brown', u'purple', u'grey', u'green', u'black', u'white', u'red'])
ag3 Set([0, 1, 2, 3, 4, 5, 6, 7])
l3ngth Set([40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54])
g3nd3r Set([u'male', u'female'])

… and can be used to print the count of good rabbits depending on a specific value for an attribute:

Enter attribute: t41l
Enter value: 8
g00d: 407       100.0%
b4d : 0 0.0%
Enter attribute: t41l
Enter value: 9
g00d: 390       100.0%
b4d : 0 0.0%
Enter attribute: t41l
Enter value: 10
g00d: 366       33.1%
b4d : 740       66.9%
Enter attribute: t41l
Enter value: 11
g00d: 371       33.8%
b4d : 726       66.2%

Relating the attribute t41l it seems likely that each rabbit with a value of 8 or 9 is always good. But we cannot predict for sure if a rabbit is good or not having a t41l value of 10 or 11.

When evaluating the other attributes I noticed the following:

Enter attribute: w31ght
Enter value: 2
g00d: 0 0.0%
b4d : 1466      100.0%
Enter attribute: w31ght
Enter value: 3
g00d: 508       100.0%
b4d : 0 0.0%
Enter attribute: w31ght
Enter value: 4
g00d: 516       100.0%
b4d : 0 0.0%
Enter attribute: w31ght
Enter value: 5
g00d: 510       100.0%
b4d : 0 0.0%

For the value w31ght with values ranging from 2 to 5 there is always a 100% match! A rabbit with a w31ght value of 2 is never good. If the value is above 2 the rabbit is always good.

Finally I wrote a python-script which uses the assignment-endpoint to retrieve a collection of rabbits to categorize. These rabbits are categorized based on their w31ght value and then send to the submit-endpoint. After this the egg from the reward-endpoint is retrieved:

root@kali:~/Documents/he18/egg23# cat run.py
#!/usr/bin/env python
# run.py

import urllib2
import json
import base64
import subprocess

j = None

def decide(r):
  global j
  w = r[j['attributes'].index('w31ght')]
  if (w>2): return 1
  else: return 0


def run():
  global j
  arr = []
  url = "http://whale.hacking-lab.com:2222/"
  resp = urllib2.urlopen(url+"gate")
  content = resp.read()

  j = json.loads(content)

  for i in range(len(j['attributes'])):
    txt = j['attributes'][i]
    j['attributes'][i] = base64.b64decode(txt)

  for i in range(len(j['data'])):
    rabbit = j['data'][i]
    arr.append(decide(rabbit))

  hdr = str(resp.headers)
  session = hdr[hdr.index("Set-Cookie: ")+12:]
  session = session[:session.index(";")]

  ret = subprocess.check_output(["curl", "-v", url+"predict", "-H", "Content-Type: application/json", "--cookie", session, "--data", str(arr)], stderr=subprocess.STDOUT)
  print(ret)

  ret = subprocess.check_output(["curl", "-v", url+"reward", "--cookie", session])
  print(ret)


run()

Running the script retrieves the egg from the reward-endpoint:

root@kali:~/Documents/he18/egg23# ./run.py
...
* Connection #0 to host whale.hacking-lab.com left intact
SCORE: MTAwLjAl - lolnice! - I'll tell my guys to set up your reward for this shift at /reward, don't forget to bring your cookie!
...
> GET /reward HTTP/1.1
> Host: whale.hacking-lab.com:2222
> User-Agent: curl/7.57.0
> Accept: */*
> Cookie: session_id=2c3cdd994d5998457e3f16c7b326247fbf500e97
...
<h2>Reward</h2>
<hr>
<div>
  <img src="data:image/png;base64,iVBORw0KGgoAAAAN...AAASUVORK5CYII=">
</div>
...

Viewing the base64-encoded images in a browser displays the egg:

24 – ELF

The provided file is a 32-bit ELF binary:

root@kali:~/Documents/he18/egg24# file lock
lock: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=8272392507ce63b716cc2e9c0feb001a5467ae53, not stripped

Running the program requireds a pin to be passed as argument:

root@kali:~/Documents/he18/egg24# ./lock
./lock <pin to unlock>

An obvious invalid pin prints a qrcode …

root@kali:~/Documents/he18/egg24# ./lock 123456
lock state:


  ██████████████      ██  ████    ██  ██████████████
  ██          ██  ██  ██    ████  ██  ██          ██
  ██  ██████  ██          ████  ████  ██  ██████  ██
  ██  ██████  ██  ██████  ████  ████  ██  ██████  ██
  ██  ██████  ██    ██      ████  ██  ██  ██████  ██
  ██          ██  ████  ████  ██      ██          ██
  ██████████████  ██  ██  ██  ██  ██  ██████████████
                    ██████  ████  ██
  ██████████  ████████  ██    ██  ████  ██  ██  ██
  ██    ██  ██        ██  ██  ██    ██    ██  ████
    ████      ██████  ██    ████████  ██████
  ████  ██  ██  ████      ██████    ██████    ██
  ██    ██    ██████████  ██    ██      ████    ██
  ██  ██████    ████                ██
  ██  ████    ██  ██    ████    ██  ████  ██  ████
  ██  ██    ██      ██████      ██  ██████  ████████
  ██      ██  ████  ██████      ████████████  ██████
                  ██  ██  ████    ██      ████████
  ██████████████  ██        ████████  ██  ██████████
  ██          ██    ████  ████  ████      ██  ██  ██
  ██  ██████  ██  ████    ██  ████████████████  ████
  ██  ██████  ██  ████          ████    ████████  ██
  ██  ██████  ██  ██    ████    ██  ██      ████  ██
  ██          ██  ██    ██    ██  ██  ████████
  ██████████████  ████████      ████        ██    ██

… which contains the string ------- locked -------.

When single-stepping through the binary using gdb

root@kali:~/Documents/he18/egg24# gdb lock
...
gdb-peda$ b *main
Breakpoint 1 at 0x650
gdb-peda$ r 1234
Starting program: /root/Documents/he18/egg24/lock 1234
[----------------------------------registers-----------------------------------]
EAX: 0xf7f9add8 --> 0xffffd6d0 --> 0xffffd818 ("LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc"...)
EBX: 0x0
ECX: 0x3af6c63c
EDX: 0xffffd654 --> 0x0
ESI: 0xf7f99000 --> 0x1d4d6c
EDI: 0x0
EBP: 0x0
ESP: 0xffffd62c --> 0xf7ddce81 (<__libc_start_main+241>:        add    esp,0x10)
EIP: 0x56555650 (<main>:        push   ebp)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x5655564c <__x86.get_pc_thunk.dx+3>:        ret
   0x5655564d <__x86.get_pc_thunk.dx+4>:        xchg   ax,ax
   0x5655564f <__x86.get_pc_thunk.dx+6>:        nop
=> 0x56555650 <main>:   push   ebp
   0x56555651 <main+1>: mov    ebp,esp
   0x56555653 <main+3>: cmp    DWORD PTR [ebp+0x8],0x2
   0x56555657 <main+7>: jne    0x56555678 <helpmsg>
   0x56555659 <main+9>: mov    ebx,DWORD PTR [ebp+0xc]
[------------------------------------stack-------------------------------------]
0000| 0xffffd62c --> 0xf7ddce81 (<__libc_start_main+241>:       add    esp,0x10)
0004| 0xffffd630 --> 0x2
0008| 0xffffd634 --> 0xffffd6c4 --> 0xffffd7f3 ("/root/Documents/he18/egg24/lock")
0012| 0xffffd638 --> 0xffffd6d0 --> 0xffffd818 ("LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc"...)
0016| 0xffffd63c --> 0xffffd654 --> 0x0
0020| 0xffffd640 --> 0x2
0024| 0xffffd644 --> 0xffffd6c4 --> 0xffffd7f3 ("/root/Documents/he18/egg24/lock")
0028| 0xffffd648 --> 0xf7f99000 --> 0x1d4d6c
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x56555650 in main ()
gdb-peda$ ni
...

… we can see, that the main function calls another function called mainPrg.

The control flow within this function is quite unusual. The return address of the function at ebp+0x4 (referencing exitProg) is moved to eax, 0xe5 is added and eax is pushed onto the stack. The je at 0x565556a8 is not taken, meaning that the pushed eax value is perceived as the return address for the ret instruction at 0x565556aa:

[----------------------------------registers-----------------------------------]
EAX: 0x56555676 (<main+38>:     jmp    0x56555685 <exitPrg>)
EBX: 0xffffd6c8 --> 0xffffd813 ("1234")
ECX: 0xe
EDX: 0xf7f9a890 --> 0x0
ESI: 0xf7f99000 --> 0x1d4d6c
EDI: 0x0
EBP: 0xffffd618 --> 0xffffd624 --> 0xffffd813 ("1234")
ESP: 0xffffd618 --> 0xffffd624 --> 0xffffd813 ("1234")
EIP: 0x565556a2 (<mainPrg+19>:  add    eax,0xe5)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x56555697 <mainPrg+8>:      call   0xf7e14ab0 <printf>
   0x5655569c <mainPrg+13>:     add    esp,0x4
   0x5655569f <mainPrg+16>:     mov    eax,DWORD PTR [ebp+0x4]
=> 0x565556a2 <mainPrg+19>:     add    eax,0xe5
   0x565556a7 <mainPrg+24>:     push   eax
   0x565556a8 <mainPrg+25>:     je     0x565556ab <doit>
   0x565556aa <mainPrg+27>:     ret
   0x565556ab <doit>:   call   0x565556b4 <checkpin>
[------------------------------------stack-------------------------------------]
0000| 0xffffd618 --> 0xffffd624 --> 0xffffd813 ("1234")
0004| 0xffffd61c ("vVUVgVUV\023\330\377\377")
0008| 0xffffd620 ("gVUV\023\330\377\377")
0012| 0xffffd624 --> 0xffffd813 ("1234")
0016| 0xffffd628 --> 0x0
0020| 0xffffd62c --> 0xf7ddce81 (<__libc_start_main+241>:       add    esp,0x10)
0024| 0xffffd630 --> 0x2
0028| 0xffffd634 --> 0xffffd6c4 --> 0xffffd7f3 ("/root/Documents/he18/egg24/lock")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x565556a2 in mainPrg ()
gdb-peda$

Let’s see where we are going when the ret instruction is executed:

[----------------------------------registers-----------------------------------]
EAX: 0x5655575b (<qr_next_line+39>:     mov    esi,0x5655703f)
EBX: 0xffffd6c8 --> 0xffffd813 ("1234")
ECX: 0xe
EDX: 0xf7f9a890 --> 0x0
ESI: 0xf7f99000 --> 0x1d4d6c
EDI: 0x0
EBP: 0xffffd618 --> 0xffffd624 --> 0xffffd813 ("1234")
ESP: 0xffffd614 ("[WUV$\326\377\377vVUVgVUV\023\330\377\377")
EIP: 0x565556aa (<mainPrg+27>:  ret)
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x565556a2 <mainPrg+19>:     add    eax,0xe5
   0x565556a7 <mainPrg+24>:     push   eax
   0x565556a8 <mainPrg+25>:     je     0x565556ab <doit>
=> 0x565556aa <mainPrg+27>:     ret
   0x565556ab <doit>:   call   0x565556b4 <checkpin>
   0x565556b0 <doit+5>: mov    esp,ebp
   0x565556b2 <doit+7>: pop    ebp
   0x565556b3 <doit+8>: ret
[------------------------------------stack-------------------------------------]
0000| 0xffffd614 ("[WUV$\326\377\377vVUVgVUV\023\330\377\377")
0004| 0xffffd618 --> 0xffffd624 --> 0xffffd813 ("1234")
0008| 0xffffd61c ("vVUVgVUV\023\330\377\377")
0012| 0xffffd620 ("gVUV\023\330\377\377")
0016| 0xffffd624 --> 0xffffd813 ("1234")
0020| 0xffffd628 --> 0x0
0024| 0xffffd62c --> 0xf7ddce81 (<__libc_start_main+241>:       add    esp,0x10)
0028| 0xffffd630 --> 0x2
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x565556aa in mainPrg ()
gdb-peda$ x/xw $esp
0xffffd614:     0x5655575b

The top value on the stack is 0x5655575b, which will be interpreted as the return address. On the second line above displaying the value of eax we can see that this is the address of qr_next_line+39.

Let’s have a look at this function:

gdb-peda$ disassemble qr_next_line
Dump of assembler code for function qr_next_line:
   0x56555734 <+0>:     push   0x56557039
   0x56555739 <+5>:     call   0xf7e14ab0 <printf>
   0x5655573e <+10>:    add    esp,0x4
   0x56555741 <+13>:    inc    DWORD PTR [ebp-0x8]
   0x56555744 <+16>:    cmp    DWORD PTR [ebp-0x8],0x19
   0x56555748 <+20>:    jne    0x565556f2 <qr_for_each_line>
   0x5655574a <+22>:    push   0x56557039
   0x5655574f <+27>:    call   0xf7e14ab0 <printf>
   0x56555754 <+32>:    add    esp,0x4
   0x56555757 <+35>:    mov    esp,ebp
   0x56555759 <+37>:    pop    ebp
   0x5655575a <+38>:    ret
   0x5655575b <+39>:    mov    esi,0x5655703f
   0x56555760 <+44>:    xor    ebx,ebx
   0x56555762 <+46>:    xor    ecx,ecx
   0x56555764 <+48>:    add    ebx,DWORD PTR [esi+ecx*4]
   0x56555767 <+51>:    inc    ecx
   0x56555768 <+52>:    cmp    ecx,0x19
   0x5655576b <+55>:    jne    0x56555764 <qr_next_line+48>
   0x5655576d <+57>:    mov    eax,0x5655703f
   0x56555772 <+62>:    mov    eax,DWORD PTR [eax-0x4]
   0x56555775 <+65>:    cmp    eax,ebx
   0x56555777 <+67>:    jne    0x56555793 <qr_next_line+95>
   0x56555779 <+69>:    mov    esi,0x5655703f
   0x5655577e <+74>:    add    esi,0x64
   0x56555781 <+77>:    xor    ebx,ebx
   0x56555783 <+79>:    xor    ecx,ecx
   0x56555785 <+81>:    mov    ebx,DWORD PTR [esi+ecx*4]
   0x56555788 <+84>:    sub    ebx,eax
   0x5655578a <+86>:    mov    DWORD PTR [esi+ecx*4],ebx
   0x5655578d <+89>:    inc    ecx
   0x5655578e <+90>:    cmp    ecx,0x19
   0x56555791 <+93>:    jne    0x56555785 <qr_next_line+81>
   0x56555793 <+95>:    mov    eax,DWORD PTR [ebp+0x4]
   0x56555796 <+98>:    add    eax,0x3e
   0x56555799 <+101>:   push   eax
   0x5655579a <+102>:   ret
   0x5655579b <+103>:   xchg   ax,ax
   0x5655579d <+105>:   xchg   ax,ax
   0x5655579f <+107>:   nop
End of assembler dump.

The ret instruction takes us right here:

   0x5655575b <+39>:    mov    esi,0x5655703f

esi is set to 0x5655703f, which is the address of a symbol called qr1:

gdb-peda$ x/xw 0x5655703f
0x5655703f <qr1>:       0x03f8b2fe

At next the registers ebx and ecx are set to zero …

   0x56555760 <+44>:    xor    ebx,ebx
   0x56555762 <+46>:    xor    ecx,ecx

… and each subsequent 4 bytes beginning at 0x5655703f are added to ebx (0x19 = 25 times):

   0x56555764 <+48>:    add    ebx,DWORD PTR [esi+ecx*4]
   0x56555767 <+51>:    inc    ecx
   0x56555768 <+52>:    cmp    ecx,0x19
   0x5655576b <+55>:    jne    0x56555764 <qr_next_line+48>

After this the address 0x5655703f-0x4 is loaded to eax:

   0x5655576d <+57>:    mov    eax,0x5655703f
   0x56555772 <+62>:    mov    eax,DWORD PTR [eax-0x4]

What is stored at this address?

gdb-peda$ x/xw 0x5655703f-4
0x5655703b <pin>:       0x000004d2

The pin we entered!

By not directly referencing the address of the pin (0x5655703b), it is more difficult to spot where the pin is used.

On the next line the pin we entered is compared to the value of ebx, which contains the added values of qr1:

   0x56555775 <+65>:    cmp    eax,ebx

If the comparison suceeds, the following jne is not taken …

   0x56555777 <qr_next_line+67>:        jne    0x56555793 <qr_next_line+95>

… and esi is set to 0x5655703f + 0x64

   0x56555779 <qr_next_line+69>:        mov    esi,0x5655703f
   0x5655577e <qr_next_line+74>:        add    esi,0x64

… which is the address of a symbol called qr2:

gdb-peda$ x/xw 0x5655703f+0x64
0x565570a3 <qr2>:       0x4572ebe0

What do we need to do? The eip is still at 0x56555775 :

gdb-peda$ context
[----------------------------------registers-----------------------------------]
EAX: 0x4d2
EBX: 0x4179dce2
ECX: 0x19
EDX: 0xf7f9a890 --> 0x0
ESI: 0x5655703f --> 0x3f8b2fe
EDI: 0x0
EBP: 0xffffd618 --> 0xffffd624 --> 0xffffd813 ("1234")
ESP: 0xffffd618 --> 0xffffd624 --> 0xffffd813 ("1234")
EIP: 0x56555775 (<qr_next_line+65>:     cmp    eax,ebx)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x5655576b <qr_next_line+55>:        jne    0x56555764 <qr_next_line+48>
   0x5655576d <qr_next_line+57>:        mov    eax,0x5655703f
   0x56555772 <qr_next_line+62>:        mov    eax,DWORD PTR [eax-0x4]
=> 0x56555775 <qr_next_line+65>:        cmp    eax,ebx
   0x56555777 <qr_next_line+67>:        jne    0x56555793 <qr_next_line+95>
   0x56555779 <qr_next_line+69>:        mov    esi,0x5655703f
   0x5655577e <qr_next_line+74>:        add    esi,0x64
   0x56555781 <qr_next_line+77>:        xor    ebx,ebx
[------------------------------------stack-------------------------------------]
0000| 0xffffd618 --> 0xffffd624 --> 0xffffd813 ("1234")
0004| 0xffffd61c ("vVUVgVUV\023\330\377\377")
0008| 0xffffd620 ("gVUV\023\330\377\377")
0012| 0xffffd624 --> 0xffffd813 ("1234")
0016| 0xffffd628 --> 0x0
0020| 0xffffd62c --> 0xf7ddce81 (<__libc_start_main+241>:       add    esp,0x10)
0024| 0xffffd630 --> 0x2
0028| 0xffffd634 --> 0xffffd6c4 --> 0xffffd7f3 ("/root/Documents/he18/egg24/lock")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
gdb-peda$

In order to bypass the comparison we just set eax (our pin) to the value of ebx

gdb-peda$ set $eax=$ebx

… and continue the execution:

gdb-peda$ c
Continuing.

  ██████████████    ██        ██████  ██████████████
  ██          ██  ████  ██████████    ██          ██
  ██  ██████  ██    ██████        ██  ██  ██████  ██
  ██  ██████  ██  ████  ██    ██      ██  ██████  ██
  ██  ██████  ██      ██  ██    ████  ██  ██████  ██
  ██          ██  ██  ██    ██████    ██          ██
  ██████████████  ██  ██  ██  ██  ██  ██████████████
                          ██    ██
  ██████████  ██████████  ████    ████  ██  ██  ██
        ████      ████      ██    ██        ██  ██
  ██  ██  ██  ████  ██  ████  ████████  ████      ██
  ██  ████      ██  ██████    ████████████████  ████
    ██████    ██  ████  ██  ████      ██  ██  ██████
  ████  ██████    ██  ██  ██████    ██  ██  ████
  ██  ████  ██████          ██  ████  ██    ██  ████
  ██      ██    ████████  ██    ████████  ████    ██
  ██  ██  ██████  ██  ██  ██  ██████████████████
                  ██████    ████  ██      ██  ██
  ██████████████  ██    ████    ████  ██  ██    ████
  ██          ██      ████        ██      ██      ██
  ██  ██████  ██  ████  ████  ████████████████
  ██  ██████  ██  ██  ██  ████      ████████  ██████
  ██  ██████  ██  ██      ████      ████    ██    ██
  ██          ██  ████    ██  ██████        ██    ██
  ██████████████  ██        ████          ██  ██████

[Inferior 1 (process 15031) exited normally]
Warning: not running or target is remote

Done 🙂 The correct pin is 0x4179dce2 = 1098505442. That’s our egg:

25 – Hidden Egg #1

Within the challenge description the word Head is highlighted. This suggests that we should have a look at the HTTP headers:

usr@host:~$ curl -v 'https://hackyeaster.hacking-lab.com/hackyeaster/challenge.html?id=25'
*   Trying 80.74.140.117...
* TCP_NODELAY set
* Connected to hackyeaster.hacking-lab.com (80.74.140.117) port 443 (#0)
...
> GET /hackyeaster/challenge.html?id=25 HTTP/1.1
> Host: hackyeaster.hacking-lab.com
> User-Agent: curl/7.55.1
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Thu, 03 May 2018 10:02:08 GMT
< Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/5.4.16
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: POST, GET, OPTIONS
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: ACCEPT, ORIGIN, X-REQUESTED-WITH, CONTENT-TYPE, AUTHORIZATION
< Strict-Transport-Security: max-age=31536000
< Expires: Thu, 3 May 2018 13:02:08 CEST
< Cache-Control: max-age=3600
< Pragma: cache
< Content-Eggcoding: aHR0cHM6Ly9oYWNreWVhc3Rlci5oYWNraW5nLWxhYi5jb20vaGFja3llYXN0ZXIvaW1hZ2VzL2VnZ3MvYmEwYzc0ZWQ0MzlhYjQ3OTVmYzM2OTk5ZjU0MmJhNTBiMzI2ZTEwOS5wbmc=
< Accept-Ranges: bytes
< ETag: W/"2287-1517388588000"
< Last-Modified: Wed, 31 Jan 2018 08:49:48 GMT
< Content-Type: text/html; charset=UTF-8
< Content-Length: 2287
< 
<!DOCTYPE HTML>
<html>
<head>
    <title>Hacky Easter 2018</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
...

There is a custom headerfield called Content-Eggcoding which seems to be base64-encoded:

usr@host:~$ echo "aHR0cHM6Ly9oYWNreWVhc3Rlci5oYWNraW5nLWxhYi5jb20vaGFja3llYXN0ZXIvaW1hZ2VzL2VnZ3MvYmEwYzc0ZWQ0MzlhYjQ3OTVmYzM2OTk5ZjU0MmJhNTBiMzI2ZTEwOS5wbmc=" | base64 -d
https://hackyeaster.hacking-lab.com/hackyeaster/images/eggs/ba0c74ed439ab4795fc36999f542ba50b326e109.png

That’s it. The egg can be retrieved at https://hackyeaster.hacking-lab.com/hackyeaster/images/eggs/ba0c74ed439ab4795fc36999f542ba50b326e109.png:

26 – Hidden Egg #2

I think this was the hardest hidden egg, but it was circumscribed very well. Within the challenge description the word tile is highlighted and it is suggested that you perhaps need to browse on the egde.

The word edge is related to the browser Edge and a tile can be found on the windows start menu. We just have to visit the challenge’s website using Edge and pin the page to start:

After this the hacky easter logo is placed on the start menu:

Now we need to change the size to big …

… in order to display the egg:

27 – Hidden Egg #3

Within the challenge description the word app is highlighted. This suggests that the egg is hidden in the app.

I actually stumbled upon the egg when extracting all app-files for the mobile challenges using the APK Editor app. There is a file called res/drawable/jc_launcher.png:

This is the egg: