Here's a write-up of the challenges of the Norwegian Police Security Service's capture the flag advent calendar for 2020.
Published: | Thu, January 7, 2021, 19:10 |
Updated: | Fri, January 8, 2021, 22:50 |
Category: |
Security
|
Tags: |
Behind the news
Challenge
|
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. They have by now made it a tradition to have Capture the Flag (CTF) style job ads and competitions. This is the fifth time I'm covering them here: 1, 2, 3, 4.
PST added an N in front of their name and created the imaginary Northern Polar Security Service (Nordpolar sikkerhetstjeneste = NPST). NPST's role is supposedly to protect Santa Claus, his elfs and Christmas itself. Like last year, PST posted a fake job ad where they said to be looking for elf officers (="alvebetjent") for a temporary position to help out NPST. A few news outlets wrote about it as well (TV 2, Avisa Oslo). Everything went down on npst.no from December 1st to December 24th.
This time PST reused an acronym from a internship job ad challenge they had earlier this year: DASS - Digitale Arkiv- og SaksbehandlingsSystem. Dass "happens" to be a slang word for toilet. ๐ฝ
New this year were Easter eggs(!) that worked as extra hidden flags that had to be found to get full score.
Let's jump straight to it. Click on a challenge to expand it.
Expand/collapse all
The first task we are handed is to reply with a verification code to confirm that we have access to the system. There's just one problem. Our manager has messed up the code: RUV{JgkJqPรฅGtFgvLwnKilgp}
We have been told the flags should be in the format PST{.*}
. The challenge mentions salad. Caesar salad? We are of course looking at a Caesar cipher. It can be solved in seconds using an online tool like Cyberchef. The solution is PST{HeiHoNรฅErDetJulIgjen}
.
DASS had a humans.txt with a twist. The file https://dass.npst.no/humans.txt was filled with close to a thousand newlines. At the bottom the flag EGG{sh4rks_d0t_txt}
was hiding. It can be a good tip to do a CTRL + A
/ CMD + A
when looking at source codes to reveal anything that is hidden behind scrollbars.
An intelligence officer from the Southern Polar Security Service (Sydpolar sikkerhetstjeneste = SPST) named Pen Gwyn was stopped in customs on departure from the North Pole. A storage medium has been secured and we are asked to look at a .mid
file inside a ZIP file.
- file
- GarageBand
- Python
The file command confirmed that the .mid
file was a MIDI file:
$ file pen_gwyn_greatest_hits.mid: Standard MIDI data (format 1) using 1 track at 1/480
A quick look in the Mac OS pre-installed music creation studio GarageBand revealed that some of the notes went very high. I guessed that we could be looking at notes describing characters and hacked together a quick Python script to print the notes and convert them into ASCII:
# pip3 install mido # pip3 install python-rtmidi import mido from mido import MidiFile output = mido.open_output(name='mido', virtual=True) file = MidiFile('pen_gwyn_greatest_hits.mid') clear_screen = '\033[H\033[J' complete_message = '' for message in file.play(): if not message.is_meta: if message.type == 'note_on': complete_message += chr(message.note) print('%sMessage: %s' % (clear_screen, complete_message))
Running the program gives this output:
The solution is PST{BabyPenGwynDuhDuhDuhDuhDuhDuh}
.
The ZIP file we got also contained a password protected 7z file which we'll get back to.
cupcake.png
๐งRef. the seizure mentioned on the 2nd. We are given the password for a 7z archive file that was inside the ZIP file we got. The password is til zip-fila,
. One of the files inside is called cupcake.png
and we are told that there probably is information that can't be seen by the naked eye.
- Steganography Online
- NPST'S enhancement tool
Using the steganography tool like Steganography Online you get the hidden text youtu.be/I_8ZH1Ggjk0
which is a YouTube clip called CSI Zoom Enhance. Of course NPST is just as good as CSI and has their own enhancement tool which you need to use to solve this task:
Enhancing the image you can see that the yellow post-it note contains the flag PST{HuskMeteren}
.
cupcake.png
๐งUsing NPST's enhancement tool with cupcake.png
, the last image returned from the server hid a message using steganography - which also could be found by Steganography Online: EGG{MeasureOnceCutTwice}
.
We are given a bunch of MS SQL files that are used to calculate the need for staffing for Easter in the coming years. We are told that the system for calculating when Easter is behaving oddly and that the number that is output is incorrect.
- Python
There were many ways to solve this challenge. The scripts that we are given looked pretty crazy for me at first look, but I was able to solve it as soon as I understand that all it really did was to sum up the days between January 1st 1900 and Easter Eve for the years 2020 to 2040. The problem with the existing scripts seemed to be that it used the "ISO year" of January 1st instead of the actual year.
I originally hardcoded the dates to solve it as quick as I could, but there is of course a library for getting the Easter Eve date.
import datetime from dateutil.easter import * print('PST{%s}' % sum([(easter(x)-datetime.date(1900, 1, 1)).days-1 for x in range(2020, 2041)])) # Prints PST{999159}
The flag was PST{999159}
.
log.csv
๐ฉโ๐ปThere are reports about problems accessing the documentation vault. The challenge is to find something suspicious in the given log file.
- log.csv
- regex101.com
- Python
We are given over 5,000 log lines in the CSV file. Probably to add some spice it was UTF-16 encoded and parts of the text was URL encoded. The lines have many similarities. I pasted a few lines into regex101.com and built a regular expression that fit those. I then created a Python script that ran through the log file and printed only a few interesting log lines. I then added in auto submitting of those flags (all log lines had what looked like a flag).
import urllib.parse import re import json import requests import time jwt = 'eyJ...' # Removed JSON Web Token expected_pattern = re.compile(r"^2020-1[0-1]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9];[a-zรฆรธรฅA-Zรรร ]+\s<[a-zรฆรธรฅA-Zรรร ]+>;SPF;.*(PST{.*}).*$") flag_pattern = re.compile(r"PST{.*}") f = open('log.csv', 'r', encoding='utf-16') for i, line in enumerate(f.readlines()): clean = urllib.parse.unquote_plus(line) match = expected_pattern.match(clean) if match is None: print('Found interesting log line:') print(i, clean) flag = flag_pattern.search(clean).group() data = {"to": "Mellomleder", "subject": "SV: Luke 5", "content": flag} headers = {'authorization': 'Bearer ' + jwt, 'content-type': 'application/json'} response = requests.post('https://dass.npst.no/.netlify/functions/submit', data=json.dumps(data), headers=headers) print(response.status_code, response.text) if response.status_code != 400: exit(0) time.sleep(1) """ 1252 2020-10-15 08:35:03;Niโssen <Jule Nissen>;SPF <Seksjon for Passord og Forebygging>;I dag har jeg lyst til at PST{879502f267ce7b9913c1d1cf0acaf045} skal vรฆre passordet mitt 1252 {"to": "Mellomleder", "subject": "SV: Luke 5", "content": "PST{879502f267ce7b9913c1d1cf0acaf045}"} 200 {"message":"Meldingen ble sendt."} """
The flag was PST{879502f267ce7b9913c1d1cf0acaf045}
.
December 5th we got an email telling we could email HR with the flag EGG{w0rlds_b3st_b0ss}
. I suppose this egg was a way of communicating that there were several eggs available if one hadn't discovered anyone yet.
We are introduced to a new assembly language called SLEDE8. We have to do the e-learning module 4032996b1bbb67f6
at slede8.npst.no:
; Fรธrste byte med fรธde er et tall N som representerer ; antallet pรฅfรธlgende bytes med fรธde. ; Beregn summen av de N pรฅfรธlgende tallene, ; og gi resultatet som oppgulp. ; Lykke til!
SLEDE8 is a neat assembly language with a very nice online IDE. Luckily I had a short introduction to assembly 20 years ago at Agder University College (now University of Agder) in Grimstad. I have never really used it since, but reading a few lines once every few years meant that it felt familiar from the start.
The challenge could be solved like this:
LES r0 ; Iterasjoner totalt SETT r1, 0 ; Iterasjoner utfรธrt SETT r2, 0 ; Sum SETT r3, 1 ; Iterasjonsรธkning pluss: MEL r0, r1 ; Iterasjoner utfรธrt <= iterasjoner totalt BHOPP slutt ; Ferdig LES r4 ; Les inn fรธde PLUSS r2, r4 ; Pluss pรฅ totalen PLUSS r1, r3 ; Pluss pรฅ iterasjon HOPP pluss slutt: SKRIV r2 STOPP
Submitting a working program gave back the following message with flag: Godkjent! Din verifikasjonskode er: PST{ATastyByteOfSled}
.
Ref. the 7z file on December 3rd. The second file in the archive was called kladd.txt
and contained 8 x ๐ท emojis followed by a Base64 encoded string:
๐ท๐ท๐ท๐ท๐ท๐ท๐ท๐ท N4Igzg9grgTgxgUwMIQCYJALhAZQKIAqBABDAIwA0xADAB5kA6AdgGo4DSAkgGInlXlm+IqWpUAbAE4h7AEqcWo5gAkA8gAV1NWgCYEzZgAsAtsczMAdABEAggRvaALI4p1xAVle0ECLwA4Adi8AI3F-MTpUSjpHXzoAQy9HRMiAMy9JVMtbe214iNp3VC8AnS94sro-AGYMuNpJTzoEJtpUAtQwhJSGnWy7BzoA+tS4ENq6auCQ9Lp3aO9phPqEYqrK+jG6HUcDJnUAGQBVHBxRAUYmDnlFGGoZG6Urwj4xbQDgh4UngiPZYjAAE9UAAHCAAGwQTGYhxOZzuFy+t3u0KuBA06j2QNBEKh5meInILiewj4ZE8pEuzG4nAAcrTiCZjMwABp4A4HPD-GBlUh9Al8Cl0KlMCDGADWADdVvjOfDqswDjYcHxdkw2RyuaRaqQ1YrOOw+QIFUwAEJqTTEMFgAAu4KgqAAllBmVc5N8YCbYadzqR3CoMcQxVLVntrXaHc6zEIXr6iTHCZRKf6mLJCH89swQBQQI6mCCoDasCAyKVJPFgnBqqkpn5JI5quJxMEdAF4qka3B5sEyKh2u1EHBm2RJJJgn50KkAtQ4JIdKgdO5hhUdH4fGQaj4PDsQABfIA
On December 6th is was clear that this was SLEDE8 code. The 8 sledges + a base64 encoded string that I recognized from the SLEDE8 editor gave it away. Appending a hash sign + the base64 encoded string to the online IDE gave a program that could be run and that printed the flag EGG{SLEDE8ExampleForSPSTInternalUseOnly}
.
There was a e-learning module called "Hei, verden!" with the following instructions:
; Skriv ut strengen "Hello, World!\n" ; Tips: ; - Du kan velge om oppgulp skal vise ASCII- eller hex-verdier ; - Det enkle er ofte det beste ; Lykke til!
It could be solved by something like this:
SETT r5, 0x01 ; Teller-รธkning FINN hilsen ; Hent adressen til hilsen skriv: LAST r3 ; Last neste tegn LIK r3, r4 ; Tegn == 0 BHOPP stopp ; Stopp SKRIV r3 ; Skriv PLUSS r0, r5 ; Sett til neste tegn HOPP skriv hilsen: .DATA 0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x57,0x6f,0x72,0x6c,0x64, 0x21, 0x0a stopp: STOPP
Submitting a working program gave back the following message with flag: Godkjent!\nEGG{Hello, SLEDE8!}
.
A strange signal has been intercepted. What could it be?
- file
- Universal Radio Hacker
The file
command didn't really help out a lot this time:
$ file data.complex16u: International EBCDIC text, with very long lines, with no line terminators
EBCDIC is an eight-bit character encoding. But the contents can be anything. Googling the filename hinted that Universal Radio Hacker (URH) might be what was needed to decode the file. URH is a tool for analyzing unknown wireless protocols.
Just opening the file in URH did the job. It found all parameters correctly and showing the signal as ASCII displayed the flag:
The flag was PST{0n_0ff_k3y1ng_1s_34sy!}
.
It's time for professional development. The topic is ASN.1
. We're given two "strings":
MIIBOTCCATAwggEnMIIBHjCCARUwggEMMIIBAzCB+zCB8zCB6zCB4zCB2zCB0zCByzCBwzCBuzCBszCBqzCBozCBnDCBlDCBjDCBhDB9MHYwbzBoMGEwWjBTMEwwRTA+MDcwMTAqMCMwHDAVMA4wBwUAoQMCAROgAwIBA6EDAgEMogMCAQChAwIBE6ADAgEBoQMCARKkAgUAoQMCARShAwIBDqIDAgEYoQMCAQShAwIBEqEDAgEOoQMCAQ6hAwIBB6IDAgECogMCAQigAwIBAaIDAgENogMCARKiAwIBAKMCBQCiAwIBE6IDAgESogMCAQ+hAwIBEaEDAgEOoQMCAQugAwIBAKIDAgEDoQMCAQyhAwIBFKEDAgESoQMCAQ+gAwIBAaEDAgEMoAMCAQOhAwIBEaEDAgEOogMCAQs=
Spec DEFINITIONS ::= BEGIN LinkedList ::= Node Node ::= SEQUENCE { child CHOICE { node Node, end NULL }, value CHOICE { digit [0] INTEGER(0..9), lowercase [1] INTEGER(0..25), uppercase [2] INTEGER(0..25), leftCurlyBracket [3] NULL, rightCurlyBracket [4] NULL } } END
ASN.1 is short for Abstract Syntax Notation One. It "is a standard interface description language for defining data structures that can be serialized and deserialized in a cross-platform way. It is broadly used in telecommunications and computer networking, and especially in cryptography."
I first used the tools at asn1.io and lapo.it to get a feel for what I was looking at and supposed to do. Unfortunately the challenge had a one-off typo in the specification when first published. That actually threw me off and made me not see the actual solution in the start (even though I feel I should have caught it).
I then found a Python library for decoding ASN.1. I assumed that the integer values 0-25 had to be letters in the English alphabet. Step by step I made the script decode the data:
import re import asn1tools # pip3 install asn1tools import base64 import string SPECIFICATION = ''' Spec DEFINITIONS ::= BEGIN LinkedList ::= Node Node ::= SEQUENCE { child CHOICE { node Node, end NULL }, value CHOICE { digit [0] INTEGER(0..9), lowercase [1] INTEGER(0..25), uppercase [2] INTEGER(0..25), leftCurlyBracket [3] NULL, rightCurlyBracket [4] NULL } } END ''' DATA = 'MIIBOTCCATAwggEnMIIBHjCCARUwggEMMIIBAzCB+zCB8zCB6zCB4zCB2zCB0zCByzCBwzCBuzCBszCBqzCBozCBnDCBlDCBjDCBhDB9MHYwbzBoMGEwWjBTMEwwRTA+MDcwMTAqMCMwHDAVMA4wBwUAoQMCAROgAwIBA6EDAgEMogMCAQChAwIBE6ADAgEBoQMCARKkAgUAoQMCARShAwIBDqIDAgEYoQMCAQShAwIBEqEDAgEOoQMCAQ6hAwIBB6IDAgECogMCAQigAwIBAaIDAgENogMCARKiAwIBAKMCBQCiAwIBE6IDAgESogMCAQ+hAwIBEaEDAgEOoQMCAQugAwIBAKIDAgEDoQMCAQyhAwIBFKEDAgESoQMCAQ+gAwIBAaEDAgEMoAMCAQOhAwIBEaEDAgEOogMCAQs=' compiled_specification = asn1tools.compile_string(SPECIFICATION, codec='ber') decoded = compiled_specification.decode(next(iter(compiled_specification.types.keys())), base64.b64decode(DATA)) # Decodes the 'LinkedList' def traverse_data(data, output): if type(data) is dict: if 'value' in data: traverse_data(data['value'], output) if 'child' in data: traverse_data(data['child'], output) elif type(data) is tuple: if data[0] == 'uppercase': output.append(string.ascii_uppercase[data[1]]) elif data[0] == 'lowercase': output.append(string.ascii_lowercase[data[1]]) elif data[0] == 'digit': output.append(str(data[1])) elif data[0] == 'leftCurlyBracket': output.append('{') elif data[0] == 'rightCurlyBracket': output.append('}') elif data[0] == 'end': return elif data[0] == 'node': traverse_data(data[1], output) else: raise Exception('Unsupported char type', data[0], '(value:', data[1], ')') else: raise Exception('Unsupported type', type(data)) flag_regex = r"(PST{.*})" color_blue = '\033[94m' color_reset = '\033[0m' output = [] traverse_data(decoded, output) output = ''.join(output) output = re.sub(flag_regex, color_blue + r'\1' + color_reset, output) print('Text:', output) # Text: Lor3m1psumD0lorPST{ASN1IChooseYou}s1tAm3t output = re.search(flag_regex, output).group(0) print('Flag:', output) # Flag: PST{ASN1IChooseYou}
The flag was PST{ASN1IChooseYou}
.
A chat log from a suspected SPST agent with "excessive use of emojis" has been intercepted. Maybe the "HEXMAS encoding" could have been used.
๐ ๐คถโโ๐๐๐ฏ๐โจ๐ฅ๐ฅฃ๐ถ๐๐ผ๐ฆ๐ท
๐คถ๐ทโจ๐ถ๐ โจ๐ ๐ ๐ท๐คถ๐๐ฅ๐๐ฆ๐๐ท๐ โ๐ท๐ท๐ ๐ถ๐ โจ๐ ๐ฆ๐ฅฃ๐ฅ๐ท๐ฆโ๐ ๐๐ท๐ท๐ฅ๐๐ฆ๐ โจ๐ฆ๐ฆ๐ฏ๐ถ๐ ๐คถ๐ฆโ๐๐ฏ๐ โจ๐ถ๐ผ๐๐๐ฏ๐โ๐ผ๐ ๐ ๐คถโ๐๐ผ๐๐ฅ๐๐ท๐คถ๐ผ๐ ๐ ๐ ๐ ๐ ๐
- Python
- CyberChef
There's a hint for hexadecimal numbers. The first line has 16 unique "characters". The second line is more like a text and consists only of these 16 different characters. If one lets the first line represent 0-F
one can then replace the characters in the second line with all 0-F
.
The output of that doesn't make much sense directly translated to text, but it starts with the hex 1f8b
which is the magic number for Gzip. Using CyberChef's "magic" operation is perfect for figuring out what kind of data one is looking at. In this case it gave the flag straight away.
Hacking it all together in Python it can look something like this:
import string import zlib emoji_charset = '๐ ๐คถโโ๐๐๐ฏ๐โจ๐ฅ๐ฅฃ๐ถ๐๐ผ๐ฆ๐ท' input = '๐คถ๐ทโจ๐ถ๐ โจ๐ ๐ ๐ท๐คถ๐๐ฅ๐๐ฆ๐๐ท๐ โ๐ท๐ท๐ ๐ถ๐ โจ๐ ๐ฆ๐ฅฃ๐ฅ๐ท๐ฆโ๐ ๐๐ท๐ท๐ฅ๐๐ฆ๐ โจ๐ฆ๐ฆ๐ฏ๐ถ๐ ๐คถ๐ฆโ๐๐ฏ๐ โจ๐ถ๐ผ๐๐๐ฏ๐โ๐ผ๐ ๐ ๐คถโ๐๐ผ๐๐ฅ๐๐ท๐คถ๐ผ๐ ๐ ๐ ๐ ๐ ๐ ' output = '' for char in input: output += string.hexdigits[emoji_charset.find(char)] print(output) # Prints 1f8b0800f149ce5f02ff0b080ea9fe307ff94e08ee6b01e25608bd7c672d00124dc95f1d000000 output = zlib.decompress(bytes.fromhex(output), 16 + zlib.MAX_WBITS).decode('utf-8') print(output) # Prints PST{๐งน๐งน๐๐ ๐๐งน}
The flag was PST{๐งน๐งน๐๐
๐๐งน}
.
There's a service pack for DASS which includes a mspaint variant. Going to one of the menu options makes it crash with a blue screen that contains some assembly. (If you scan the QR code you can listen to some great music while solving the egg.)
This assembly is a bit out of my league, but using the tool at https://defuse.ca/online-x86-assembler.htm#disassembly you can get the raw machine code: 4547477B7838365F6D616368696E455F636F6445727D
. Using CyberChef to convert the hex you get the text and flag EGG{x86_machinE_codEr}
.
It's another SLEDE8 e-learning module: 82ec70284b51eb12
; Fรธde bestรฅr av to tall, A og B ; Skriv ut resultatet av (A + B) mod 256 som en ASCII-streng ; Eksempel: A=0xA0 og B=0x08 => '168' ; Eksempel: A=0xFF og B=0xFF => '254'
- https://bisqwit.iki.fi/story/howto/bitmath/
The task at hand looks easy. Just add two numbers together. However, since the output has to be in ASCII one has to separate the ones, tens and hundreds. I started out implementing the modulo operation as described on https://bisqwit.iki.fi/story/howto/bitmath/ , but I got problems with overflow and couldn't easily wrap my head around how to solve it.
I then changed strategy to just hack together some code that gave the right answer. It's the wrong way to go about it, but I was fed up and just wanted the flag:
LES r0 LES r1 SETT r2, 0x00 PLUSS r2, r0 PLUSS r2, r1 SETT r3, 0x0a SETT r5, r2 TUR minus SETT r15, r5 SETT r3, 0x64 MINUS r2, r5 ; minus enere SETT r5, r2 TUR minus ; minus hundrere MINUS r2, r5 ; minus tiere TUR hack SETT r14, r5 SETT r5, r2 TUR hack2 SETT r13, r5 SETT r6, 0x30 ; ascii-offset SETT r7, 0x00 ; 0-sjekk LIK r13, r7 ;hundrere = 0 BHOPP tiersjekk ; hopp til PLUSS r13, r6 SKRIV r13 HOPP tiere tiersjekk: LIK r14, r7 BHOPP eneresjekk tiere: PLUSS r14, r6 SKRIV r14 HOPP enere eneresjekk: ;LIK r15, r7 ;BHOPP slutt enere: PLUSS r15, r6 SKRIV r15 slutt: STOPP minus: ME r5, r3 BHOPP returminus MINUS r5, r3 HOPP minus returminus: RETUR hack: SETT r6, 0x0a ULIK r6, r5 BHOPP tjue SETT r5, 0x01 RETUR tjue: SETT r6, 0x14 ULIK r6, r5 BHOPP tretti SETT r5, 0x02 RETUR tretti: SETT r6, 0x1e ULIK r6, r5 BHOPP forti SETT r5, 0x03 RETUR forti: SETT r6, 0x28 ULIK r6, r5 BHOPP femti SETT r5, 0x04 RETUR femti: SETT r6, 0x32 ULIK r6, r5 BHOPP seksti SETT r5, 0x05 RETUR seksti: SETT r6, 0x3c ULIK r6, r5 BHOPP sytti SETT r5, 0x06 RETUR sytti: SETT r6, 0x46 ULIK r6, r5 BHOPP atti SETT r5, 0x07 RETUR atti: SETT r6, 0x50 ULIK r6, r5 BHOPP nitti SETT r5, 0x08 RETUR nitti: SETT r6, 0x5A ULIK r6, r5 BHOPP null SETT r5, 0x09 RETUR null: SETT r5,0x00 RETUR hack2: SETT r6, 0x64 ULIK r6, r5 BHOPP tohundre SETT r5, 0x01 RETUR tohundre: SETT r6, 0xc8 ULIK r6, r5 BHOPP nullto SETT r5, 0x02 RETUR nullto: SETT r5,0x00 RETUR
Dirty or not, the unit test on the server side completed successfully and gave back the message Godkjent!\nDin verifikasjonskode er: PST{++AndKissesWillBeAwardedToYou}
.
When handing in the December 10 challenge there's another e-learning module code returned: 8e7c9876c85e5471
The task is to write this algorithm:
; Fรธde bestรฅr av to tall, A og B ; Skriv ut resultatet av A + B som en ASCII-streng ; Eksempel: A=0xA0 og B=0x08 => '168' ; Eksempel: A=0xFF og B=0xFF => '510'
It's pretty similar to the first one, with one important change. There's no modulo 256 of the added numbers (which is the default when adding numbers in SLEDE8). So I changed to first split each input number into ones, tens and hundreds before adding the numbers. But since I had a hack of a code from the beginning, this code is even worse. Don't try this at home.
LES r0 LES r1 ;ener SETT r3, r0 SETT r5, 0x0a ; skal treffe fra alle 10ere TUR minus SETT r4, r3 ; kopi vi kan endre SETT r7, r3 ; kopi av enere SETT r15, r3 ;tier SETT r3, r0 MINUS r3, r4 ; fjernet enere SETT r5, 0x64 ; skal treffe fra alle 100ere TUR minus SETT r4, r3 ; kopi vi kan endre TUR hack SETT r14, r3 ;hundrer SETT r3, r0 MINUS r3, r7 ; fjernet enere MINUS r3, r4 ; fjernet tiere SETT r5, r3 ; hack2 opererer pรฅ r5 TUR hack2 SETT r13, r5 tall2: ;ener SETT r3, r1 SETT r5, 0x0a ; skal treffe fra alle 10ere TUR minus SETT r4, r3 ; kopi vi kan endre SETT r7, r3 ; kopi av enere PLUSS r15, r3f ; tieroverskudd pรฅ enere SETT r5, 0x0a SETT r12, 0x01 ME r15, r5 BHOPP tall2_tier MINUS r15, r5 PLUSS r14, r12 tall2_tier: SETT r3, r1 MINUS r3, r4 ; fjernet enere SETT r5, 0x64 ; skal treffe fra alle 100ere TUR minus SETT r4, r3 ; kopi vi kan endre TUR hack PLUSS r14, r3 ; tieroverskudd pรฅ tiere tieroverkusdd: SETT r5, 0x0a SETT r12, 0x01 ME r14, r5 BHOPP tall2_hundrer MINUS r14, r5 PLUSS r13, r12 HOPP tieroverkusdd tall2_hundrer: ;hundrer SETT r3, r1 MINUS r3, r7 ; fjernet enere MINUS r3, r4 ; fjernet tiere SETT r5, r3 ; hack2 opererer pรฅ r5 TUR hack2 PLUSS r13, r5 SETT r6, 0x30 ; ascii-offset SETT r7, 0x00 ; 0-sjekk LIK r13, r7 ;hundrere = 0 BHOPP tiersjekk ; hopp til PLUSS r13, r6 SKRIV r13 HOPP tiere tiersjekk: LIK r14, r7 BHOPP eneresjekk tiere: PLUSS r14, r6 SKRIV r14 HOPP enere eneresjekk: ;LIK r15, r7 ;BHOPP slutt enere: PLUSS r15, r6 SKRIV r15 slutt: STOPP hack: SETT r6, 0x0a ULIK r6, r3 BHOPP tjue SETT r3, 0x01 RETUR tjue: SETT r6, 0x14 ULIK r6, r3 BHOPP tretti SETT r3, 0x02 RETUR tretti: SETT r6, 0x1e ULIK r6, r3 BHOPP forti SETT r3, 0x03 RETUR forti: SETT r6, 0x28 ULIK r6, r3 BHOPP femti SETT r3, 0x04 RETUR femti: SETT r6, 0x32 ULIK r6, r3 BHOPP seksti SETT r3, 0x05 RETUR seksti: SETT r6, 0x3c ULIK r6, r3 BHOPP sytti SETT r3, 0x06 RETUR sytti: SETT r6, 0x46 ULIK r6, r3 BHOPP atti SETT r3, 0x07 RETUR atti: SETT r6, 0x50 ULIK r6, r3 BHOPP nitti SETT r3, 0x08 RETUR nitti: SETT r6, 0x5A ULIK r6, r3 BHOPP null SETT r3, 0x09 RETUR null: SETT r3,0x00 RETUR hack2: SETT r6, 0x64 ULIK r6, r5 BHOPP tohundre SETT r5, 0x01 RETUR tohundre: SETT r6, 0xc8 ULIK r6, r5 BHOPP nullto SETT r5, 0x02 RETUR nullto: SETT r5,0x00 RETUR minus: ME r3, r5 BHOPP returminus MINUS r3, r5 HOPP minus returminus: RETUR
After passing the unit tests on the server side ones get back the following message Godkjent!\nEGG{ba92ae3a9af1a157703ca83d9a9fb11d}
The internal security team at NPST has discovered that there's been made unauthorized modifications to Santa's naughty or nice list. They are saying that a MD5 sum has changed, but need help to see what has changed.
- Base
- Python
The ZIP file contained a SQLite database file. There was one database table for nice children, and one for naughty children. There were 10,000 nice ones, and 1,000 naughty ones. The columns in the tables were fornavn
, etternavn
and md5
which was the MD5 sum of the first + last names.
There were also two temporary files used by SQLite - .db-shm
and .db-wal
.
The WAL file is a Write-Ahead Log that is used to implement atomic commit and rollback. "The WAL file is created when the first connection to the database is opened and is normally removed when the last connection to the database closes. However, if the last connection does not shutdown cleanly, the WAL file will remain in the filesystem and will be automatically cleaned up the next time the database is opened."
The SHM file is a Shared Memory that is used as an index for the WAL file. "The shared-memory file has the same lifetime as its associated WAL file. The shared-memory file is created when the WAL file is created and is deleted when the WAL file is deleted. During WAL file recovery, the shared memory file is recreated from scratch based on the contents of the WAL file being recovered."
So the clue is that we have a database that has some changes that hasn't been written yet. One has to figure out the difference.
I noticed that when I opened the database in the SQLite tool Base the temporary files were handled and deleted. I then unzipped the database once more, deleted the two temporary files before opening the database, and wrote a Python script that looked for differences between the two databases:
import sqlite3 connection1 = sqlite3.connect("altered.db") cursor1 = connection1.cursor() connection2 = sqlite3.connect("original.db") cursor2 = connection2.cursor() tables = set() cursor1.execute("SELECT name FROM sqlite_master WHERE type='table';") for row1 in cursor1: tables.add(row1[0]) cursor2.execute("SELECT name FROM sqlite_master WHERE type='table';") for row2 in cursor2: tables.add(row2[0]) print('Found tables', tables) # Found tables {'snille', 'slemme'} def check_table(table, cursor1, cursor2): cursor1.execute("SELECT * FROM %s" % table) cols = [] query = 'SELECT COUNT(*) FROM %s WHERE ' % table for (i, col_desc) in enumerate(cursor1.description): cols.append(col_desc[0]) if i > 0: query += 'AND ' query += col_desc[0] + '=? ' print('Columns:', cols) print('Query:', query) for row1 in cursor1: params = [] for i in range(len(cols)): params.append(row1[i]) cursor2.execute(query, params) numberOfRows = cursor2.fetchone()[0] if numberOfRows != 1: # Not same row in second cursor print(numberOfRows, params) for table in tables: print('Checking table', table) check_table(table, cursor1, cursor2) check_table(table, cursor2, cursor1) # 0 ['Agnes', 'Brekke', '49422712408d5409a3e40945204314e6'] # PST{49422712408d5409a3e40945204314e6}
Another way of solving the the challenge was to just verify the MD5 sum of all rows. The MD5 hash is incorrect for the altered row.
The row we are looking for is the one with MD5 49422712408d5409a3e40945204314e6
. The flag was PST{49422712408d5409a3e40945204314e6}
.
SPST had in the start of December published SLEDE8 source code to their GitHub user. NPST was able to get hold of the code before it was removed and is now working on finding out how SPST could have gotten the language specification. We are given the assembled code and is asked to make sense of it. Luckily we also got the source code of the SLEDE8 runtime.
- program.s8
- github.com/PSTNorge/slede8
- Python
- slede8.npst.no
I created a SLEDE8 decompiler in Python. I used slede8.npst.no to get assembled bytecode and see how different aspects of the source code ended up byte code. After a little while I had a decompiler that decompiled program.s8
into source code that slede8.npst.no assembled into the exact same bytecode. However, because of the way that SLEDE8 lets one store generic data (that is run as regular code if jumped to) one can't know for sure if it the source code looks the same as it did for the author.
Making the decompiler wasn't necessary, but made it easier to understand the flow of the code and gave a real good understanding of the language. There was one variable in program.s8
that controlled how much input the program read, and if all input was correct it would print out "Korrekt!". I created a simple HTML file that included the SLEDE8 runtime, gave in the program and run it. Then I made two for loops - one going from 1 to the original number of input bytes, and one running through all ASCII characters. Whenever "Korrekt!" was printed I stored the correct character and increased the size of the input.
<html> <meta charset="utf-8" /> <head> <script src="./index.js"></script> <script> var input = []; for (var inputLength = 1; inputLength <= 0x1a; inputLength++) { var code = '.DATA 0x51, 0x00, 0x61, 0x01, 0xA1, 0x00, 0xB1, 0x01, 0xC1, 0x00, 0x83, 0x03, 0x91, 0x' + inputLength.toString(16).padStart(2, '0') + ', 0x06, 0x02, 0x04, 0x03, 0x72, 0x05, 0x55, 0x67, 0x25, 0x72, 0x25, 0x32, 0x15, 0x2C, 0x52, 0x06, 0x62, 0x07, 0x65, 0xB9, 0x55, 0xB0, 0x17, 0xA9, 0xE9, 0x00, 0x07, 0xAC, 0x29, 0x03, 0xB3, 0x05, 0x1A, 0x06, 0x00, 0x00, 0x23, 0x05, 0x1A, 0x06, 0x00, 0x00, 0x51, 0x51, 0x57, 0x7E, 0x6E, 0x64, 0x77, 0x12, 0x59, 0x38, 0xF3, 0x8A, 0x48, 0x3D, 0xEB, 0x53, 0x7D, 0x21, 0x5C, 0xAF, 0x1C, 0xAE, 0x50, 0x25, 0x55, 0x3F, 0x4B, 0x6F, 0x72, 0x72, 0x65, 0x6B, 0x74, 0x21, 0x00, 0x46, 0x65, 0x69, 0x6C, 0x21, 0x00, 0x04, 0x02, 0x07, 0xA2, 0xD9, 0x06, 0x16, 0x02, 0x55, 0xB0, 0x18, 0x06, 0x0B, 0x00'; for (var char = 0x00; char <= 0xff; char++) { var tempInput = [...input]; tempInput.push(char); var a = assemble(code); var s = step(a.exe, tempInput); var next; var output = '' do { next = s.next() } while (!next.done); var output = '' for (var i = 0; i < next.value.stdout.length; i++) { output += String.fromCharCode(next.value.stdout[i]); } if (output != 'Feil!') { console.log(output); input.push(char); var flag = '' for (var i = 0; i < input.length; i++) { flag += String.fromCharCode(input[i]); } console.log('Flag:', flag); // Flag: PST{fib0nacc1_0net1m3_p4d} break; } } } </script> </head> <body> </body> </html>
The correct input to the program - and the flag - was PST{fib0nacc1_0net1m3_p4d}
.
The fibonacci one-time pad in the flag is related to how the registers are set to the fibonacci sequence when checking the input to the program.
๐.s8
When handing in the flag for December 11 we were given an extra task in a SLEDE8 program called ๐.s8
. I solved it by having the SLEDE8 runtime printing out every instruction it executed.
I saw that in the end if the r8
register was 0
, it would do a jump and print "Correctamundo!". I also noticed that it did OR
between r8
and r9
(which was 0
) after reading the input. When I started the input with EGG{
I saw that r8
was 0
for each character at that stage. So i altered the runtime to count the number of ELLER r8, r9
where they were both 0
and threw an exception whenever it wasn't. Just like in the December 12 challenge I then altered one character at the time until I got one more "successful" OR
.
The correct input to the program - and the flag - was EGG{513d38_master_reverser}
.
A message has been sent to NPST via fax, but no one is able to understand the contents. There's some kind of hex encoding, but it doesn't make any sense when doing hex decoding.
- dcode.fr
- Text editor
This was probably the most annoying challenge. It's really not my cup of tea. Hours were wasted trying to attack this from all kinds of angles like fax formats and just staring to see some kind of pattern. It turned out to be a lot like last year's December 11th challenge called "1337" (which I also disliked): There is a hidden pattern in there.
To make the pattern clear one could use frequency analysis on each character (not hex pairs). I used https://www.dcode.fr/frequency-analysis and then just search and replace in a text editor for the most frequent occurrences.
But of course it's much cooler to do this visually with a few lines of Python and ANSI escape codes:
import time import re contents = f = open('melding.txt').read() frequencies = {} char_percentage = 1 / len(contents) for char in contents: if char == '\n': continue if char in frequencies: frequencies[char] = frequencies[char] + char_percentage else: frequencies[char] = char_percentage frequencies = dict(sorted(frequencies.items(), key=lambda item: item[1])) clear_screen = '\033[H\033[J' color_black = '\033[30m' color_green = '\033[1;32m' color_reset = '\033[0m' threshold_removal = 0.08 print(clear_screen + color_black + contents) chars_to_color = [] for key in frequencies: time.sleep(0.7) if frequencies[key] < threshold_removal: chars_to_color.append(key) print(clear_screen + color_black + re.sub('(' + '|'.join(chars_to_color) + ')', color_green + r'\1' + color_reset + color_black, contents)) else: break # PST{SNEAKY_FLAG_IS_SNEAKY}
Running the script looks like this:
The flag was PST{SNEAKY_FLAG_IS_SNEAKY}
.
It's yet another SLEDE8 e-learning module: 97672649875ca349
; Fรธde bestรฅr av et ukjent antall verdier, der verdien 0x00 markerer siste verdi. ; Skriv ut verdiene i motsatt rekkefรธlge. ; Eksempel: 11223344556600 => 665544332211 ; Eksempel: 0123456789abcdef00 => efcdab8967452301
It's a pretty simple task. It's a good exercise in using FINN
, LAGR
and LAST
.
; Fรธde bestรฅr av et ukjent antall verdier, der verdien 0x00 markerer siste verdi. ; Skriv ut verdiene i motsatt rekkefรธlge. ; Eksempel: 11223344556600 => 665544332211 ; Eksempel: 0123456789abcdef00 => efcdab8967452301 SETT r2, 0x01 ; Teller-รธkning FINN data ; Lagrer adressen til data i r0+r1 SETT r3, r0 ; Teller LSB SETT r4, 0x00 ; Teller MSB SETT r5, 0xff ; Maks-verdi fรธr overflyt SETT r6, 0x00 ; 0-sjekk ; r7 brukes til tegn som leses/skrives SETT r8, r0 ; Husker opprinnelig teller les: LES r7 ; Les neste tegn LIK r7, r6 ; Ikke mer input BHOPP skriv ; Ferdig รฅ lese - skriv SETT r0, r3 ; Setter adressen til rett sted SETT r1, r4 ; Setter adressen til rett sted LAGR r7 ; Lagrer input i data LIK r3, r5 ; Maks grense pรฅ LSB-teller BHOPP juster_teller PLUSS r3, r2 ; Teller++ HOPP les ; Les neste tegn skriv: LIK r3, r8 ; Teller nede pรฅ fรธrste data-adresse BHOPP sjekk_msb_teller LIK r3, r6 ; Teller nede pรฅ 0 BHOPP juster_teller_ned skriv2: MINUS r3, r2 ; Teller-- SETT r0, r3 ; Setter adressen til rett sted SETT r1, r4 ; Setter adressen til rett sted LAST r7 ; Hent tegn som skal skrives SKRIV r7 ; Skriv tegn HOPP skriv juster_teller: PLUSS r4, r2 ; Teller MSB++ SETT r3, 0x00 ; Teller LSB = 0 HOPP les juster_teller_ned: LIK r4, r6 BHOPP stopp ; MINUS r4, r2 ; Teller MSB-- HOPP skriv2 sjekk_msb_teller: LIK r4, r6 ; Teller MSB = 0 BHOPP stopp HOPP skriv2 stopp: STOPP data:
The server gave back the flag Godkjent!\nDin verifikasjonskode er: PST{InReverseCountryEverythingIsPossible}
.
When handing in the December 14 challenge there's another e-learning module code returned: dc0583ff102e48c6
The challenge is the exact same, only that the code needs to run using 2,600 or less cycles (instruction calls). My previous code wasn't very efficient in regards of looking up and storing the memory addresses. I changed that and was good to go:
SETT r0,0x00 ; Minne LSB SETT r1,0x01 ; Minne MSB SETT r2,0x01 ; Teller-รธkning SETT r3,0x00 ; 0-sjekk ; r4 = innlesing SETT r5, 0xff les: LES r4 LIK r4, r3 BHOPP skriv LAGR r4 PLUSS r0, r2 LIK r0, r3 ; Rundt igjen pรฅ 0 BHOPP รธk_msb HOPP les รธk_msb: PLUSS r1, r2 HOPP les mink_msb: LIK r1, r2 BHOPP stopp MINUS r1,r2 HOPP fortsett_skriv skriv: MINUS r0, r2 LIK r0, r5 BHOPP mink_msb fortsett_skriv: LAST r4 LIK r4, r3 BHOPP stopp SKRIV r4 HOPP skriv stopp: STOPP
After passing the unit tests on the server side ones get back the following message Godkjent!\nDin verifikasjonskode er: EGG{5f5fc8819e2cc6be9c6a19370a5030af}
Following a private trip (earlier this year) to watch football in England, one of the elf officers has repeatedly picked up a mysterious signal. It looks like the signal is quite continuous, but it varies slightly in frequency.
This one took me a while as I overlooked the solution the first time around. As with the other challenge with a complex16u
file it can be solved using the Universal Radio Hacker (urh) program. I suppose the football in England part can be a hint. What is a popular team in England? Manchester United. What is the name of a line code? Manchester II. Opening the file in urh one could analyse the signal as Manchester II and get the ASCII chars - and flag - PST{m4nch3st3r_3nc0d1ng_1s_4_l0t_0f_fun!}
.
It's yet another SLEDE8 e-learning module: a522c5a55bcb743e
; Fรธrste byte med fรธde er et tall N som representerer ; antallet pรฅfรธlgende bytes med fรธde. ; de pรฅfรธlgende verdiene representerer en liste med verdier. ; skriv ut verdiene i lista sortert i stigende rekkefรธlge ; Eksempel: 06112233445566 => 112233445566 ; Eksempel: 06665544332211 => 112233445566 ; OBS: Implementasjonen kan ikke benytte mer enn (24* N^2 + 5000) skritt. ; OBS: Du kan endre maks antall skritt lokalt ved รฅ skrive localStorage.setItem('๐ฒ', 10000000)
I'm a big fan of SLEDE8, but waking up to having to implement yet another algorithm in assembly was the lowest point for me on this CTF. If I was into coding algorithms for fun I would sign up for a different challenge. It felt like advent of code - assembly edition.
Luckily we were told that this would be the last e-learning module, and because of the constraints of SLEDE8 this wasn't like implementing the average sorting algorithm. This solution uses the fact that the highest number in can be 0xff
. It just stores each number's count in the memory address equal the number itself.
SETT r9, 0x01 ; Teller-endring SETT r14, 0xff ; Maks stรธrrelse LES r15 ; Antall tegn igjen SETT r12, 0x00 ; 0-sjekk SETT r1, 0x01 ; Adresse for lagring SETT r3, 0xff ; Laveste tall funnet/tallet som sorteres nรฅ SETT r4, 0x00 ; Hรธyeste tall funnet les: LIK r15, r12 ; 0 tegn igjen BHOPP sortere MINUS r15, r9 ; Antall tegn igjen -= 1 LES r0 ; Neste tall inn LAST r7 ; Last tilsvarende adresse inn PLUSS r7, r9 ; Antall tall for denne verdien += 1 LAGR r7 ; Lagre r7 tilbake ME r0, r3 ; Siste leste tall mindre enn minste tall funnet BHOPP ny_minste SE r0, r4 ; Siste leste tall stรธrre enn stรธrste tall funnet BHOPP ny_stรธrste HOPP les sortere: SETT r0, r3 ; Sett nรฅvรฆrende tall som sorteres LAST r6 ; Last antall tall av nรฅvรฆrende verdi her: LIK r6, r12 ; 0 tall av nรฅvรฆrende verdi BHOPP neste SKRIV r3 ; Skriv nรฅvรฆrende tall MINUS r6, r9 ; Antall tall av nรฅvรฆrende tall -= 1 HOPP her neste: LIK r3, r4 ; Nรฅvรฆrende tall er lik hรธyeste tall funnet BHOPP stopp PLUSS r3, r9 ; Nรฅvรฆrende tall += 1 HOPP sortere stopp: STOPP ny_minste: SETT r3, r0 HOPP les ny_stรธrste: SETT r4, r0 HOPP les
When the unit tests were approved the server gave back this message - and flag: Godkjent!\nDin verifikasjonskode er: PST{youtu.be/k4RRi_ntQc8}
When handing in the December 16 challenge there's another e-learning module code returned: 611b1f7f8c63469e
The challenge is the exact same, only that the code needs to run using 4,608 or less cycles (instruction calls).
I trimmed my original solution to not keep track of the highest and lowest number to avoid too many instructions used. But I couldn't immediately make it past test 5/12 because it used too many cycles. That's when I added a pretty nice hack. The test didn't give any details on how it worked, but I made the app crash on purpose if the input size was 0xff
numbers. Indeed test 5/12 crashed. Then I did a dirty hack to make a special case if the input size was 0xff
and just read the input blindly 255 times and stored the numbers like in the previous solution. I saved a lot of jumps and checks and it immediately made it through 12 out of 12 tests.
I won't publish the code here as it just looks silly with 255 x the same lines.
After passing the unit tests on the server side ones get back the following message Godkjent!\nDin verifikasjonskode er: EGG{a34ae56d455e16b08cfe07f585ed44d9}
NPST has tapped the communication of a suspected intelligence officer from SPST. The telecommunications operator has transmitted data in accordance with ETSI232-1, but NPST's systems are unable to understand the content. It's suspected that a very simple code has been used, but it is unlikely that XMAS
has been used.
- data.b64.txt
- ETSI232-1.txt
ETSI TS 102 232-1 is a technical specification for lawful interception. It uses ASN.1 for representing the information.
I used asn1.io to decode the intercepted conversation. The conversation itself is encrypted, but we're told that it's likely to be simple to crack. Instead of the mentioned XMAS
it's XOR. CyberChef helped me to brute force the ciphertext and decrypt it for me. The key for the encryption was 0x24
(ASCII $
).
I've since created a Python script that decompiles the message, cracks the XOR key, decrypts it and prints the conversation:
import asn1tools # pip3 install asn1tools import base64 import string SPECIFICATION = ''' Spec DEFINITIONS ::= BEGIN LawfulInterceptionIdentifier ::= OCTET STRING (SIZE (1..25)) PS-PDU ::= SEQUENCE { pSHeader [1] PSHeader, payload [2] Payload } PSHeader ::= SEQUENCE { li-psDomainId [0] OBJECT IDENTIFIER, lawfulInterceptionIdentifier [1] LawfulInterceptionIdentifier, authorizationCountryCode [2] PrintableString (SIZE (2)) OPTIONAL, communicationIdentifier [3] CommunicationIdentifier, sequenceNumber [4] INTEGER (0..4294967295), timeStamp [5] GeneralizedTime OPTIONAL, ..., interceptionPointID [6] PrintableString (SIZE (1..8)) OPTIONAL } Payload ::= CHOICE { cCPayloadSequence [1] SEQUENCE OF CCPayload, ... } CommunicationIdentifier ::= SEQUENCE { networkIdentifier [0] NetworkIdentifier, communicationIdentityNumber [1] INTEGER (0..4294967295) OPTIONAL, deliveryCountryCode [2] PrintableString (SIZE (2)) OPTIONAL, ... } NetworkIdentifier ::= SEQUENCE { operatorIdentifier [0] OCTET STRING (SIZE(1..16)), networkElementIdentifier [1] OCTET STRING (SIZE(1..16)) OPTIONAL, ..., eTSI671NEID [2] Network-Element-Identifier OPTIONAL } Network-Element-Identifier ::= CHOICE { iP-Format [3] OCTET STRING (SIZE (1..25)), dNS-Format [4] OCTET STRING (SIZE (1..25)), ... } CCPayload ::= SEQUENCE { payloadDirection [0] PayloadDirection OPTIONAL, timeStamp [1] GeneralizedTime OPTIONAL, cCContents [2] CCContents, ... } PayloadDirection ::= ENUMERATED { fromTarget(0), toTarget(1), ... } CCContents ::= CHOICE { undefinedCC [0] OCTET STRING, ... } END ''' DATA = 'MIIEJaE3MDWgCQYHBAICAAUBDqEJBAdwZW5nd3luogQTAk5PoxAwDqAMMAqgCAQGU0FOVEVMpAUCAwj4k6KCA+ihggPkMIID4DAVoAMKAQCiDqAMBApjS0AET1JBSEAFMBCgAwoBAKIJoAcEBWtSQVYKMBCgAwoBAaIJoAcEBWxBTQoEMCKgAwoBAaIboBkEF2xFVgRAUQRCUUpKQVAESktBBEPnnF0bMBagAwoBAKIPoA0EC25FCARXQQRMQVYKMA6gAwoBAaIHoAUEAxsbBDAfoAMKAQGiGKAWBBRuQUMEV0FWBE1KQ0FKBFBNSkMKBDAboAMKAQCiFKASBBAODg4ODg4ODg4ODg4ODg4OMCigAwoBAaIhoB8EHW5BQwRXQVYERkVWQQQODg4ODg4ODg4ODg4ODg4OMDugAwoBAKI0oDIEMGtNCAROQUMER0tUXQtURVdQQVAEVEVXV0tWQEFQBElNUFAEUkFABEFKBEJBTUgKBDAeoAMKAQCiF6AVBBNmVkUEQEFQBEZIQQRXSEVAQEFQMBGgAwoBAaIKoAgEBk5BQ0FWFjANoAMKAQCiBqAEBAIbGzAdoAMKAQGiFqAUBBJgQVAEQlFKT0FQBE1PT0EKCgowG6ADCgEAohSgEgQQCgoKBFJBSlAESE1QUAQKCjAToAMKAQCiDKAKBAhAHUcXEkdHQjAPoAMKAQGiCKAGBARM54IbMA+gAwoBAKIIoAYEBBJFFxwwD6ADCgEAogigBgQEEBYcFTAPoAMKAQCiCKAGBARGEBxCMA+gAwoBAaIIoAYEBBsbGxswF6ADCgEAohCgDgQMQBUQQEYSHRBARUVBMBugAwoBAaIUoBIEEGxSRQRXQVYETkFDBFTngRkwI6ADCgEAohygGgQYYEFQBFdPRUgEUueCVkEEQUoEUVFNQAoEMCygAwoBAKIloCMEIWZNSkBBV1BWQU9PSkVUVEFKBElNSgRCUUpPQVYETU9PQTAroAMKAQGiJKAiBCBrQwRMUkUEQ0tAUARXT0VIBEBBUARDTuecVkEESUFDGzBPoAMKAQCiSKBGBERgUQRJ54EEUEUESUARBEVSBFFRTUADQUoEV0tJBEhLU0FWR0VXQQRMQVwES0MESEFDQ0EEUE1IBEBBUARSRUpITUNBCjAVoAMKAQGiDqAMBAp3T07nnEpKQVYFMB6gAwoBAaIXoBUEE2BBUARCUUpPQVYETU9PQQQKCgowNKADCgEAoi2gKwQpc0xLS1RXCgRxUU1AQUoEV09RSEhBBFdQRVZQQQRJQUAERx1HDAoKCg0wJaADCgEAoh6gHAQaCgoKBEtDBFdIUVBQQQRJQUAEDAoKCg0QRRcwFaADCgEBog6gDAQKc21qBQRwRU9PCjAYoAMKAQCiEaAPBA1xSkBBVgRLQwRNSkoK' compiled_specification = asn1tools.compile_string(SPECIFICATION, codec='ber') # for key in compiled_specification.types: # print('\n', key, ':', compiled_specification.types[key]) decoded = compiled_specification.decode('PS-PDU', base64.b64decode(DATA)) target = 'target' other = 'other' conversation = [] def traverse_data(data, output, level): global target, other, conversation if type(data) is dict: #output.append(' ' * (level * 4) + '\n') for key in data: if type(data[key]) is str: if key == 'payloadDirection': sender = target if data[key] == 'fromTarget' else other message = data['cCContents'][1].hex() conversation.append({'from': sender, 'message': message}) output.append(' ' * (level * 4) + key + ': s[' + data[key] + ']\n') elif type(data[key]) is bytes: if key == 'lawfulInterceptionIdentifier': target = data[key].decode('ascii') output.append(' ' * (level * 4) + key + ': b[' + data[key].decode('ascii') + ']\n') elif type(data[key]) is int: output.append(' ' * (level * 4) + key + ': i[' + str(data[key]) + ']\n') else: output.append(' ' * (level * 4) + key + ':\n') traverse_data(data[key], output, level + 1) #output.append(' ' * (level * 4) + ' \n') elif type(data) is list: #output.append(' ' * (level * 4) + '\n') for element in data: traverse_data(element, output, level + 1) #output.append(' ' * (level * 4) + '
\n') elif type(data) is tuple: #output.append(' ' * (level * 4) + '\n') if len(data) == 2 and data[0] == 'undefinedCC': # Type of data = octet string output.append(' ' * (level * 4) + data[0] + ': h[' + data[1].hex() + ']\n') else: for t in data: traverse_data(t, output, level + 1) #output.append(' ' * (level * 4) + ' \n') elif type(data) is str: output.append((' ' * (level * 4)) + data + '\n') elif type(data) is bytes: #output.append(' ' * (level * 4) + 'hex:[' + str(data.hex()) + '] / ascii:[' + data.decode('ascii', 'ignore') + ']\n') try: output.append((' ' * (level * 4)) + data.decode('ascii') + '\n') except UnicodeDecodeError: output.append((' ' * (level * 4)) + data.hex() + '\n') elif type(data) is int: #output.append(' ' * (level * 4) + 'int:[' + str(data) + ']\n') output.append((' ' * (level * 4)) + str(data) + '\n') else: raise Exception('Unsupported char type ' + str(type(data)) + '(value:' + str(data) + ')') output = [] traverse_data(decoded, output, 0) output = ''.join(output) print(output) color_blue = '\033[94m' color_reset = '\033[0m' padding = max(len(target), len(other)) cipher_text = '' for c in conversation: cipher_text += c['message'] color = color_blue if c['from'] == target else '' print('%s%s: %s%s' % (color, c['from'].ljust(padding, ' '), c['message'], color_reset)) def xor(data, key): key = bytes(key * (len(data) // len(key) + 1), encoding='utf8') return bytearray(a ^ b for a, b in zip(*map(bytearray, [data, key]))) def count_chars(s, chars): return sum(s.count(ord(c)) for c in chars) max_key_length = 8 alphabet = string.ascii_uppercase + string.ascii_lowercase + 'รฆรธรฅรรร .,{}' + string.whitespace + string.digits # Assuming these should be most of the chars in the text cipher_hex = bytearray.fromhex(cipher_text) best_key = None best_key_score = 0 for key_length in range(1, max_key_length + 1): # Trying key lengths 1-max print('Checking key size %d.' % key_length) key = 'a' * key_length # Default starting key for i in range(1, key_length + 1): best_hex_score = 0 best_letter = None for l in string.printable: key = key[0:i - 1] + l + key[i: len(key)] xor_result = xor(cipher_hex, key) score = count_chars(xor_result, alphabet) # Scoring the key by the number of expected chars the plaintext contains #print(key, score) if score >= best_hex_score: best_hex_score = score best_letter = l key = key[0:i - 1] + best_letter + key[i: len(key)] # Keeping the best letter for each position score = count_chars(xor(cipher_hex, key), alphabet) if score >= best_key_score: best_key_score = score best_key = key # Prints stuff like 'Found new best key [$] (score: 573).': print('Found new best key [%s] (score: %d).' % (best_key, best_key_score)) print('\nDecrypting conversation using key [%s]:' % (best_key)) for c in conversation: color = color_blue if c['from'] == target else '' decrypted_text = xor(bytearray.fromhex(c['message']), best_key).decode('utf-8') print('%s%s: %s%s' % (color, c['from'].ljust(padding, ' '), decrypted_text, color_reset))
The conversation output is like this:
If you follow the flow of the conversation you can see that the UUID is c9c36ccf-6a38-4281-b48f-d14db694d4a3
. To get the flag one needs the MD5 sum:
$ echo -ne 'c9c36ccf-6a38-4281-b48f-d14db694d4a3' | md5 0ae06caf767ac7ebce290cfc57be6a6f
The flag was PST{0ae06caf767ac7ebce290cfc57be6a6f}
. Oh, and the whole jeger2
and ****************
is of course the good old hunter2 meme.
SPST has published what they say are very advanced artificial intelligence to their GitHub account.
The AI is available in action at pingvin.spst.no. We are asked to have look at it.
- pingvin.spst.no
- github.com/SydpolarSikkerhetstjeneste/kunstig-pingvinopptellingsintelligens
- Python
This was one of my favourite challenges. It's not every day you get to do a buffer overflow combined with a remote code execution.
The website at pingvin.spst.no lets you input a number of penguin emojis - ๐ง - and a call goes to the backend which counts them and returns the answer.
Luckily we have got the source code that runs on the backend: https://github.com/SydpolarSikkerhetstjeneste/kunstig-pingvinopptellingsintelligens/blob/main/ai.js
Clearly there's a buffer called input_buffer
where all the input data is stored. As an attacker it's very practical that the main program runs just after the buffer:
... 22: SETT r10, 0 23: SETT r11, 1 24: HOPP forbi 25: 26: flagg: 27: .DATA ${Buffer.from(flag).join(",")},0 28: 29: print: 30: LAST r2 31: PLUSS r0, r11 32: LIK r2, r10 33: BHOPP print_ferdig 34: SKRIV r2 35: HOPP print 36: print_ferdig: 37: RETUR 38: 39: input_buffer: 40: .DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 41: .DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 42: .DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 43: .DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 44: .DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 45: .DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 46: .DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 47: .DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 48: 49: forbi: 50: TUR les_input 51: TUR tell_pingviner 52: TUR skriv_svar 53: fin: 54: STOPP ...
On line 49 the code jumps to les_input
and reads the input until it gets a null-byte and returns to execute line 50. What we can do is to overflow the buffer so that it's our code that is executed on line 50. We can execute the code FINN flagg
to load the address into memory and then HOPP print
which will print out the contents of the address we just loaded.
We can calculate the address of the flag (byte 0x06), but the position of the print
function depends on the size of the flag. Even though the position of input_buffer
+ input size is stored in the registers r0
and r1
we are unable to use the values for jumping. I couldn't find one single perfect payload to get the flag without doing some sort of brute forcing or guessing.
When I tried to find the address scope I filled the buffer with SKRIV r11; r11 = 0x01
(160b
). Then I had a large hit area where I could count the number of 1
s returned to quickly get oriented.
After locating print
the only call needed to print the flag would be this:
import requests import json import base64 hex_string = '0c' * 130 # Filling the buffer + 2 with nonsense to reach execution point hex_string += '0106' # SETT r0, 0x06 (position calculated) (couldn't use FINN flagg as it would have inserted a null-byte: 0x6300) hex_string += '7a02' # TUR print (position found by brute forcing) - jumps to print function at 0x27 hex_string += '58ff' # HOPP 0xff5 (position that should contain nullbytes) - jumps to null-byte to stop execution response = requests.get('https://pingvin.spst.no/.netlify/functions/count', params={'input': base64.b64encode(bytearray.fromhex(hex_string))}) print(''.join(map(chr,json.loads(response.text)['svar']))) # Prints PST{EveryoneAboardTheNOPESlede8}
The flag was Prints PST{EveryoneAboardTheNOPESlede8}
.
I wonder if the flag was referring to the NOPE
operation (0c00
(can be set to 0c0c
to avoid null byte)) that can be used in combination with SETT r0, 0x0
before skriv_svar
is called and then the program is stopped. Depending on how good or bad your brute force guessing is this could be a just as effective solution.
pingvin.spst.no linked to egg.spst.no where you can input the right value to get an egg returned. The correct input was the password ****************
from December 17. It looks like the conversation wasn't really a hunter2 meme, but a real slip-up of the password. However, jeger2
didn't work as input at the site.
Inputting ****************
gave a redirect to https://egg.spst.no/c9ac37f8b4a4d689456d756485428522/ which had the flag EGG{AllIWantForChristmasIsPfeffErminZ}
.
To prevent the responsibility for the Christmas gift vault from resting on one individual, elf officer Sigurd has developed an algorithm that can divide a secret into X numbers of equal shares. The algorithm is further designed so that a Y number of arbitrary proportions is needed to be able to return to the original secret.
In the trial phase, Sigurd has divided the key to the Christmas gift vault into five parts, and decided that three parts are needed to unlock it. Sigurd has given the first two shares (1 and 2) to Santa, while elf officer Reidar has received share 3, and elf officer Adrian has received share 5. Sigurd has kept share 4 himself.
(X = 5, Y = 3)
This means that the vault can be opened either by Santa together with one arbitrary elf officer, or by all three elf officers together.
As a "curiosity" we are told that Sigurd's favourite number is 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812554028
It turns out that Jule NISSEN has lost its shares. The remaining known shares are
Reidar: (3, 570999082059702856147787459046280784390391309763131887566210928611371012340016305879778028495709778777
)
Sigurd: (4, 922383132557981536854118203074761267092170577309674587606956115449137789164641724882718353723838873409
)
Adrian: (5, 1361613195680829887737031633110361870469394661742852962657887598996346260195423498636393760259000241699
)
The challenge is to recreate the key to the Christmas gift vault.
- Wikipedia
- Python
This is clearly about the very cool concept of secret sharing. The text strongly hints towards RSA. The S in RSA stands for Shamir who invented Shamir's Secret Sharing.
Conveniently the Wikipedia article includes Python code for the algorithm. It can be used directly to recreate the key.
The challenge when being handed code like this is that it's easy to just use it as-is without really understanding all conecepts. That's what I did at first. The prime number in the code example needs to be changed with Sigurd's favourite number.
First the code from Wikipedia:
from __future__ import division from __future__ import print_function import random import functools # 12th Mersenne Prime # (for this application we want a known prime number as close as # possible to our security level; e.g. desired security level of 128 # bits -- too large and all the ciphertext is large; too small and # security is compromised) _PRIME = 2 ** 127 - 1 # 13th Mersenne Prime is 2**521 - 1 _RINT = functools.partial(random.SystemRandom().randint, 0) def _eval_at(poly, x, prime): """Evaluates polynomial (coefficient tuple) at x, used to generate a shamir pool in make_random_shares below. """ accum = 0 for coeff in reversed(poly): accum *= x accum += coeff accum %= prime return accum def make_random_shares(minimum, shares, prime=_PRIME): """ Generates a random shamir pool, returns the secret and the share points. """ if minimum > shares: raise ValueError("Pool secret would be irrecoverable.") poly = [_RINT(prime - 1) for i in range(minimum)] points = [(i, _eval_at(poly, i, prime)) for i in range(1, shares + 1)] return poly[0], points def _extended_gcd(a, b): """ Division in integers modulus p means finding the inverse of the denominator modulo p and then multiplying the numerator by this inverse (Note: inverse of A is B such that A*B % p == 1) this can be computed via extended Euclidean algorithm http://en.wikipedia.org/wiki/Modular_multiplicative_inverse#Computation """ x = 0 last_x = 1 y = 1 last_y = 0 while b != 0: quot = a // b a, b = b, a % b x, last_x = last_x - quot * x, x y, last_y = last_y - quot * y, y return last_x, last_y def _divmod(num, den, p): """Compute num / den modulo prime p To explain what this means, the return value will be such that the following is true: den * _divmod(num, den, p) % p == num """ inv, _ = _extended_gcd(den, p) return num * inv def _lagrange_interpolate(x, x_s, y_s, p): """ Find the y-value for the given x, given n (x, y) points; k points will define a polynomial of up to kth order. """ k = len(x_s) assert k == len(set(x_s)), "points must be distinct" def PI(vals): # upper-case PI -- product of inputs accum = 1 for v in vals: accum *= v return accum nums = [] # avoid inexact division dens = [] for i in range(k): others = list(x_s) cur = others.pop(i) nums.append(PI(x - o for o in others)) dens.append(PI(cur - o for o in others)) den = PI(dens) num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p) for i in range(k)]) return (_divmod(num, den, p) + p) % p def recover_secret(shares, prime=_PRIME): """ Recover the secret from share points (x, y points on the polynomial). """ if len(shares) < 2: raise ValueError("need at least two shares") x_s, y_s = zip(*shares) return _lagrange_interpolate(0, x_s, y_s, prime)
Then the code for solving the challenge:
from Crypto.Util.number import long_to_bytes # pip3 install pycrypto prime = 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151 shares = [] shares.append((3, 570999082059702856147787459046280784390391309763131887566210928611371012340016305879778028495709778777)) shares.append((4,922383132557981536854118203074761267092170577309674587606956115449137789164641724882718353723838873409)) shares.append((5,1361613195680829887737031633110361870469394661742852962657887598996346260195423498636393760259000241699)) secret = recover_secret(shares, prime) # 43923006312284835088291343003560060337722408443317837505093148354720847103078177375367540653516136829 print('Secret recovered from minimum subset of shares:', secret) flag = long_to_bytes(secret).decode() print(flag) # Prints PST{f0rd3lt_4nsv4r_3r_d3t_b3st3_4nsv4r3t!}
The flag was PST{f0rd3lt_4nsv4r_3r_d3t_b3st3_4nsv4r3t!}
.
It's believed that an intruder may have gained access to NPST's internal network. System variables seem to be tampered with, and also that some information might be missing.
- Wireshark
- file
- binwalk
- unzip
- dd
pcapng
files are packet capture files. Wireshark is probably the most common GUI tool for analyzing them. First step is to open it and look for interesting things. Filtering for http
traffic reveals a POST
to the path /api/submitit?code=aMdhWlyaKU5cMjZ4sk6njSRcVVrS6FpiKrLLvbNIswaNcBAW/PyPwg==
at http://shadyserverfunction.azurewebsites.net
. Pretty shady stuff.
There is data being sent to the server. Going to File --> Export Objects --> HTTP...
lets you export the data as a files.
Using binwalk we can make sense of the main file:
$ file * PyPwg==: data PyPwg==2: ASCII text, with no line terminators $ cat PyPwg==2 Hemmeligheten er mottat $ binwalk PyPwg== DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 9342 0x247E Zip archive data, at least v2.0 to extract, compressed size: 1539787, uncompressed size: 1855860, name: file2 1549251 0x17A3C3 End of Zip archive, footer length: 22 $ unzip PyPwg== Archive: PyPwg== warning [PyPwg==]: 9342 extra bytes at beginning or within zipfile (attempting to process anyway) inflating: file2 $ file file2 file2: pcapng capture file - version 1.0
We got ourselves another packet capture file.
But do you see how the ZIP file starts at hex 0x247E
in the file? And how unzip
complains about extra data? If you open the PyPwg==
file in a hex editor or just look closer at the HTTP call in Wireshark you'll see that the file contains of two files from a multipart/form-data
POST.
We have to carve out the other file ourselves - either by copy and pasting from Wireshark, a hex editor or by command line after finding the start and end:
$ dd if="PyPwg==" bs=1 skip=156 count=9030 of=file1 9030+0 records in 9030+0 records out 9030 bytes transferred in 0.021150 secs (389887 bytes/sec) $ file file1 file1: ASCII text, with very long lines, with CRLF line terminators
file1
contains base64 encoded data that is decoded like this:
CLIENT_HANDSHAKE_TRAFFIC_SECRET c08e088c3a8de40c4e984836f470b57ddd9563580d77039a07902265be82c392 9a396f29df0c36bd2a48bc02230ba5e45593c8b8645d5cc095762c633ce1f40b
SERVER_HANDSHAKE_TRAFFIC_SECRET c08e088c3a8de40c4e984836f470b57ddd9563580d77039a07902265be82c392 677422db66a266caaef05441d06f62fd8d52a2133ecafc4b9a84fdad4e58c7fb
...
EXPORTER_SECRET 8b91e59009e1bed5fe88fd66640898c8c60ff61a14b0354dec6b1c965e822a6e 0c38ea238677e50a0347dd058d46c991ba5eb548b189829bd80e2cf79a2bd907dd7fb1fe11fdc27ece31c44420a4b331
I recognized this from PST's previous Easter CTF. I loaded file2
into Wireshark and went to Wireshark --> Preferences --> Protocols --> TLS --> (Pre)-Master-Secret log filename
and set it to the base64 decoded file1
.
This lets Wireshark decrypt the TLS traffic in the belonging packet capture file. Looking at the http
traffic there's a secretdoc.pdf
that is downloaded and can be stored in the same matter as explained above. The PDF contains the flag PST{5h4dy53rv3r}
NPST has received a message from a cooperating service, but it seems that they have forgotten to send the key in a separate, secure channel.
Another elf officer has identified a pattern in the message, and has managed to decode the first four characters. Unfortunately, this elf officer has taken a break today, reportedly to play tetris.
- CyberChef
- Python
The mention of Tetris and patterns brings back memories from challenge 23 in last year's calendar. The challenge is about cellular automation. Last year it was about Rule 30.
There are 10 lines - or generations - in the text file. All except the first one is 288 chars long. Putting all the binary into a binary to ascii tool reveals that the first line is decoded to PST{
. That can't be a coincident.
We need to follow the generations backwards to get the full line - and flag - of the first generation. But what kind of ruleset are we looking at?
In hindsight, looking at other solutions, I see that I could have done this about a 100 times simpler, but here's what I did to get the solution:
import binascii lines = open('generasjoner.txt', 'r').read().split('\n') for i, line in enumerate(lines): line = line[5:] # Removing the 'genX:' prefix lines[i] = line RULE_SET = ['111', '110', '101', '100', '011', '010', '001', '000'] rule = [None] * len(RULE_SET) # First we look through all data to find the rule for all patterns: for line_no in range(1, len(lines) - 2): # Skipping first and last line (first for missing data, last as we don't know what comes after) for cell_no in range(1, len(lines[1]) - 2): # We skip the edges to avoid finding out what rule they follow pattern = lines[line_no][cell_no - 1:cell_no + 2] pattern_index = RULE_SET.index(pattern) if rule[pattern_index] is None: # Haven't registered this pattern yet rule[pattern_index] = lines[line_no + 1][cell_no] elif rule[pattern_index] != lines[line_no + 1][cell_no]: print('Error: Found conflicting states for pattern', pattern, ':', rule[pattern_index], lines[line_no + 1][cell_no]) # exit(0) # Second we check that we have a rule for all patterns for i, state in enumerate(rule): if state is None: print('Error: Missing state for pattern', RULE_SET[i]) exit(0) print(RULE_SET[i], ' => ', state) # Third we calculate which rule this is: print('The lines seem to follow rule', int(''.join(rule), 2)) # Rule 86 """ Function for getting the right pattern for a cell """ def get_state(cell_no, possible_patterns): possible_patterns2 = [] if(len(lines[1]) <= cell_no + 1): # Reached the edge return '0' for i, state in enumerate(rule): if state == lines[1][cell_no + 1]: candidate = RULE_SET[i] for pre_candidate in possible_patterns: if candidate.startswith(pre_candidate[1:]): if not candidate in possible_patterns2: possible_patterns2.append(candidate) if len(possible_patterns2) == 1: # Found one matching candidate return possible_patterns2[0] print(cell_no, possible_patterns2) return get_state(cell_no + 1, possible_patterns2) # Fourth we look at generation 1 to calculate the generation 0: for cell_no in range(len(lines[0]) - 1, len(lines[1]) - 2): # Still skipping edge cases possible_patterns = [] for i, state in enumerate(rule): if state == lines[1][cell_no]: candidate = RULE_SET[i] if candidate.startswith(lines[0][cell_no - 1 : cell_no + 1]): possible_patterns.append(candidate) #print(cell_no, lines[1][cell_no], possible_patterns) lines[0] = lines[0][0:cell_no] + get_state(cell_no, possible_patterns) def decode_binary_string(s): return (''.join(chr(int(s[i*8:i*8+8], 2)) for i in range(len(s)//8))) print(lines[0]) # 010100000101001101010100011110110111001000110011011101100011001101110010011100110011000101100010011011000011001101011111011000110011001101101100011011000111010101101100001101000111001001011111001101000111010101110100001100000110110100110100011101000011000001101110011100110011111101111101 print(decode_binary_string(lines[0])) # PST{r3v3rs1bl3_c3llul4r_4ut0m4t0ns?}
This flag was PST{r3v3rs1bl3_c3llul4r_4ut0m4t0ns?}
.
The only receiver for decrypting wish lists has broken down. NPST has received an encrypted wish list from a person high up on Santa's nice list, but can no longer decrypt it.
One of the elf officers has tried to read the firmware from one of the backup senders to get the crypto key, without succeeding. Unfortunately, it appears that the microcontroller has read protection turned on.
In another attempt an elf officer tried to connect with an oscilloscope to measure power consumption, while she sent 50 wish lists that contained only nonsense. Unfortunately the elf officer does not see any connection between the sent wish lists and measured power consumption.
The challenge is to find a connection between the wish lists and power consuption, and then get the crypto key to decrypt the message.
- viktig_melding.json
- รธnskelister.npy
- strรธmforbruk.npy
- Python
- CyberChef
A Side-channel attack is an attack based on information gained from the implementation of a computer system, rather than weaknesses in the implemented algorithm itself. Timing information, power consumption, electromagnetic leaks or even sound can provide an extra source of information, which can be exploited.
In this case we are doing a power analysis to extract the secret data from a secure device.
Luckily the hard part has been done for us. We are served Numpy files that contains information about the encryption of wish lists and the corresponding power consumption.
Searching for wordings related to this challenge gives a great walk-through that can be used for solving this challenge: https://wiki.newae.com/V4:Tutorial_B6_Breaking_AES_(Manual_CPA_Attack)
Using that code with our files we have this script:
import numpy as np HW = [bin(n).count("1") for n in range(0, 256)] sbox = ( 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16) def intermediate(pt, keyguess): return sbox[pt ^ keyguess] traces = np.load('strรธmforbruk.npy') pt = np.load('รธnskelister.npy') numtraces = np.shape(traces)[0] numpoint = np.shape(traces)[1] bestguess = [0]*16 for bnum in range(0, 16): cpaoutput = [0]*256 maxcpa = [0]*256 for kguess in range(0, 256): #print("Subkey %2d, hyp = %02x: "%(bnum, kguess)) # Initialize arrays & variables to zero sumnum = np.zeros(numpoint) sumden1 = np.zeros(numpoint) sumden2 = np.zeros(numpoint) hyp = np.zeros(numtraces) for tnum in range(0, numtraces): hyp[tnum] = HW[intermediate(pt[tnum][bnum], kguess)] # Mean of hypothesis meanh = np.mean(hyp, dtype=np.float64) # Mean of all points in trace meant = np.mean(traces, axis=0, dtype=np.float64) # For each trace, do the following for tnum in range(0, numtraces): hdiff = (hyp[tnum] - meanh) tdiff = traces[tnum, :] - meant sumnum = sumnum + (hdiff*tdiff) sumden1 = sumden1 + hdiff*hdiff sumden2 = sumden2 + tdiff*tdiff cpaoutput[kguess] = sumnum / np.sqrt(sumden1 * sumden2) maxcpa[kguess] = max(abs(cpaoutput[kguess])) print('Best', sorted(maxcpa, reverse=True)[0:3]) bestguess[bnum] = np.argmax(maxcpa) print('Best key guess: ') key = '' for b in bestguess: b = "%02x" % b key += b print(key) # 9dedc4e592b7c01d43667efaa74eb6e5
We found the secret key 9dedc4e592b7c01d43667efaa74eb6e5
. Now we can decrypt the message. I just used CyberChef and got the flag PST{1n_4_w0rld_th4t_sh0uts_4ll_1_n33d_1s_4_wh1sp3r!}
.
Santa's workshop has received a Christmas card. There was no envelope or stamp. We are asked to investigate the card.
Using StegOnline I saw that there are three different images hiding in each of the rgb 0 bit planes. In red there's a QR code which can decoded/scanned to get the text So close, yet so far...
. In green there's something that looks like a broken or half QR code. In blue there's a black and white pattern.
What you need to do is to XOR the different planes together to get another QR code. I would recommend doing this bit for bit in the different planes as it's a more generic approach. I re-used some old code on the saved the images from StegOnline and read them one QR code block at the time and diff'ed that:
from PIL import Image # pip3 install pillow files = [ '23/10-julekort-qr-r-inverse.png', '23/11-julekort-qr-g-inverse.png', '23/12-julekort-qr-b-inverse.png' ] BLOCKS_PER_LINE = 25 qrcode_binaries = [] for f in files: image = Image.open(f) pixels = image.load() width, height = image.size qrcode_binary = [] for x in range(0, width, int(width / BLOCKS_PER_LINE)): binary_string = '' for y in range(0, height, int(height / BLOCKS_PER_LINE)): t = pixels[x, y] # Colour tuple if t[0] < 200 and t[1] < 200 and t[2] < 200: # Relatively dark colour binary_string += '1' else: # Relatively white colour binary_string += '0' qrcode_binary.append(binary_string) qrcode_binaries.append(qrcode_binary) result = [] for qrcode_binary in qrcode_binaries: for i, row in enumerate(qrcode_binary): if len(result) < i + 1: result.append([]) for j, col in enumerate(row): if len(result[i]) < j + 1: result[i].append('0') if result[i][j] == '0': result[i][j] = col else: if col == '1': result[i][j] = '0' else: result[i][j] = '1' for qrcode_binary in qrcode_binaries: for row in qrcode_binary: for col in row: if col == '1': print('โโ', end='') else: print(' ', end='') print('') print('\n\n\n') for row in result: for col in row: if col == '1': print(' ', end='') else: print('โโ', end='') print('') print('\n\n\n') # PST{4ll_th3s3_d3l1c10us_l4y3rs}
This prints out the different layers + the following end result:
Scanning the QR code game the flag PST{4ll_th3s3_d3l1c10us_l4y3rs}
.
Short story: Santa's sledge has an autopilot that doesn't work well. It's written in SLEDE8. We are asked to develop a new one. There's a service pack for DASS which includes a simulator for Santa's sledge. The simulator accepts firmware upload that is compiled SLEDE8.
Both the input and output needs to be in the ASN.1 format.
The speficication is as follows:
Position ::= SEQUENCE { x INTEGER(0..255), y INTEGER(0..255) } Target ::= SEQUENCE { upperLeftCorner Position, lowerRightCorner Position } AutopilotFรธde ::= SEQUENCE { currPos Position, prevPos Position, target Target } AutopilotOppgulp ::= SEQUENCE { leftThruster BOOLEAN, rightThruster BOOLEAN, verticalThruster BOOLEAN }
- slede8.npst.no
- Vivaldi developer tools
By now we should know both ASN.1 and SLEDE8 well enough to solve this without too much trouble.
Using the browser developer tools I got a feel for how the autopilot was run. There was a simple regex for validating the ASN.1 formatted output expected from our code: /^30090101([0-9a-f]{2})0101([0-9a-f]{2})0101([0-9a-f]{2})$/
. The three were booleans telling if the left, right or vertical thruster should be turned on.
The input to the script was the coordinates for the landing platform, for the sledge and the previous position of the sledge. The only gotcha here is that ASN.1 numbers above 128 would use two bytes instead of one, so you would need to check for that.
I had a pretty simple algrithm that can roughly be described like this:
There was room for making the sledge go faster when far from landing, but simple is often the best. The code was like this:
SETT r12, 0x00 ; Venstre avslรฅtt SETT r13, 0x00 ; Hรธyre avslรฅtt SETT r14, 0x00 ; Vertikal avslรฅtt LES r0 ; 48 = Sequence start LES r0 ; Antall tegn i sequence LES r0 ; 48 = Sequence start LES r0 ; Antall tegn i sequence LES r0 ; 2 = Integer TUR hent_heltall SETT r1, r0 ; pos.x LES r0 ; 2 = Integer TUR hent_heltall SETT r2, r0 ; pos.y LES r0 ; 48 = Sequence start LES r0 ; Antall tegn i sequence LES r0 ; 2 = Integer TUR hent_heltall SETT r3, r0 ; 15=prevPos.x LES r0 ; 2 = Integer TUR hent_heltall SETT r4, r0 ; prevPos.y LES r0 ; 48 = Sequence start LES r0 ; Antall tegn i sequence LES r0 ; 48 = Sequence start LES r0 ; Antall tegn i sequence LES r0 ; 2 = Integer TUR hent_heltall SETT r5, r0 ; target.x LES r0 ; 2 = Integer TUR hent_heltall SETT r6, r0 ; target.y LES r0 ; 48 = Sequence start LES r0 ; Antall tegn i sequence LES r0 ; 2 = Integer TUR hent_heltall SETT r7, r0 ; target.x + target.w LES r0 ; 2 = Integer TUR hent_heltall SETT r8, r0 ; target.y + target.h sjekk_venstre: ME r1, r5 ; Hvis pos.x < target.x BHOPP slรฅ_pรฅ_venstre SEL r1, r3 ; Hvis pos.x >= prevPos.x (gรฅr mot hรธyre eller i ro) BHOPP sjekk_hรธyre SETT r10, r3 MINUS r10, r1 SETT r11, 0x02 ; Stor fart SEL r10, r11 ; Hvis stor fart mot venstre BHOPP slรฅ_pรฅ_venstre sjekk_hรธyre: SE r1, r7 ; Hvis pos.x > target.x + target.w BHOPP slรฅ_pรฅ_hรธyre MEL r1, r3 ; Hvis pos.x <= prevPos.x (gรฅr mot venstre eller i ro) BHOPP sjekk_vertikal SETT r10, r1 MINUS r10, r3 SETT r11, 0x02 ; Stor fart SEL r10, r11 ; Hvis stor fart mot hรธyre BHOPP slรฅ_pรฅ_hรธyre sjekk_vertikal: SETT r11, 0x05 MINUS r6, r11 ; Litt buffer SE r2, r6 ; Hvis pos.y > target.y BHOPP slรฅ_pรฅ_vertikal MEL r2, r4 ; Hvis pos.y < prevPos.y (kjรธrer oppover eller i ro) BHOPP skriv SETT r11, 0x01 ; Stor fart SETT r10, r2 MINUS r10, r4 SEL r10, r11 ; Hvis stor fart nedover BHOPP slรฅ_pรฅ_vertikal skriv: SETT r15, 0x30 ; Sequence start SKRIV r15 SETT r15, 0x09 ; 9 tegn SKRIV r15 SETT r15, 0x01 ; Type boolean SKRIV r15 SETT r15, 0x01 ; Ett tegn SKRIV r15 SKRIV r12 ; Venstre pรฅ/av SETT r15, 0x01 SKRIV r15 SETT r15, 0x01 SKRIV r15 SKRIV r13 ; Hรธyre SETT r15, 0x01 SKRIV r15 SETT r15, 0x01 SKRIV r15 SKRIV r14 ; Vertikal STOPP slรฅ_pรฅ_venstre: SETT r12, 0x01 HOPP sjekk_vertikal slรฅ_pรฅ_hรธyre: SETT r13, 0x01 HOPP sjekk_vertikal slรฅ_pรฅ_vertikal: SETT r14, 0x01 HOPP skriv hent_heltall: LES r0 ; Antall tegn SETT r10, 0x01 ; ULIK r0, r10 ; Ikke bare ett tegn BHOPP to_tall LES r0 ; (Egentlig 00-7f = positivt, 80-ff = negativt) RETUR to_tall: LES r0 ; 0 eller 1 (men vi fรฅr maks 255 inn, sรฅ alltid 0) LES r0 ; 00-ff RETUR
SLEDE8 in action:
When one had a working client side program it was sent to the server for testing. If successful one got the message
npst.no/temmelig-hemmelig/3545c4054b7fb20d387bbdd1f3d2aec8 back.
That page had the following message and flag:
Gratulerer, du reddet julen!
PST{MerryChristmasYaFilthyAlgorithm}
The flag is a reference to a scene in Home Alone 2.
And that's it! Christmas is saved, once again. ๐
I'm glad the scoreboard was kept open until January 5th. By that time 35 users had managed to get all 240 points, and 29 of those had all 11 eggs. There were 1481 users that successfully submitted at least 1 correct flag, though I suppose there's quite a few non-real users among them.
๐ It's been yet another great CTF by PST. It's really cool that they do these. I'm sure it's helpful for recruitement. And personally I'm glad more people learn more about anything IT security related.
โค๏ธ This year CTFd was been replaced by a beautiful retro Windows 95 like user interface. Except for missing solve time per challenge I loved this year's user interface.
๐โโ๏ธ I'm so happy the challenges were published at 7 a.m. instead of at midnight like last year. I didn't have many minutes to spare at 7 in the morning, but at least I didn't stay up too long and had the brain working overtime while it's supposed to sleep.
๐ต What I didn't enjoy was the number of challenges. 24 main challenges + 11 eggs. If you spend on average 1 hour on each you have spent 1 work week through those 24 days. That is just way too much if you have a job, school, exams or a family. The CTF experts of course solve every single challenge in less than 1 hour, but most of us aren't there. I would like to see the workload be cut in half.
๐ There were no days without a new challenge. I couldn't work on the calendar every day. That meant that by the end of the calendar I was several days behind. I was constantly chasing to get even, but I never made it. That was stressful. There should probably be 2 days without anything new every week. If the workload is this big next year I don't think I will prioritze to take part of it.
๐ฅ The concept with the Easter eggs is ok, but I didn't like how there were eggs which you had no idea where existed. It meant that the moment you were done with the challenges you had to go egg hunting - without any idea where to look. Especially egg #1 with humans.txt
was torture, and probably also egg #3 (cupcake.png
) for some people. It added what felt like more stress than fun.
๐ณ For me it also felt a bit strange that it was the last solve time - regardless of if it was a regular challenge or an egg - that decided your placement on the scoreboard. I would have thought the main challenges were more important than the eggs. Solving eggs wasn't something extra, it was absolutely necessary to be near the top.
๐ท I loved the assembly language SLEDE8 and its great tooling. However, while the e-learning was necessary to get us ready for bigger tasks there were too many algorithms to be implemented. If I want to do algorithms I will find another type of competition. And again, the eggs of all the algorithms didn't give a good feeling. You had finally created an algorithm that worked and got the flag, only to get a message back telling that it isn't good enough to get the egg.
๐ I hope PST will keep having the storyline with NPST and SPST. It's a nice touch that makes the CTF unique and playful.
๐ค Oh, and for those really competing for the first place I think it's important to never change the time of day of releases of challenges. Yes, I'm thinking about the final day that was released the night before. It was a good thing to do, but it should have been announced.
๐คฉ Anyways, except the workload, it's just all minor stuff, because I really love PST's CTFs. I'm looking forward to the next one! ๐
If you have thoughts about my solutions, the CTF, or if I have missed something cool; don't hesitate to comment here or ccontact me in some other way. ๐