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. |
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:
- calculate the radius and degree of a coordinate
- determine on which of the 13 circles/squares the coordinate is located based on the radius
- 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:
- the sausages (Wow)
- a text layer: That‘s the flag 🙂 For real! Wasn‘t that simple?
- 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:
- Train
- Assignment
- Submit
- 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: