Another job opportunity at the Police Security Service

Here's a walk-through of the Norwegian Police Security Service's latest job ad riddle. Were you able to crack the code?

Published: Mon, October 14, 2019, 06:25
Category:
Security
Tags:
Behind the news
Challenge

Background ๐Ÿ‘ฎ ๐Ÿ”—

The Norwegian Police Security Service (with the Norwegian abbreviation PST which I'll use for this write-up) is the police security agency of Norway. Once in a while they have job ads with some more or less hidden challenges - almost Capture the Flag style.

Previous shark sighting ๐Ÿฆˆ ๐Ÿ”—

In December 2018 they posted a job listing were they included a riddle that they wanted people to solve. I didn't hear of the job posting until it suddenly in January was on the front page of most Norwegian newspapers. I published my version of the solution just after the application deadline. The solution was also translated to Norwegian and published at kode24.no - a site for Norwegian developers.

PST themselves approved the walk-through:


To my surprise and amusement, screenshots from that kode24.no post was shown for some seconds on a show on the Norwegian TV channel NRK:

Now, I didn't expect to do another one of these walk-throughs this soon, but all of the sudden the interesting Twitter handle twitt3rhai (=Twitter shark) tweeted what might appear as random characters. Twitt3rhai was part of the challenge in December. Of course I had to try to figure what the tweet was all about..

Luckily this turned out to be less of a challenge than the previous one.

The job ad ๐Ÿ”—

It all starts with PST's job ad where they want both digital forensics specialists and a system developer. The text in the job advertisement doesn't hint about any challenges as I could see, but there is an interesting header image:
Screenshot with challenge
It's got all the nerdy details you'd want: A pcap (packet capture) hat, lotsa computer screens, some file dump or something, some code, and more.

Step 1 - understanding the code ๐Ÿ”—

While the image doesn't have the world's highest resolution you can see that the person in the image (possibly nicknamed HackerMan) has got the following Python 3 code file named encrypt\.py open:

#!/usr/bin/python3
from base64 import b64encode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

def get_primes(count):
    primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61]
    return(primes[0:count])

with open('plain.txt', 'rb') as f:
    plaintext = f.read()

iv = ""
for i in get_primes(16):
    iv += chr(plaintext[i + 16])

key = b'\xba\xda\x55 HackerMan \x13\x37'  # <----- DESTROY AFTER USE

cipher = AES.new(key, AES.MODE_CBC, IV=iv)
ciphertext_b64 = b64encode(cipher.encrypt(pad(plaintext, AES.block_size))).decode('utf-8')
print(ciphertext_b64)

#TODO: automate posting of ciphertext to twitter.com/twitt3rhai

Now, this is some interesting code. Let's follow the flow of it:

  1. It reads the contents of the file plain.txt (aka unencrypted information) into a variable plaintext.
  2. It creates a variable iv by using 16 characters from plaintext offset by 16 + a prime number.
  3. It defines a variable key (nerd bonus for using the hex numbers BA, DA, 55, 13, 37) which is indicated that should be deleted later on.
  4. It creates an AES cipher with CBC mode using the key variable as key and the iv variable as initialization vector.
  5. The cipher is then used to encrypt the padded plaintext before the ciphertext is Base64 encoded.
  6. Notingly there is a TODO about posting the ciphertext to twitt3rhai. (And what do you know, the same Twitter handle is also in that header image.)

Step 2 - getting the ciphertext ๐Ÿ”—

Our ultimate goal seems to be to get the plaintext to see what it says. So lets start by heading over to Twitter:

This does indeed seem like it could be some Base64 encoded text and can be assumed to be the ciphertext. Now we've got quite a few pieces of the puzzle.

Step 3 - decrypting the ciphertext ๐Ÿ”—

So, how can we decrypt the ciphertext in the tweet? Let's just tweak the encrypt.py to make our own decrypt.py:

#!/usr/bin/python3
from base64 import b64encode, b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

def get_primes(count):
    primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61]
    return(primes[0:count])

# TODO: automate reading of ciphertext from twitter.com/twitt3rhai ;)
ciphertext_b64 = '/lb0WZDpaIDJVJwy+Q04LCqERqVj7AUItWGREJuXJeWtZN77yP6grehn1gRif31hjTEjLNFyxESweea81/QluWUyhZV9vmabm8NYkkSc6JJWuylGJKQJzA/wC2cM2ScrQQ8gV7GcnVyBCh7eq/N0jUm/L4xrX6IUIDi5CAkVZ9xSS5Tb4o01onOTbGWLd1EZwzZOMlq88wsTPZ6zY7dqj+LKq3Pj6SKlZfaR9eo6PXrRUOARCe9sQVtWVKc5DJfI'
ciphertext = b64decode(ciphertext_b64.encode('utf-8'))

iv = " " * 16  # Must be 16 bytes

key = b'\xba\xda\x55 HackerMan \x13\x37'

cipher = AES.new(key, AES.MODE_CBC, IV=bytes(iv, 'utf-8'))
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)

print(plaintext)  # Output: b'\x15\x01Tx)5,d%,*<&:u%erer! Du klarte det! Beklager, men denne gangen har vi ikke laget flere oppgaver. H\xc3\xa5per du vil s\xc3\xb8ke jobben. Hvis du blir ansatt kan vi love deg mange utfordrende oppgaver.'

Here's what the script does:

  1. Decodes the Base64 encoded cipertext.
  2. Creates just an empty initialization vector as we don't have the plaintext to create it.
  3. Decrypts the ciphertext and unpads the output again.
  4. Prints the plaintext.

The output is the following:

b'\x15\x01Tx)5,d%,*<&:u%erer! Du klarte det! Beklager, men denne gangen har vi ikke laget flere oppgaver. H\xc3\xa5per du vil s\xc3\xb8ke jobben. Hvis du blir ansatt kan vi love deg mange utfordrende oppgaver.'

Success, right? Well, almost!

Creating the initialization vector ๐Ÿ”—

When I did the challenge I was at first happy with getting most of the plaintext. And I was thinking that the first part missing probably was the start of the word "Gratulerer" (=congratulations).

But what is an "initialization vector" and why is it used here? AES (Advanced Encryption Standard) is a block cipher, meaning that the algorithm is operating on a fixed-length groups of bits (blocks). To avoid equal plaintext blocks to become equal ciphertext blocks many of the "modes of operation" of the encryption algrithms use some part of the previous block part of the input to the following one. The mode used here is Cipher Block Chaining (CBC). In this mode each block of plaintext is XORed with the previous ciphertext block before being encrypted. To produce distinct ciphertexts even if the same plaintext is encrypted multiple times - and to protect the first block - there must be some be some unique input to the first block; an initialization vector.

Looking at the output there are actually 16 bytes that are "garbage" (the AES' block size). That means that the first word can't just be "Gratulerer".

But how can we get hold of that initialization vector? The answer lies in the code. It is generated from the plaintext - and luckily for us it only uses plaintext above character number 16, meaning that we have everyhting we need. So let's tweak the script:

#!/usr/bin/python3
from base64 import b64encode, b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

def get_primes(count):
    primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61]
    return(primes[0:count])

# TODO: automate reading of ciphertext from twitter.com/twitt3rhai ;)
ciphertext_b64 = '/lb0WZDpaIDJVJwy+Q04LCqERqVj7AUItWGREJuXJeWtZN77yP6grehn1gRif31hjTEjLNFyxESweea81/QluWUyhZV9vmabm8NYkkSc6JJWuylGJKQJzA/wC2cM2ScrQQ8gV7GcnVyBCh7eq/N0jUm/L4xrX6IUIDi5CAkVZ9xSS5Tb4o01onOTbGWLd1EZwzZOMlq88wsTPZ6zY7dqj+LKq3Pj6SKlZfaR9eo6PXrRUOARCe9sQVtWVKc5DJfI'
ciphertext = b64decode(ciphertext_b64.encode('utf-8'))

iv = " " * 16  # Must be 16 bytes

key = b'\xba\xda\x55 HackerMan \x13\x37'

cipher = AES.new(key, AES.MODE_CBC, IV=bytes(iv, 'utf-8'))
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)

iv = ""
for i in get_primes(16):
    iv += chr(plaintext[i + 16])

cipher = AES.new(key, AES.MODE_CBC, IV=bytes(iv, 'utf-8'))
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size).decode('utf-8')

print(iv)  # Output: er uate!k,mngn i
print(plaintext)  # Output: PST-haien gratulerer! Du klarte det! Beklager, men denne gangen har vi ikke laget flere oppgaver. Hรฅper du vil sรธke jobben. Hvis du blir ansatt kan vi love deg mange utfordrende oppgaver.

The script now does the decryption in two rounds; first without knowing the init vector, and then again with it correctly initialized.

Success!

The solution ๐Ÿ”—

The solution to the challenge is this:

PST-haien gratulerer! Du klarte det! Beklager, men denne gangen har vi ikke laget flere oppgaver. Hรฅper du vil sรธke jobben. Hvis du blir ansatt kan vi love deg mange utfordrende oppgaver.

English:
The PST shark congratulates you! You made it! Sorry, but this time we haven't created any more challenges. Hope you will apply for the job. If you get hired we can promise you many challenging assignments.

So that's it. Not as much work to do as the previous challenge, but still a fun challenge I personally enjoyed a lot. ๐Ÿ™‚ I hope PST got the candidates they need. ๐Ÿ•ต๏ธโ€

Get notified when there are new posts! :-)