Pretty fun challenges (those that I solved at least) :) I usually don’t even dare to touch anything ICS/SCADA related, but it turns out that these weren’t overly technical. We were also so close to solving the last Web challenge ‘scp-terminal’! We had all steps nailed down except the very last…
Details | Links |
---|---|
CTFtime.org Event Page | https://ctftime.org/event/1315 |
ICS
A Different Type of Serial Key
Attached are serial captures of two different uploads to an embedded device. One of these uploads is a key and the other is a function block. Your goal is to decode the serial traffic, extract the key and function block, and use these to find the flag. The flag will be in format flag{}.
Author: CISA
Attached: capture.sal, key.sal
To analyze serial captures, we can use Saleae Logic 2.
Extracting Function Block
capture.sal
has 4 Channels.
After reading other writeups online, I deduced this to be SPI communication where
- Channel 0 is the Clock because it is at a constant frequency
- Channel 1 is the Enable signal because it falls just before the Clock and Data starts and rises just after the Clock and Data stops; no other changes
- Channel 2 is empty
- Channel 3 is a Data signal because it has irregular signals that only occur within the intervals of the Channel 0 Clock, and aligns with the Clock’s edges
Since there is only one Data channel, it can be assigned to either MISO (Master In Slave Out) or MOSI (Master Out Slave In); it does not matter.
Knowing this, we can setup the SPI Analyzer with the correct Channels. In this case I chose MOSI for the single Data channel. The other settings can be left as default because it happens to be correct for this situation.
If we then turn on ASCII display for our SPI Analyzer and zoom out, we get a very nice looking string with no errors! The extracted ASCII characters are also shown in a table on the right, under the ‘Data’ section.
We can turn off all columns except mosi
since that’s where our ASCII data is contained, and then do an Export Table
either to CSV or to clipboard.
Clean up the data by removing the table headers and replacing instances of \n
with actual newline characters and you should get something like this!
S00C00004C6F63616C204B6579BF
S221020018423B165105BDAAFF27DB3B5D223497EA549FDC4D27330808F7F95D95B0EC
S5030001FBS0210000506F77657250432042696720456E6469616E2033322D42697420537475620E
S12304EC9421FFD093E1002C7C3F0B78907F000C909F000839200000913F001C4800012C7E
S123050C813F001C552907FE2F890000409E0058813F001C815F000C7D2A4A1489290000FF
S123052C7D2A07743D20100281090018813F001C7D284A14892900003929FFFD5529063EC7
S123054C7D2907747D494A787D280774813F001C815F00087D2A4A14550A063E9949000074
S123056C480000BC815F001C3D205555612955567D0A48967D49FE707D2940501D29000317
S123058C7D2950502F890000409E0058813F001C815F000C7D2A4A14892900007D2A077476
S12305AC3D20100281090018813F001C7D284A1489290000392900055529063E7D2907743F
S12305CC7D494A787D280774813F001C815F00087D2A4A14550A063E99490000480000408D
S12305EC813F001C815F000C7D2A4A14890900003D20100281490018813F001C7D2A4A145A
S123060C89490000813F001C80FF00087D274A147D0A5278554A063E99490000813F001CA1
S123062C39290001913F001C813F001C2F89001C409DFED0813F00083929001D3940000040
S11B064C9949000060000000397F003083EBFFFC7D615B784E80002060
S503000CF0
If we throw it into IDA Pro, we see that this is actually a Motorola S-record.
I wanted to know what processor the binary executable code in the S-record was for, so I googled the first few bytes and multiple results pointed to PowerPC as the answer.
We can now reload the file into IDA and select ‘PowerPC big-endian’ as the processor type! There’ll be a few prompts following that, but no worries. Select ppc
as the device name, cancel the dialog box for Loaded information type
, and pick 32-bit ISA for the instruction set architecture.
Create a function at the start of the disassembled code with ‘P’, and then we can decompile it nicely with ‘F5’.
The function block sub_4EC
loops through 29 bytes stored at 0x10020018
, XORs each byte with a byte from the argument key
, and then stores it in result
. We just need to know the key
and then we can calculate our own result
which is probably the flag or something!
Replicating the decryption pseudocode routine in Python, it looks like this:
|
|
Extracting Key
key.sal
has 2 visible Channels.
Similarly, with online writeups I reasoned that this could be I2C communication where
- Channel 0 is the Clock (SCL) because it has a consistent pulse
- Channel 1 is the Data (SDA) because it occurs irregularly within the intervals of the Clock signals
So just set up the I2C Analyzer with the correct channels,
and we will get a nice 29 bytes of Hex data parsed from the serial traffic. We can also see that these bytes are being read at an address 0x08
.
Extract the bytes the same way as we did for the function block by turning off irrelevant columns and exporting the table, and the result cleaned up is essentially the key that we need for our function block!
|
|
Final Decryption Script
|
|
Flag: flag{s3r14l_ch4ll3ng3_s0lv3r}
Tripping Breakers
Attached is a forensics capture of an HMI (human machine interface) containing scheduled tasks, registry hives, and user profile of an operator account. There is a scheduled task that executed in April 2021 that tripped various breakers by sending DNP3 messages. We would like your help clarifying some information. What was the IP address of the
substation_c
, and how many total breakers were tripped by this scheduled task? Flag format: flag{IP-Address:# of breakers}. For example ifsubstation_c
’s IP address was192.168.1.2
and there were 45 total breakers tripped, the flag would be flag{192.168.1.2:45}.Author: CISA
Attached: hmi_host_data.zip
In the .zip
file,
host\scheduled_tasks.csv
contains scheduled taskshost\Registry\SOFTWARE_ROOT.json
contains registry hiveshost\operator
contains the User-Profile folder of a Windows account
Powershell Scheduled Task
I first opened up scheduled_tasks.csv
, sorted by Last Run Time
and focused on tasks that were run in April 2021 (i.e. 4/1/2021).
In the sea of various system binaries, a Powershell entry stood out: wcr_flail.ps1
was run with -ExecutionPolicy Bypass
. Suspicious. Since it resided in the the %temp%
directory, we can retrieve it from the operator
User-Profile folder - specifically under operator\AppData\Local\Temp
.
After formatting the script nicely with newlines, this is what it looks like:
|
|
Decoding Second-Stage Payload
A payload is retrieved from Pastebin, deobfuscated, Base64-decoded and then used as a Path
to get an Item Property from.
We can debug the Powershell script in Windows Powershell ISE and evaluate expressions at runtime with ease to find out what is the Path
contained in the $SLPH
variable.
[DBG]: PS C:\Users\[redacted]>> $SLPH
HKLM:\SOFTWARE\Microsoft\Windows\TabletPC\Bell
It’s a registry path! Good thing we have the registry hives JSON file then. I opened up SOFTWARE_ROOT.json
in EmEditor since it’s a large file and used the TidyJSON Macro to format the JSON nicely.
Since the Powershell script tries to get the Data associated with Value Name Blast
at Key HKLM\SOFTWARE\Microsoft\Windows\TabletPC\Bell
, I searched for Blast
and retrieved the Data as M4RK_MY_W0Rd5
.
Repeating this same process with the $BRN
Path decoded from $TWR
, we get \\EOTW\\151.txt
stored at Value Name Off
at HKLM\SOFTWARE\Microsoft\Wbem\Tower
.
The Powershell script is then essentially simplified to this:
|
|
Referencing the manual page for openssl, we can understand that the SHA256 hash of M4RK_MY_W0Rd5
is used to generate the AES-256-CBC key to decrypt the Base64-encoded data in %temp%\EOTW\151.txt
. The decrypted data is then executed.
Since 151.txt
is in the %temp%
folder, we can retrieve it from the operator
User-Profile folder as well. After that, we can replicate the decryption easily to get the second-stage payload fate.exe
(just make sure you have openssl
):
|
|
Decompiling to Python
The decrypted fate.exe
has a PyInstaller file icon and is also detected as a PyInstaller executable by Exeinfo PE.
Thus, we can use PyInstaller Extractor to extract the Python files contained within the .exe
.
However, do note that it is important to run the extractor using the same Python version that was used to make the executable that we are trying to extract. But how would we know which version was used for the executable? PyInstaller Extractor will tell you in the first few lines of its output even if you run it with the wrong Python version:
|
|
In this case since the Python version detected in fate.exe
is 36
, I installed Python 3.6.8 and reran the extractor script.
|
|
trip_breakers.pyc
sounds like what we want! But because it still in compiled bytecode format (.pyc
), we will use uncompyle6 to decompile it back into normal Python source code.
|
|
Source Code Analysis
I shifted out the source code from the output above to here:
|
|
To find substation_c
’s IP address, we just need to look on line 64 where the Substation
object is instantiated with an IP address of 10.95.101.82
.
To find the total number of breakers tripped, we first need to understand what is the DNP3 TRIP Control Code.
In this user manual https://download.schneider-electric.com/files?p_Doc_Ref=SEPED305001EN, the Control Code is described as 8 bits of data with bits 7 and 6 being 10
for a TRIP code. Along with the Q and Cl bits being 0
for normal operation, and the Code being 0001
= 1 for Pulse On, the TRIP Control Code would then be 1000 0001
= 129, which matches OPT_4
in the Python code (initialised at line 6)!
Another manual online also corroborated my findings that the TRIP Control Code is OPT_4
= 129 (0x81).
Going back to the code, we see that OPT_4
is sent to Substations B, C, D, F, H, I, through the activate_all_breakers
function.
|
|
At first glance, activate_all_breakers
appears to loop through each device in each substation (line 5), and send the TRIP Control Code OPT_4
to each device multiple times specified by device['count']
(line 7).
|
|
However, looking at this paper https://arxiv.org/pdf/2102.11455.pdf made me realize that device['count']
actually represented the number of different devices. While device
determined the dst
to be used by get_dnp3_header
, device['count']
determined the index
used in get_dnp3_data
. And this index
actually identifies unique devices!
Thus, to calculate the number of breakers tripped, i.e. the number of devices that the TRIP Control Code was sent to, we add up all the values of device['count']
for Substations B, C, D, F, H, I.
Modifying the decompiled Python script:
|
|
|
|
Flag: flag{10.95.101.82:200}
Forensics
mic
My Epson InkJet printer is mysteriously printing blank pages. Is it trying to tell me something?
Attached: scan.pdf
scan.pdf
looks like 34 blank pages, but if you zoom in a good bit you’ll notice that the pages are covered with numerous yellow dots. This is actually Machine Identification Code (MIC), which explains the challenge’s title.
To decode these dots, the common solution is to first use pdftoppm to convert the PDF pages into image files, then use deda_parse_print
from deda to parse the image files for the identification information contained within. So if we do that and just test out deda_parse_print
on the first PDF page, we get the following output:
|
|
The value in serial
and printer
looks like an ASCII character code. In particular, the value for this first page represents f
and could be the first character of the flag. So all we need to do is probably just run deda_parse_print
on all the PDF pages and extract either the serial
or printer
value from the output.
However, I sought for a solution that did not require external dependencies and could all be done with just Python libraries in a single script for the entire process. This led me to use PyMuPDF for the PDF-to-image conversion, and libdeda
from deda
instead of the binaries.
PyMuPDF is imported as fitz
and is used to read the PDF and configure the DPI of the saved image using a Matrix
. It is also then important to pass this same DPI value to libdeda
’s PrintParser
, otherwise it will make an incorrect assumption of the image’s DPI and fail to detect the tracking dots. In my script below, I retrieved the value from printer
to form the flag string.
|
|
Flag: flag{watchoutforthepoisonedcoffee}
Thanks for stopping by :)