LABS

FIRST SecLounge CTF 2021 – Challenge Solutions

July 13, 2021

We competed at the FIRST SecLounge 2021 Virtual CTF (Capture the Flag) event, which was organized throughout the 7th – 9th of June. It is an annual event held by the FIRST organization, which brings together a network of teams working in computer security and incident response. During the competition, participants solved challenges in various categories, such as network, cryptography, reverse engineering, and programming.

Our team of four – Albert Zsigovits (Threat Researcher), Dorka Palotay (Senior Threat Researcher), Filip Savin (Senior Security Engineer), and Zoltan Balazs (Head of Vulnerability Research Lab) – took second place at the CTF event, solving 45 challenges in various categories.

The following write-up has some of our team’s favorite challenges and solutions, detailing how we solved them. Each description starts with the original assignment from the challenge in italics.

HMI Pwning 1 – 100 points

These challenges involve reverse engineering and exploiting a custom HMI program.

Can you determine the password that can be used to log in as the user “engineer”?

Note: hmi_coolant is a Linux binary that is safe to run on your local machine. Once running, you can communicate with your local instance of the HMI software via:
nc localhost 5050

Solution:

staplebatterycorrecthorse

If you open the program in a disassembler like Cutter or Ghidra, you will see a specific function called check_login. The account “engineer” can be accessed with a password that is hardcoded in plain-text. This password is simply compared with the password the user put in.

Decompiled code
check_login function decompiled

 

HMI Pwning 2 – 100 points

The HMI software sends CAN messages to various controllers (PLCs) in the building management network to open and close valves and check their current statuses.

We want to be able to send CAN “check status” messages from other applications.

Can you determine the CAN ID and data this HMI software sends to poll controllers for their status?

Note: Flag format is CAN-ID:Data

Example – the flag for a CAN message to ID 0xabc with data 0xde 0xad 0xbe 0xef would be abc:deadbeef.

Solution:

3d9:c4a064efb863

First, we enabled the virtual vcan0 network interface to capture CAN traffic.

  • ~$ sudo modprobe vcan
  • ~$ sudo ip link add dev vcan0 type vcan
  • ~$ sudo ip link set up vcan0
  • ~$ sudo wireshark

With Wireshark running, we could start capturing the traffic. Next, we needed to run the program and connect to it via TCP port 5050. We initiated the STATUS command and extracted the data seen over the wire from Wireshark or tshark.

tshark monitoring results
Connecting to the hmi_coolant program

 

HMI Pwning 3 – 250 points

We would like to determine what CAN message we need to send to close valve 109. We are aware that the current HMI software only allows us to open and close valves from 100-105. However, we believe the messages follow a pattern that we can use to determine how to interact with valve 109.

What CAN ID and data should we send to close valve 109?

Note: Flag format is CAN-ID:Data (for example, the flag for a CAN message to ID 0xabc with data 0xde 0xad 0xbe 0xef would be abc:deadbeef).

Solution:

185:c46d117459b81f

After we use the LOGIN command to log in with the “engineer” account, we see two other commands: OPEN_VALVE and CLOSE_VALVE. As confirmed by the disassembled code, there are 6 VALVES to control, 100 through 105. One way to solve this challenge is to iterate through the valves and try and see a pattern in the traffic to guess the traffic pattern for VALVE 109.

Captured traffic on the vcan0 interface
Captured traffic on the vcan0 interface

We could also patch the program locally to let through any VALVE number without throwing up an error. If you follow the disassembly, at 0x00002946, you will notice the user prompt for the valve number and there is a compare instruction at 0x00002987 to throw the user off if the VALVE number is less than 100 or more than 105. We can easily modify these hex bytes to be 109 (6d).

The compare instruction responsible for checking valve numbers at the FIRST 2021 CTF
The compare instruction responsible for checking valve numbers

 

HMI Pwning 4 – 500 points

We would like to send a factory reset command to the PLCs on the network to take them offline.

Unfortunately, this requires administrator access, and we believe there may be another way to login as the administrator without cracking the administrator password.

Once you have determined how to login as the administrator without the password, please send a reset command via the HMI we have running at ics_hmi.firstseclounge.org on TCP port 5050.

Note: Please wait to connect to our HMI until you have a working password bypass and have successfully logged in as the administrator on your local system.

Solution:

7224746d33

This challenge proved to be the most difficult in this track. We cannot simply read the password for the account “administrator”, since the given password is compared against an SHA-1 hash value, which is stored at admin_pass: 134818e10b19c39995249c100296dd09a776c2a7

HMI pwning admin_pass SHA-1
admin_pass SHA-1

Unfortunately, this SHA-1 value is not crackable within reasonable time, so we need to find a smarter workaround.

If we look at the decompiled code, we can see that after successful login by “administrator”, the return value is 0xc4.

decompiled code checking the administratorr password
check_login function checking the administrator password

disassembled code connection handler function
connection_handler function

The interesting part of the code is where the username and password are received. The program expects 12 bytes of username and 33 bytes of password. However, if a 33-byte password is received, then the last byte of the password will be placed to the location where the program checks if either the “engineer” or “administrator” could login successfully. Looking at the code above, we can see that a successful login by “engineer” is indicated by the 0xb8 byte, while value 0xc4 is for the “administrator”.

See the layout of this memory section below.

undefined8 local_1058; // 12 bytes of username read into this address
undefined8 local_1050;
undefined8 local_1048; // 33 bytes of password read into this address
undefined8 local_1040;
undefined8 local_1038;
undefined8 local_1030;
byte local_1028; // this we want to overwrite with 0xc4

The overflow is possible in line 103 where the password can overwrite local_1028. Furthermore, as we can see after line 107, local_1028 is not cleared or set if the password was incorrect. So, if we set local_1028 to 0xc4 by using a password where the 33rd byte is 0xc4, then the program will not break as per line 129, because the checked value will be 0xc4, which means that we can execute commands as the “administrator”.

Below you can see how the password overflows local_1028, when we debug the sample at runtime.

CTF HMI Pwning part 4

Python script CTF
Python script to get the flag

FIRST 2021 CTF getting the flag
Getting the flag for Part 4

 

HMI Pwning 5 – 250 points

There is a flag located in the same directory where the hmi software is run at ics_hmi.firstseclounge.org.

Can you execute remote commands on the system and find the flag?

Note: Please wait to try this on our system until after you have successfully sent commands on your local system.

Solution:

[email protected]

Moving on from the previous challenge we can simply issue a whoami|ls command to see what files are present on the remote server. As per the disassembled code, only 3 strings can be used as starting commands and a bunch of special characters are not allowed either, like ><`&!^@ [)(,;%\/_-].

CTF code showing denied strings
Denied starting strings as commands

CTF code showing specified denied characters
Denied characters specified

Since pipe is allowed, we can chain two commands together (a.k.a. command injection), but we also need to escape the whitespace somehow. This is where IFS comes into the picture.

p.sendline(b’whoami|cat${IFS}flag.txt’)

IFS is a special shell variable that is used for word splitting after expansion and to split lines into words.

Getting the flag with an internal field separator CTFGetting the flag via an internal field separator

You could also use <tab> as a valid whitespace.
 

The Golden DNS Part 1 – 250 points

mutaloitepammo.xyz

What is the key (mykey)?

Solution:

sFWxPdcc5rCKrSiGaXaVHnxs/D25+mbc7GxQOqpUvZ8=

We were certain that the key was hidden in a DNS record, so we enumerated all the records. Once the basic ideas lead nowhere, we came up with more complex plans, for example, enumerating TXT records from CHAOS class.

CHAOS allows responses to TXT queries in the CH class. It is useful for retrieving version or author information from the server by querying a TXT record for a special domain name in the CH class.

dig @ns1.mutaloitepammo.xyz CH TXT version.bind
version.bind.0CHTXT"mykey:sFWxPdcc5rCKrSiGaXaVHnxs/D25+mbc7GxQOqpUvZ8="

 

The Golden DNS Part 2 – 250 points

What is the algorithm used?

Solution:

hmac-sha384

We figured that an AXFR transfer had to be initiated for ns3.mutaloitepammo.xyz. The first algorithm we tried did not work (hmac-sha256), so we decided to try them all and hmac-sha384 worked at last.

AXFR is a type of DNS transaction. It is one of the many mechanisms available for administrators to replicate DNS databases across a set of DNS servers. It is also widely misused for enumerating subdomains from vulnerable DNS servers.

dig -6 @ns3.mutaloitepammo.xyz -y hmac-sha384:mykey:sFWxPdcc5rCKrSiGaXaVHnxs/D25+mbc7GxQOqpUvZ8= mutaloitepammo.xyz axfr +short

"UEsDBBQACQAIAAFfsFIIAvvcxgEAAA8DAAALABwAbWVzc2FnZS50eHRVVAkAA4EyoWCsMqFgdXgL"
"Pg0gUOF0FETqJ1BLBwgIAvvcxgEAAA8DAABQSwECHgMUAAkACAABX7BSCAL73MYBAAAPAwAACwAY"
"AAAAAAABAAAAtIEAAAAAbWVzc2FnZS50eHRVVAUAA4EyoWB1eAsAAQToAwAABOgDAABQSwUGAAAA"
"AAEAAQBRAAAAGwIAAAAA"
"AAEE6AMAAAToAwAAQxjd/Ae/AB1BTSt/tA2p4d5S6i6QklYZXxfZVvF3tJlLiRT8EEM2HJbNxdqf"
"1LhAuL6y6eAUzGt4oeALfg1unqmr9OdYvwZpWZjxld30VM6xSZ02sMBBCGESeX/s7uWFX3uiOw8x"
"85MRR/szaIjHMK6U0TGU/86/vWeDrshBOP4Q3cN0zCkmHjMO86AW+v834D75I6szJJ5gfXqV3wyf"
"ejo2IrWN2Xobr43dOtHrqlNlcGHXTjkV0LTjDA0JjHrQyCxR+Oe18eMkdgSbtbu2idjicbk2Fbku"
"eoBO5YZRXbXWuTkE0/KAu6GaeNg4bSfrfz269mz69QdWdeCU0I5XQfVBJs056vglQsxd4rh6+Pc2"
"N2/3VoSSzq6AGB92J2ht7p5+WML4Cxdoyr9cWRT2evoB392tDZ4Iq07mHViNvgdoX0IE41v1Vjyw"
"8xlRtCbl1Kv6h+PbF2sU4N9Qmseq3wam4+q5VQeQ+bfGApJkXRUDsGbM+l/xu54NrEK3XyQS7BFm"
"RTw1tCBqeNfhVurAMlv8Yftllku6M0p1EUZkHlAqWREFmhsszmXh6VGqqsSm0VzEh8fBdA5SeL1m"

 

The Golden DNS Part 3 – 500 points

What is the first hidden string?

Solution:

UEsDBBQACQAIAAFfsFIIAvvcxgEAAA8DAAALABwAbWVzc2FnZS50eHRVVAkAA4EyoWCsMqFgdXgL

We just had to grab the first Base64 string.

 

The Golden DNS Part 4 – 100 points

What is the flag-Z?

Solution:

im doing good

For this, we had to append our existing dig command with the +nsid parameter to get additional EDNS results.

EDNS allows for larger messages and also provides an extension mechanism for the protocol. It enables DNSSEC, DNS Cookies, EDNS Client Subnet as well as larger UDP packets. It also expands on the 4-bit RCODE field of un-extended DNS. The EDNS content is attached to a pseudo-record called OPT in the additional section of a message and an answer.

NSID (Name Server Identifier) is stored in the pseudo OPT record.

dig -6 @ns3.mutaloitepammo.xyz -y hmac-sha384:mykey:sFWxPdcc5rCKrSiGaXaVHnxs/D25+mbc7GxQOqpUvZ8= m.mutaloitepammo.xyz +all +besteffort +class +qr +nsid; <<>> DiG 9.10.6 <<>> -6 @ns3.mutaloitepammo.xyz -y hmac-sha384 m.mutaloitepammo.xyz +all +besteffort +class +qr +nsid

; EDNS: version: 0, flags:; udp: 1232
; NSID: 66 6c 61 67 2d 5a 3a 69 6d 20 64 6f 69 6e 67 20 67 6f 6f 64 ("flag-Z:im doing good")

 

The Golden DNS Part 5 – 500 points

What is the Golden Flag?

Solution:

SQaNXOxpIxwyfLO8Qoy3MFppu43KbZjc

Once we puzzled together the base64 strings into one big chunk and decoded it, we had a ZIP file at our hands. We could not open it at first, since it was password protected.

We used Zip2john to create a password hash and then tried to crack it with the default password.lst list file via john.

downloa.zip/message.txt:$pkzip$1*1*2*0*1c6*30f*dcfb0208*0*45*8*1c6*5f01*4318ddfc07bf001d414d2b7fb40da9e1de52ea2e909256195f17d956f177b4994b8914fc1043361c96cdc5da9fd4b840b8beb2e9e014cc6b78a1e00b7e0d6e9ea9abf4e758bf06695998f195ddf454ceb1499d36b0c041086112797feceee5855f7ba23b0f31f3931147fb336888c730ae94d13194ffcebfbd6783aec84138fe10ddc374cc29261e330ef3a016faff37e03ef923ab33249e607d7a95df0c9f7a3a3622b58dd97a1baf8ddd3ad1ebaa53657061d74e3915d0b4e30c0d098c7ad0c82c51f8e7b5f1e32476049bb5bbb689d8e271b93615b92e7a804ee586515db5d6b93904d3f280bba19a78d8386d27eb7f3dbaf66cfaf5075675e094d08e5741f54126cd39eaf82542cc5de2b87af8f736376ff7568492ceae80181f7627686dee9e7e58c2f80b1768cabf5c5914f67afa01dfddad0d9e08ab4ee61d588dbe07685f4204e35bf5563cb0f31951b426e5d4abfa87e3db176b14e0df509ac7aadf06a6e3eab9550790f9b7c60292645d1503b066ccfa5ff1bb9e0dac42b75f2412ec1166453c35b4206a78d7e156eac0325bfc61fb65964bba334a751146641e502a5911059a1b2cce65e1e951aaaac4a6d15cc487c7c1740e5278bd663e0d2050e1741444ea27*$/pkzip$:message.txt:download.zip::../../download.zip

Cracked password:

5oLd

This password opened the zip file and we got the final flag:

Abstract
The standard means within the Domain Name System protocol for
maintaining coherence among a zone’s authoritative name servers
consists of three mechanisms. Authoritative Transfer (AXFR) is one
of the mechanisms and is defined in RFC 1034 and RFC 1035.
The definition of AXFR has proven insufficient in detail, thereby
forcing implementations intended to be compliant to make assumptions,
impeding interoperability. Yet today we have a satisfactory set of
implementations that do interoperate. This document is a new
definition of AXFR — new in the sense that it records an accurate
definition of an interoperable AXFR mechanism.
THE GOLDEN FLAG IS: SQaNXOxpIxwyfLO8Qoy3MFppu43KbZjc

 

Man or Machine – 500 points

This one is simple. There’s a PCAP which contains 100 SSH connections. Only one from the connections was human driven. All we want to know is the source port number for that one connection. You ONLY have two attempts, so don’t brute force guess!

Attachment: 100-ssh.pcap

Solution:

54712

The basic idea is that humans interact via SSH slower than a machine. This is why time intervals between packets in human-driven SSH sessions will be a lot larger than in machine-driven sessions. First of all, we extracted the packet timings to a CSV file:

$ tshark -r ~/Downloads/100-ssh.pcap -Tfields -e frame.time_relative -e tcp.srcport -Y '(tcp.srcport != 22) and ssh' -Eseparator=, > ssh.csv

Now we had a CSV file with each packet time and source address. We wrote a simple script to gather average time intervals between packets in each session from the CSV file:

import csv
from collections import defaultdict
import statistics

lasts = {}
medians = defaultdict(list)

with open('ssh.csv', newline='') as csvfile:
    spamreader = csv.reader(csvfile, delimiter=',', quotechar='|')
    for row in spamreader:
        ts = float(row[0])
        port = int(row[1])
        if port in lasts:
            dif = ts - lasts[port]
            medians[port].append(dif)
        lasts[port] = ts

for port, vals in medians.items():
    print("src port %d: %d packets, %.4f avg, %.4f median" % (port, len(vals), sum(vals) / len(vals), statistics.median(vals)))

Running it reveals:

$ python3 ./t.py
...
src port 54706: 749 packets, 0.0729 avg, 0.0002 median
src port 54708: 750 packets, 0.0729 avg, 0.0002 median
src port 54710: 748 packets, 0.0725 avg, 0.0002 median
src port 54712: 119 packets, 0.4485 avg, 0.2319 median # !!! Big one
src port 54714: 746 packets, 0.0715 avg, 0.0001 median
src port 54716: 746 packets, 0.0709 avg, 0.0001 median
...

The flag is source port number 54712

 

Sudo su – 750 points

This is another easy one. The PCAP contains a single SSH session. The user authenticated with a public key. The user was then provided with a pseudo-terminal on the server. The user entered the “sudo su” command. The user then typed their password and successfully elevated to root. The user then pressed CTL+D twice, which exited first the root and then the user’s SSH session.

All we want to know is the length of the user’s password. It’s a number.

YOU ONLY GET 2 ATTEMPTS. DON’T WASTE THEM.

Attachment: sudo_su.pcap

Solution:

6

Simple experimenting showed that each character typed into the SSH session is transferred in an SSH-encrypted packet with a length of 36. Also, the last ^D generated an additional packet. So, in sudo_su.pcap, we annotated what was known from the challenge scenario and counted the password length:

CTF password length FIRST 2021

The flag is 6.

 

Hiding in the waves – 1 – 250 points

Can you analyze the signal and find the flag within? The flag will be within braces (e.g. {<flag>})

Attachment: hitw_ch1_434MHz_1MSps.wav

Solution:

Playing the WAV indeed sounds like an actual signal. We visualized it in Audacity:

WAV file signal visualization in Audacity

That burst at the beginning of the file looked suspicious – maybe it was just noise, but it could also be something embedded in there by using steganography. We tried all WAV steganography tools we had available, but those did not bring any results. So, the next thing we tried was decoding the signal itself. First of all, we needed tooling. An online search revealed software called rtl_433, which looked like what we needed. After reading the manual, we could do an initial analysis by passing the correct sampling rate and frequency that was present in the filename:

$ rtl_433 -f 434MHz -s 1M -r ./hitw_ch1_434MHz_1MSps.wav -A
rtl_433 version 21.05 branch at 202105091238 inputs file rtl_tcp RTL-SDR
Use -h for usage help and see https://triq.org/ for documentation.
Trying conf file at "rtl_433.conf"...
Trying conf file at "/Users/filipsavin/.config/rtl_433/rtl_433.conf"...
Trying conf file at "/usr/local/etc/rtl_433/rtl_433.conf"...
Trying conf file at "/etc/rtl_433/rtl_433.conf"...
Registered 157 out of 186 device decoding protocols [ 1-4 8 11-12 15-17 19-23 25-26 29-36 38-60 63 67-71 73-100 102-105 108-116 119 121 124-128 130-149 151-161 163-168 170-175 177-186 ]
Test mode active. Reading samples from file: ./hitw_ch1_434MHz_1MSps.wav
baseband_demod_FM: low pass filter for 1000000 Hz at cutoff 100000 Hz, 10.0 us
Detected FSK package @0.001025s
Analyzing pulses...
Total count: 270, width: 37.63 ms (37626 S)
Pulse width distribution:
[ 0] count: 1, width: 0 us [0;0] ( 0 S)
[ 1] count: 50, width: 25 us [21;31] ( 25 S)
[ 2] count: 51, width: 39 us [32;47] ( 39 S)
[ 3] count: 33, width: 13 us [11;16] ( 13 S)
[ 4] count: 17, width: 18 us [17;20] ( 18 S)
[ 5] count: 13, width: 10 us [10;11] ( 10 S)
[ 6] count: 20, width: 139 us [115;173] ( 139 S)
[ 7] count: 39, width: 73 us [61;90] ( 73 S)
[ 8] count: 13, width: 103 us [96;111] ( 103 S)
[ 9] count: 23, width: 53 us [49;60] ( 53 S)
[10] count: 8, width: 228 us [195;265] ( 228 S)
[11] count: 1, width: 175 us [175;175] ( 175 S)
[12] count: 1, width: 510 us [510;510] ( 510 S)
Gap width distribution:
[ 0] count: 36, width: 54 us [44;67] ( 54 S)
[ 1] count: 35, width: 118 us [96;146] ( 118 S)
[ 2] count: 55, width: 36 us [29;43] ( 36 S)
[ 3] count: 34, width: 22 us [18;27] ( 22 S)
[ 4] count: 1, width: 2156 us [2156;2156] (2156 S)
[ 5] count: 1, width: 1633 us [1633;1633] (1633 S)
[ 6] count: 1, width: 539 us [539;539] ( 539 S)
[ 7] count: 2, width: 769 us [746;793] ( 769 S)
[ 8] count: 35, width: 78 us [65;95] ( 78 S)
[ 9] count: 4, width: 310 us [282;353] ( 310 S)
[10] count: 23, width: 11 us [10;13] ( 11 S)
[11] count: 17, width: 180 us [150;218] ( 180 S)
[12] count: 22, width: 15 us [14;17] ( 15 S)
[13] count: 2, width: 28 us [28;28] ( 28 S)
[14] count: 1, width: 235 us [235;235] ( 235 S)
Pulse period distribution:
[ 0] count: 55, width: 56 us [45;68] ( 56 S)
[ 1] count: 69, width: 139 us [111;173] ( 139 S)
[ 2] count: 1, width: 2171 us [2171;2171] (2171 S)
[ 3] count: 1, width: 1646 us [1646;1646] (1646 S)
[ 4] count: 2, width: 537 us [521;554] ( 537 S)
[ 5] count: 2, width: 801 us [782;820] ( 801 S)
[ 6] count: 55, width: 90 us [75;111] ( 90 S)
[ 7] count: 21, width: 287 us [245;356] ( 287 S)
[ 8] count: 25, width: 203 us [177;241] ( 203 S)
[ 9] count: 10, width: 25 us [20;29] ( 25 S)
[10] count: 1, width: 375 us [375;375] ( 375 S)
[11] count: 17, width: 71 us [69;75] ( 71 S)
[12] count: 10, width: 38 us [34;44] ( 38 S)
Pulse timing distribution:
[ 0] count: 1, width: 0 us [0;0] ( 0 S)
[ 1] count: 89, width: 25 us [21;31] ( 25 S)
[ 2] count: 101, width: 39 us [32;48] ( 39 S)
[ 3] count: 68, width: 13 us [11;16] ( 13 S)
[ 4] count: 33, width: 18 us [17;20] ( 18 S)
[ 5] count: 17, width: 10 us [10;11] ( 10 S)
[ 6] count: 54, width: 132 us [108;173] ( 132 S)
[ 7] count: 84, width: 73 us [59;90] ( 73 S)
[ 8] count: 23, width: 101 us [92;111] ( 101 S)
[ 9] count: 38, width: 53 us [49;60] ( 53 S)
[10] count: 21, width: 207 us [175;265] ( 207 S)
[11] count: 2, width: 524 us [510;539] ( 524 S)
[12] count: 1, width: 2156 us [2156;2156] (2156 S)
[13] count: 1, width: 1633 us [1633;1633] (1633 S)
Level estimates [high, low]: 16311, 15068
RSSI: -0.0 dB SNR: 0.3 dB Noise: -0.4 dB
Frequency offsets [F1, F2]: 7190, -7875 (+109.7 kHz, -120.2 kHz)
Guessing modulation: No clue...

Detected OOK package @0.137640s
Analyzing pulses...
Total count: 280, width: 648.79 ms (648795 S)
Pulse width distribution:
[ 0] count: 1, width: 395 us [395;395] ( 395 S)
[ 1] count: 1, width: 22 us [22;22] ( 22 S)
[ 2] count: 157, width: 802 us [790;827] ( 802 S)
[ 3] count: 120, width: 1601 us [1590;1616] (1601 S)
[ 4] count: 1, width: 12337 us [12337;12337] (12337 S)
Gap width distribution:
[ 0] count: 1, width: 16 us [16;16] ( 16 S)
[ 1] count: 1, width: 42 us [42;42] ( 42 S)
[ 2] count: 121, width: 1599 us [1584;1697] (1599 S)
[ 3] count: 156, width: 797 us [775;810] ( 797 S)
Pulse period distribution:
[ 0] count: 1, width: 411 us [411;411] ( 411 S)
[ 1] count: 1, width: 64 us [64;64] ( 64 S)
[ 2] count: 107, width: 2399 us [2391;2423] (2399 S)
[ 3] count: 67, width: 3200 us [3193;3301] (3200 S)
[ 4] count: 103, width: 1600 us [1590;1610] (1600 S)
Pulse timing distribution:
[ 0] count: 1, width: 395 us [395;395] ( 395 S)
[ 1] count: 1, width: 22 us [22;22] ( 22 S)
[ 2] count: 313, width: 799 us [775;827] ( 799 S)
[ 3] count: 241, width: 1600 us [1584;1697] (1600 S)
[ 4] count: 1, width: 12337 us [12337;12337] (12337 S)
[ 5] count: 1, width: 16 us [16;16] ( 16 S)
[ 6] count: 1, width: 42 us [42;42] ( 42 S)
[ 7] count: 1, width: 100001 us [100001;100001] (100001 S)
Level estimates [high, low]: 16366, 14891
RSSI: -0.0 dB SNR: 0.4 dB Noise: -0.4 dB
Frequency offsets [F1, F2]: -1295, 0 (-19.8 kHz, +0.0 kHz)
Guessing modulation: No clue...
view at https://triq.org/pdv/#AAB0150801018B0016031F064030310010002A86A18596A355+AAB0130805018B0016031F064030310010002A86A1B355+AAB0140801018B0016031F064030310010002A86A1B2A355+AAB0150801018B0016031F064030310010002A86A1A2A2B355+AAB0150801018B0016031F064030310010002A86A1B2A2A355+AAB0140801018B0016031F064030310010002A86A1A2B355+AAB0160801018B0016031F064030310010002A86A1B2A2A2A355+AAB0150801018B0016031F064030310010002A86A1A2B2A355+AAB0130802018B0016031F064030310010002A86A1B355+AAB0140801018B0016031F064030310010002A86A1B2A355+AAB0160801018B0016031F064030310010002A86A1A2B2A2A355+AAB0170801018B0016031F064030310010002A86A1B2A2A2A2A355+AAB0130801018B0016031F064030310010002A86A1B355+AAB0160801018B0016031F064030310010002A86A1A2A2A2B355+AAB0140801018B0016031F064030310010002A86A1B2A355+AAB0170801018B0016031F064030310010002A86A1A2A2A2B2A355+AAB0130801018B0016031F064030310010002A86A1B355+AAB0160801018B0016031F064030310010002A86A1B2A2A2A355+AAB0140801018B0016031F064030310010002A86A1B2A355+AAB0140801018B0016031F064030310010002A86A1A2B355+AAB0140801018B0016031F064030310010002A86A1B2A355+AAB0130801018B0016031F064030310010002A86A1B355+AAB0160801018B0016031F064030310010002A86A1A2A2B2A355+AAB0140801018B0016031F064030310010002A86A1B2A355+AAB0150801018B0016031F064030310010002A86A1A2B2A355+AAB0150801018B0016031F064030310010002A86A1B2A2A355+AAB0130801018B0016031F064030310010002A86A1B355+AAB0140801018B0016031F064030310010002A86A1B2A355+AAB0140801018B0016031F064030310010002A86A1A2B355+AAB0150802018B0016031F064030310010002A86A1A2A2B355+AAB0170801018B0016031F064030310010002A86A1A2B2A2A2A355+AAB0130801018B0016031F064030310010002A86A1B355
Too many pulse groups (189 pulses missed in rfraw)

Analysis showed that it could be either an FSK or an OOK type modulation. There are many pulse and gap widths present with FSK, but we saw only two significant intervals with OOK – ~800 and ~1600. We thought this might be the 1 and 0 we were looking for. Next, we visited the recommended triq.org link from the end of the output and analyzed a part of the signal:

CTF audio signal analysis with triq

We saw the exact pulse/gap durations of ~800 and ~1600, but the analyzer still couldn’t guess the modulation. Luckily, the triq analyzer had an “Add Pulse Builder or Examples” button, where examples could be added. We added them one-by-one and spotted a visual similarity of the “THGR221 MC” example with the signal we had:

CTF pulse analysis with triq FIRST 2021

This example shows that it uses the same value 499.4 for short and long pulses, as for the gap – 999.7 (twice as long). It looked pretty much like ours: 800 and 1600. We applied the same slicer to our signal. Indeed, it looked pretty similar:

Signal similarity with THGR221 MC at FIRST CTF 2021

Now we could try to decode the signal with an OOK type MC modulation scheme. Our rtl_433 program supported the following modulations:

OOK_MC_ZEROBIT : Manchester Code with fixed leading zero bit

OOK_PCM : Pulse Code Modulation (RZ or NRZ)

OOK_PPM : Pulse Position Modulation

OOK_PWM : Pulse Width Modulation

OOK_DMC : Differential Manchester Code

OOK_PIWM_RAW : Raw Pulse Interval and Width Modulation

OOK_PIWM_DC : Differential Pulse Interval and Width Modulation

OOK_MC_OSV1 : Manchester Code for OSv1 devices

FSK_PCM : FSK Pulse Code Modulation

FSK_PWM : FSK Pulse Width Modulation

FSK_MC_ZEROBIT : Manchester Code with fixed leading zero bit

Candidates to try: OOK_MC_ZEROBIT and OOK_DMC. We tried the first one:

rtl_433 -f 434MHz -s 1M -r ./hitw_ch1_434MHz_1MSps.wav -X name=blah,s=800,l=800,g=1600,r=10000,m=OOK_MC_ZEROBIT
rtl_433 version 21.05 branch at 202105091238 inputs file rtl_tcp RTL-SDR
Use -h for usage help and see https://triq.org/ for documentation.
Trying conf file at "rtl_433.conf"...
Trying conf file at "/Users/filipsavin/.config/rtl_433/rtl_433.conf"...
Trying conf file at "/usr/local/etc/rtl_433/rtl_433.conf"...
Trying conf file at "/etc/rtl_433/rtl_433.conf"...
Registered 158 out of 186 device decoding protocols [ 1-4 8 11-12 15-17 19-23 25-26 29-36 38-60 63 67-71 73-100 102-105 108-116 119 121 124-128 130-149 151-161 163-168 170-175 177-186 ]
Test mode active. Reading samples from file: ./hitw_ch1_434MHz_1MSps.wav
baseband_demod_FM: low pass filter for 1000000 Hz at cutoff 100000 Hz, 10.0 us
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
time : @0.137640s
model : blah count : 1 num_rows : 1 rows :
len : 400 data : 55562e5e6acefa161af65a366eb2227a4a42266a36aedeea32521b4a12433ed32e1ac22b66ca6a0b2ea66a36cb3e36695555
codes : {400}55562e5e6acefa161af65a366eb2227a4a42266a36aedeea32521b4a12433ed32e1ac22b66ca6a0b2ea66a36cb3e36695555

We got 400 bits of data. We were not sure that the bitstream started at the intended byte boundary. Also, we were not sure we correctly decoded 0 and 1 and not vice versa. A little mangling in CyberChef showed that we had indeed mixed up 1 and 0, and shifted the data by 6 bits:

CyberChef helped decoding the CTF flag

The flag is: {o0K4yOu&Me}

 

Hiding in modbus 1 – 100 points

There is a modbus device located at ics_hmi.firstseclounge.org on TCP port 5020.

Can you find the ASCII string hidden in the discrete inputs?

Solution:

[email protected]

We found a nice command-line tool to access modbus devices: https://github.com/favalex/modbus-cli

Reading a discrete input is possible with the following command:

$ modbus ics_hmi.firstseclounge.org:5020 [email protected]<number>

This will return a bit corresponding to the address given by <number>.

We read the input bits:

$ for m in {0..100};do modbus ics_hmi.firstseclounge.org:5020 [email protected]$m;done

Converting those bits to ASCII revealed the flag: [email protected]

 

Hiding in modbus 2 – 250 points

On the same modbus device as the previous challenge, can you find the hidden flag within the input registers?

Solution:

Surprise_on_the_M0dBus

In the second part, we needed to find the flag within the input registers. We used the following command to read those registers:

$ modbus ics_hmi.firstseclounge.org:5020 [email protected]<number>

This returned two bytes corresponding to the address given by <number>.

Reading the first 100 addresses, we found the string “modbus_flag.jpg”, which, unfortunately, was not the flag. We assumed that the flag would be hidden in the image, so we started looking for a JPEG header (ff d8 ff) within the registers. Looking through all the available registers (more than 8000), we didn’t find a JPEG header.

Then, we decided to try another way: read and save all the data from the input registers. To do so, we used the following python module: https://pymodbus.readthedocs.io/en/v1.5.0/index.html

To read the input registers, we needed to know the slave_id parameter. Brute-forcing showed that slave 100 had some data. Also, there were a total of 8250 register addresses, and 125 addresses could be read at a time. Output for each register was 16bits. We hacked up a script that read all available registers and wrote the output to a file:

python script

Result file is aut.bin:

$ file ./aut.bin
./aut.bin: Zip archive data, at least v2.0 to extract

Unfortunately, the zip file threw up an error when unpacking. We tried binwalk and voila:

FIRST CTF modbus flag image with surprised Pikachu

Once we found the flag and looked back on our original efforts, we could see right away that the first register stored the values “PK”. We were looking so hard for a JPEG header that we didn’t realize that we have had the magic bytes of a ZIP! This is a typical CTF phenomenon: you focus so much on one idea, that you don’t see what is right in front of you.

While we were experimenting with different ideas to solve this challenge, we found QModMaster, a free Qt-based implementation of a ModBus master application. Using the graphical user interface, one could easily see the content of the input registers, which in this case started with “PK”.

QModMaster GUI

 

Acknowledgments

As a closing point, we wanted to express our gratitude towards the SecLounge team for making excellent and difficult challenges at the same time and thanks to FIRST.org for again organizing such a great competition. See you all next year!