Part 5. Hacking the network (Analysis of network protocols)

6 October 2023 39 minutes Author: Lady Liberty

Deciphering network traffic: In-depth protocol analysis

In an era of digital revolution and an ever-increasing number of Internet-connected devices, knowledge of network protocols is key for cybersecurity professionals. Our hacking site presents a detailed tutorial on network protocol analysis that will allow you to decipher, analyze and understand important aspects of network communication. By looking at popular protocols such as TCP, UDP, ICMP and others, we examine how data is transmitted over the network and what potential threats and vulnerabilities may be hidden in those transmissions. For a hacker, the ability to read and analyze network traffic is a vital skill that can reveal system weaknesses or potential targets for attack. Each protocol has its own unique structure and transmission mechanisms. Understanding these mechanisms is key to effective analysis.

For example, knowing how the TCP three-way handshake works will help you determine if a SYN flood attack is being attempted on your network. This part of Network Protocol Analysis is designed to provide you with specific tools, techniques, and knowledge to deeply investigate network traffic. From the basics to advanced techniques, you’ll learn about tools like Wireshark, tcpdump, and more to help you with your analysis. Don’t let network protocols be a mystery to you. With our help, you can become a real expert in this field, expanding your capabilities and skills in the field of cyber security. We invite you to explore the network world in depth with us!

Checking network protocols

Protocol analysis is important for tasks such as fingerprinting, information gathering, and even surgery. But in the IoT world, you often have to work with proprietary, custom, or new network protocols. These protocols can cause difficulties in

Even if you manage to capture network traffic, packet sniffers like Wireshark usually can’t make out what you’ve found. Sometimes you need to use new tools to communicate with your IoT device.

In this chapter, we will explain the process of analyzing network communications, focusing on the problems you will encounter when working with unusual protocols. Let’s start by learning how to assess the security of unfamiliar network protocols and implement special tools for their analysis. Next, we’ll extend the most popular traffic analyzer, Wireshark, by writing our own protocol analyzer. After that, we’ll write custom modules for Nmap that will scan fingerprints and even attack any new network protocol that dares to get in your way.

The examples in this section are not aimed at anything out of the ordinary, but DICOM is one of the most common protocols in medical devices and clinical systems. However, almost no security tools support DICOM, so this section will help you deal with any unusual network protocol you may encounter in the future.

When you work with unusual protocols, it is better to analyze them according to the methodology. Follow the procedure described in this section when evaluating the security of a network protocol. We try to cover the most important tasks, including information gathering, analysis, prototyping and security testing.

Collection of information

At the stage of gathering information, try to find all the relevant resources available to you. But first, find out if the protocol is well documented: look for its official and unofficial documentation.

Listing and installation of customers

After accessing the documentation, locate and install any client programs that may interact with the protocol. You can use them to replicate and generate traffic as you wish. Different clients may implement the protocol with slight differences – pay attention to these differences! Also, check if the programmers have written implementations of the protocol in different programming languages. The more customers and implementations you find, the more chances you have to gather complete information and reproduce network messages.

Detection of dependent protocols

Find out if the protocol really depends on other protocols. For example, the Server Message Block (SMB) protocol typically works with NetBios over TCP/IP (NBT). If you’re writing new tools, you need to know all the protocol dependencies in order to read and understand messages and create and send new ones. Be sure to find out what transport protocol your protocol uses. Is it TCP or UDP? Or maybe something else – SKTP?

Defining the protocol port

Determine the default port number for the protocol and determine whether the protocol can run on alternate ports. Defining a default port and being able to change that port is useful information that you will use when writing scanners or information gathering tools. For example, Nmap network exploration scripts may not work if we write the wrong rule, and Wireshark may use the wrong low-level parser/decoder (dissector). While there are workarounds for these issues, it’s best to have robust scanning rules in place from the start.

Find additional documentation

For more documentation or capture examples, visit the Wireshark website. The Wireshark project often includes packet capture and is generally a great source of information. The project uses a wiki (https://gitlab.com/wireshark/wireshark/-/ wikis/home/) that allows users to edit each page.

Also note which areas lack documentation. Can you identify features that are not described well enough? Lack of documentation can lead you to interesting discoveries.

Wireshark dissector tests

Check that all Wireshark dissectors work correctly with the protocol you are using. Can Wireshark correctly interpret and read all fields in protocol messages?

To do this, first check if Wireshark has an analyzer for the protocol and if it is enabled: click Analyze > Enabled Protocols.

If the protocol specifications are publicly available, ensure that all fields are correct. Dissectors are often prone to errors, especially when dealing with complex protocols. If you notice any errors, pay close attention to them. For more ideas, see the list of common vulnerabilities and vulnerabilities (CVEs) for which Wireshark dissectors exist.

Analysis

During the analysis phase, generate and reproduce traffic to understand how the protocol works. The goal is to gain a clear understanding of the overall structure of the protocol, including its transport layer, messages, and available operations.

Obtaining a copy of network traffic

Depending on the type of device, there are different ways of receiving network traffic that need to be analyzed. Some of them can support proxy configurations right out of the box! Determine whether you need to perform active or passive network traffic analysis. (You can find some examples of how to do this in James Forshaw’s book Attacking Protocol-Level Networks.) Try to generate traffic for every available use case, and generate as much as possible. Having different customers will help you understand the differences and nuances in existing implementations.

One of the first steps in the analysis phase should be to monitor the traffic and inspect the packets sent and received. There may be some obvious problems, so it is useful to do this before moving on to active analysis. The website https://gitlab.com/wireshark/wireshark/-/ wikis/SampleCaptures/  is a great resource for finding public captures.

Network traffic analysis using Wireshark

If Wireshark has a dissector that can analyze the traffic you generate, enable it by checking its name in the Enabled Protocols window. 5.2.

Now try to find the following:

  • The first bytes in the message. Sometimes the first few bytes in the initial connection or message are magic: they allow you to quickly identify a service.

  • Initial connection. This is an important feature of any protocol. Usually at this stage you will learn about the protocol version and supported features, including security features such as encryption. Repeating this step will also help you design scanners to easily find these devices and services on networks.

  • Any TCP/UDP streams and common data structures used in the protocol. Sometimes you will identify strings in the plaintext or common data structures, such as packets, whose length is appended to the beginning of the message.

  • Byte order in the protocol. Some protocols use mixed byte-passing order, which can cause problems if not detected at an early stage. The byte order varies greatly from protocol to protocol, but it is necessary to create correct packets.

  • Structure of messages. Define different message headers and structures, and how to initialize and close a connection.

Prototyping and tool development

After you’ve analyzed the protocol, you can start prototyping or turning the notes you’ve gathered from the analysis into real software that you can use to communicate with the service over the protocol. The prototype will allow you to make sure you understand the packet structure of each message type correctly. At this stage, it is important to choose a programming language that will allow you to work very quickly. For this reason, we prefer dynamically typed scripting languages such as Lua or Python. Check if there are libraries and frameworks available that can be used to speed up development.

If Wireshark does not support the protocol, develop a parser to help with the analysis. We’ll discuss this process below in the section Developing a Wireshark Dissector for the Lua DICOM Protocol. We will also use Lua to prototype the Nmap Scripting Engine to communicate with the service.

Conducting a security assessment

After you have completed your analysis, confirmed your assumptions about the protocol, and created a working prototype for communication using the DICOM service, you need to evaluate the security of the protocol.

In addition to the general security assessment process described in Chapter 3, consider the following key points.

  • Check for an attack involving server and client authentication. Ideally, the client and server should authenticate each other, a process known as mutual authentication. If not, you can impersonate the client or server. Such behavior can have serious consequences; For example, we once conducted a customer impersonation attack to spoof a drug library component and inject fraudulent drug libraries into an infusion pump. Even though the two endpoints exchanged data over Transport Layer Security (TLS),  This could not prevent the attack because there was no mutual authentication.

  • Deliberately miscoding the protocol and checking for flood attacks. Also, try to reproduce crashes and detect errors. Fuzzing is the process of automatically feeding malformed input data into the system with the ultimate goal of finding implementation errors. In most cases, this leads to a system crash. The more complex the protocol, the higher the chances of detecting memory corruption defects. DICOM (discussed below) is a great example. Given its complexity, buffer overflows and other security issues can be found in various implementations. In flood attacks, attackers send a large number of requests to the system to exhaust the system’s resources, causing the system to become unresponsive. A typical example is TCP SYN, a flood attack that can be mitigated with SYN cookies.

  • Check encryption and digital signature. Is the data confidential? Can we guarantee data integrity? How reliable are cryptographic algorithms? We’ve seen vendors implement their own encryption algorithms, always with disastrous results. In addition, many network protocols do not require a digital signature, which ensures message authentication, data integrity, and non-repudiation. For example, DICOM does not use a digital signature unless it uses a secure protocol such as Transport Layer Security (TLS), which is vulnerable to man-in-the-middle attacks.

  • Check the possibility of an attack on an outdated version. These are cryptographic attacks on the protocol that force the system to use a lower quality and less secure mode of operation (for example, one that sends data in the clear). Examples include Padding Oracle on Downgraded Legacy Encryption (POODLE) attacks at the TLS/SSL layer. In this attack, a man-in-the-middle attacker forces clients to use SSL 3.0 and exploits an inherent vulnerability in the protocol to steal cookies or passwords.

  • Check for a reservation attack. These attacks are triggered when the protocol has features that far exceed the request, as attackers can abuse these features to cause a denial of service. An example is a DDoS attack on mDNS mapping where some mDNS implementations responded to unicast requests from sources outside the local network. We’ll cover mDNS in Chapter 6.

Development of Wireshark dissector for DICOM protocol in Lua

This section shows how to write a dissector that can be used with Wireshark. When testing network protocols used by IoT devices, it is critical to understand how data is transmitted, how messages are generated, and whether features, operations, and security mechanisms are involved. We can then start modifying the data streams to find vulnerabilities. We will use Lua to write our dissector; This will allow for quick analysis of intercepted network communications with a small amount of code. We’ll go from presenting an array of information to readable messages with just a few lines of code.

In this exercise, we will focus on only a subset of the functions required to process DICOM Type A messages (discussed in the next section). Another thing to keep in mind when writing Wireshark dissectors for TCP in Lua is that packets can be fragmented. Also, depending on factors such as packet retransmission, Wireshark failure, or configuration errors, by limiting the size of captured packets (the default capture packet size limit is 262,144 bytes), we may receive less or more than one message per TCP segment. Let’s ignore this for now and focus on the A-ASSOCIATE queries, which will be sufficient to identify the DICOM services when writing the scanner. If you want to learn more about how to combat TCP fragmentation, see the sample orthanc.lua file in this book or go to https:// nostarch.com/practical-iot-hacking/.

Working with Lua

Lua is a scripting language for creating extensible or scriptable modules in many important security projects such as Nmap, Wireshark, and even commercial security products such as LogRhythm’s NetMon. Some of the products you use every day are likely powered by Lua. Many IoT devices also use Lua because of its small binary size and well-documented API, making it easy to use to extend projects in other languages such as C, C++, Erlang, and even Java. This makes Lua ideal for embedding into applications. You’ll learn how to represent and work with data in Lua, and how popular applications such as Wireshark and Nmap use Lua to enhance traffic analysis, network exploration, and exploit capabilities.

Overview of the DICOM protocol

DICOM is a non-proprietary protocol developed by the American College of Radiology and the National Electrical Manufacturers Association. It has become the international standard for the transmission, storage and processing of information about medical images. Although not proprietary, DICOM is a good example of a network protocol implemented in many medical devices; Traditional network security tools do not support it very well. DICOM communication over TCP/IP is two-way: the client requests an action, and the server performs it, but if necessary, they can be swapped. In DICOM terminology, the client is a Service Call User (SCU), and the server is a Service Call Provider (SCP). Before we start writing the code, let’s review some important DICOM messages and protocol structure.

C-ECHO message

DICOM C-ECHO messages serve, among other things, to exchange information about calling and calling programs, objects, versions, unique identifiers (UIDs), names, and roles. We commonly refer to these messages as DICOM Queries because they allow us to determine if a DICOM service provider is on the network. The C-ECHO message uses several type A messages, so we will cover them in this section. The first packet sent by the C-ECHO operation is an A-ASSOCIATE request, which is sufficient to identify the DICOM service provider. Information about the service can be obtained from the A-ASSOCIATE response.

Protocol Data Units (PDUs)

There are seven types of Type A messages used in C-ECHO messages:

  • A-ASSOCIATE request (A-ASSOCIATE-RQ): requests sent by the client to establish a DICOM connection;

  • A-ASSOCIATE accept (A-ASSOCIATE-AC): responses sent by the server to accept the DICOM A-ASSOCIATE request;

  • A-ASSOCIATE rejection (A-ASSOCIATE-RJ): responses sent by the server to reject a DICOM A-ASSOCIATE request;

  • (P-DATA-TF): data packets sent by the server and the client;

  • A-RELEASE request (A-RELEASE-RQ): a request sent by a client to close a DICOM connection;

  • A-RELEASE response (PDU A-RELEASE-RP): response sent by the server to confirm the A-RELEASE request;

  • A-ASSOCIATE Abort (A-ABORT PDU): Responses sent by the server to abort the A-ASSOCIATE operation.

All of these PDUs begin with a similar packet structure. The first part is a single-byte unsigned integer in Big Endian format that indicates the PDU type. The second part is a one-byte reserved section set at 0x0. The third part is the PDU length information, a four-byte unsigned integer in Little Endian format. The fourth part is a variable-length data field. This structure is shown in fig. 5.3.

Once we know the structure of the message, we can start reading and analyzing the DICOM message. Using the size of each field, we can calculate the offset when defining the fields in our prototypes for analysis and interaction with DICOM services.

DICOM traffic generation

To complete this exercise, you need to set up a DICOM server and client. Orthanc is a robust open source DICOM server that runs on Windows, Linux, and macOS. Install it on your system, make sure the DicomServerEnabled flag is enabled in the configuration file, and run the Orthanc binary. If everything works correctly, you should have a DICOM server running on TCP port 4242 (the default port). Enter the orthanc command to view the following logs that describe the configuration options:

$ ./Orthanc
<timestamp> main.cpp:1305] Orthanc version: 1.4.2
<timestamp> OrthancInitialization.cpp:216] Using the default Orthanc
configuration
<timestamp> OrthancInitialization.cpp:1050] SQLite index directory: "XXX"
<timestamp> OrthancInitialization.cpp:1120] Storage directory: "XXX"
<timestamp> HttpClient.cpp:739] HTTPS will use the CA certificates from this
file: ./orthancAndPluginsOSX.stable
<timestamp> LuaContext.cpp:103] Lua says: Lua toolbox installed
<timestamp> LuaContext.cpp:103] Lua says: Lua toolbox installed
<timestamp> ServerContext.cpp:299] Disk compression is disabled
<timestamp> ServerIndex.cpp:1449] No limit on the number of stored patients
<timestamp> ServerIndex.cpp:1466] No limit on the size of the storage area
<timestamp> ServerContext.cpp:164] Reloading the jobs from the last execution
of Orthanc
<timestamp> JobsEngine.cpp:281] The jobs engine has started with 2 threads
<timestamp> main.cpp:848] DICOM server listening with AET ORTHANC on port:
4242
<timestamp> MongooseServer.cpp:1088] HTTP compression is enabled
<timestamp> MongooseServer.cpp:1002] HTTP server listening on port: 8042
(HTTPS encryption is disabled, remote access is not allowed)
<timestamp> main.cpp:667] Orthanc has started

If you do not want to install an Orthanc server, you can find sample captured packets in the online resources for this book or on the Wireshark page for sample packets for DICOM.

Enabling Lua in Wireshark

Before jumping into the code, make sure you have Lua installed and enabled in Wireshark’s settings. You can check if it is available in the About Wireshark window – see 5.4.

The Lua engine is disabled by default. To enable it, set the disable_lua boolean variable to false in the init.lua file from the Wireshark installation directory:

disable_lua = false

After you have verified that Lua is available and enabled, verify that Lua support is working correctly by writing a test script and running it as follows:

$ tshark -X lua_script: <ваш тестовый сценарий Lua>

If we include a simple print statement (for example, the line print “Hello from Lua”) in the test file, we should see the output before the capture starts.

$ tshark -X lua_script:test.lua
Hello from Lua
Capturing on 'ens33'

On Windows, the output may not be displayed if a normal print operator is used. But the report_failure() function will open a window with your message, so you can do without the print statement.

Definition of a dissector

Let’s define our new protocol dissector using the Proto(name, description) function. As mentioned earlier, it specifically identifies a DICOM Type A message (one of the seven messages listed earlier):

dicom_protocol = Proto("dicom-a", "DICOM A-Type message")

Next, we define the header fields in Wireshark to match the DICOM PDU structure discussed earlier using the ProtoField class:

pdu_type = ProtoField.uint8("dicom-a.pdu_type","pduType",
base.DEC, {[1]="ASSOC Request",
 [2]="ASSOC Accept",
 [3]="ASSOC Reject",
 [4]="Data",
 [5]="RELEASE Request",
 [6]="RELEASE Response",
 [7]="ABORT"}) -- unsigned 8-bit integer
 message_length = ProtoField.uint16("dicom-a.message_length", "messageLength",
base.DEC) -- unsigned 16-bit integer
 dicom_protocol.fields = {pdu_type, message_length}

We use these ProtoFields to add items to the parse tree. For our parser, let’s call ProtoField twice: once to create a one-byte unsigned integer to store the PDU type, and a second time for two bytes to store the message length. Notice how we have assigned a table of values to the PDU types. Wireshark will automatically display this information. We then represent the fields of our protocol parser as a Lua table containing our ProtoFields.

Definition of the main function of the dissector

Next, we declare our main dissector function, Dissector(), which takes three arguments: a buffer for Wireshark parsing, packet information, and a tree that displays protocol information.

In this Dissector() function, we will parse our protocol and add the ProtoFields we defined earlier to the tree containing our protocol information.

function dicom_protocol.dissector(buffer, pinfo, tree)
 pinfo.cols.protocol = dicom_protocol.name
 local subtree = tree:add(dicom_protocol, buffer(), "DICOM PDU")
 subtree:add_le(pdu_type, buffer(0,1)) -- big endian
 subtree:add(message_length, buffer(2,4)) -- skip 1 byte
end

We set the protocol field to the protocol name we defined in dicom_protocol.name. For each element to be added, we use either add_le() for Big Endian format or add() for Little Endian format, as well as a ProtoField and a buffer range to parse.

Completion of the dissector

DissectorTable contains the sub-dissector table for the protocol, which is displayed through the Wireshark decoding dialog.

local tcp_port = DissectorTable.get("tcp.port")
tcp_port:add(4242, dicom_protocol)

To complete the dissector, simply add it to the DissectorTable for TCP ports on port 4242.

Code listing 5.1. DICOM type A dissector completed

 dicom_protocol = Proto("dicom-a", "DICOM A-Type message")
 pdu_type = ProtoField.uint8("dicom-a.pdu_type", "pduType", base.DEC, {[1]="ASSOC 
Request",
 [2]="ASSOC Accept", [3]="ASSOC Reject", [4]="Data", [5]="RELEASE Request", [6]="RELEASE
 Response", [7]="ABORT"})
 message_length = ProtoField.uint16("dicom-a.message_length", "messageLength", base.DEC)
 dicom_protocol.fields = {message_length, pdu_type} 
 function dicom_protocol.dissector(buffer, pinfo, tree)
 pinfo.cols.protocol = dicom_protocol.name
 local subtree = tree:add(dicom_protocol, buffer(), "DICOM PDU")
 subtree:add_le(pdu_type, buffer(0,1))
 subtree:add(message_length, buffer(2,4))
 end
 local tcp_port = DissectorTable.get("tcp.port")
 tcp_port:add(4242, dicom_protocol)

We enable this dissector by placing the Lua file in the Wireshark plugin directory and then restarting Wireshark. Then, when parsing the DICOM capture, we should see the pduType byte and  message length displayed in the DICOM PDU column we defined in our tree:add() call. In fig. Figure 5.5 shows how this looks in Wireshark. Dicom-a filters can also be used. message_length and dicom-a.pdu_type which we defined for traffic filtering.

You can now clearly define the PDU type and message length in DICOM packets.

Creating a C-ECHO dissector

Analyzing the C-ECHO request with our new parser, we should see that it consists of various type A messages, as shown in Fig. 5.5. The next step is to analyze the data contained in these DICOM packages.

To show how we can process strings in our Lua dissector, let’s add code to the dissector to parse the A-ASSOCIATE message. Fig. Figure 5.6 shows the structure of the A-ASSOCIATE query.

Note the 16-byte headers of calling and calling programs. The application object header is a label that identifies the service provider. The message also contains a 32-byte reserved section that must be zero-padded, as well as variable-length elements, including Application Context, Presentation Context, and User Information elements.

Getting the string values of the application object’s header

Let’s start by extracting the fixed-length message fields, including the subscriber string values and the entity name program called. This is useful information; often the services do not have authentication, so if you have the correct application object header you can connect and start issuing DICOM commands. We can define new ProtoField objects for our A-ASSOCIATE request message with the following code:

protocol_version = ProtoField.uint8("dicom-a.protocol_version",
"protocolVersion", base.DEC)
calling_application = ProtoField.string( "dicom-a.calling_app", 
"callingApplication")
called_application = ProtoField.string("dicom-a.called_app",
"calledApplication")

We use the ProtoField ProtoField function to extract the string values of the names of called and calling applications. line. We pass it a name that will be used in filters, an optional name that will be displayed in the v tree, a display format (base. ASCII or base. UNICODE) and an optional description field. Initial loading of dissector function data

After adding the new ProtoFields as fields to the protocol dissector, we need to add code to load them into the dissector function, dicom_protocol.dissector(), so that they are included in the protocol mapping tree:

local pdu_id = buffer(0, 1):uint() -- Convert to unsigned int
if pdu_id == 1 or pdu_id == 2 then -- ASSOC-REQ (1) / ASSOC-RESP (2)
 local assoc_tree = subtree:add(dicom_protocol, buffer(), "ASSOCIATE REQ/
RSP")
 assoc_tree:add(protocol_version, buffer(6, 2))
 assoc_tree:add(calling_application, buffer(10, 16))
 assoc_tree:add(called_application, buffer(26, 16))
end

The dissector must add the extracted fields to a  subtree in the protocol tree. To create a subtree, call the add()  function from our existing protocol tree. Our simple parser can now determine PDU types, message length, ASSOCIATE message type, protocol, call, and called applications.

Analysis of fields of variable length

Now that we have defined and analyzed fixed-length sections, let’s analyze variable-length message fields. In DICOM, we use identifiers called contexts to store, represent, and share various characteristics. We’ll show you how to find the three different types of contexts available: application context, presentation context, and user information context, which have varying numbers of field elements. But we We will not write the code to parse the content of the element.

For each of the contexts, add a subtree that displays the length of the context and a variable number of context elements. Change the main protocol dissector to look like this:

function dicom_protocol.dissector(buffer, pinfo, tree)
 pinfo.cols.protocol = dicom_protocol.name
 local subtree = tree:add(dicom_protocol, buffer(), "DICOM PDU")
 local pkt_len = buffer(2, 4):uint()
 local pdu_id = buffer(0, 1):uint()
 subtree:add_le(pdu_type, buffer(0,1))
 subtree:add(message_length, buffer(2,4))
 if pdu_id == 1 or pdu_id == 2 then -- ASSOC-REQ (1) / ASSOC-RESP (2)
 local assoc_tree = subtree:add(dicom_protocol, buffer(), "ASSOCIATE REQ/RSP")
 assoc_tree:add(protocol_version, buffer(6, 2))
 assoc_tree:add(calling_application, buffer(10, 16))
 assoc_tree:add(called_application, buffer(26, 16))
 --Extract Application Context 
 local context_variables_length = buffer(76,2):uint() 
 local app_context_tree = assoc_tree:add(dicom_protocol, buffer(74, context_variables_
length + 4), "Application Context") 
 app_context_tree:add(app_context_type, buffer(74, 1))
 app_context_tree:add(app_context_length, buffer(76, 2))
 app_context_tree:add(app_context_name, buffer(78, context_variables_length))
 --Extract Presentation Context(s) 
 local presentation_items_length = buffer(78 + context_variables_length + 2, 2):uint()
 local presentation_context_tree = assoc_tree:add(dicom_protocol, buffer(78 + context_
 variables_length, presentation_items_length + 4), "Presentation Context")
 presentation_context_tree:add(presentation_context_type, buffer(78 + context_variables_
length, 1))
 presentation_context_tree:add(presentation_context_length, buffer(78 + context_
variables_length + 2, 2))
 -- TODO: Extract Presentation Context Items
 --Extract User Info Context 
 local user_info_length = buffer(78 + context_variables_length + 2 + presentation_items_
 length + 2 + 2, 2):uint()
 local userinfo_context_tree = assoc_tree:add(dicom_protocol, buffer(78 + context_
variables_length + presentation_items_length + 4, user_info_length + 4), "User Info 
Context")
 userinfo_context_tree:add(userinfo_length, buffer(78 + context_variables_length + 2 +
presentation_items_length + 2 + 2, 2))
 -- TODO: Extract User Info Context Items
 end
end

When working with network protocols, there are often fields of variable length that require the calculation of offsets. It is very important that you get the length values correct because all offset calculations depend on them.

Testing the dissector

After making the changes mentioned in the previous section, verify that your DICOM packets are analyzed correctly by checking the resulting length. You should now see a subtree for each context (Figure 5.8). Note that since we provide a range of buffers in the new subtrees, you can select them to select the appropriate partition. Take the time to make sure that the context of each DICOM protocol is recognized as you expect.

If you want to practice, we recommend adding fields from different contexts to the dissector. You can download the DICOM package from the sample Wireshark package page, where we have posted the package containing the DICOM echo request. You will also find a complete example, including TCP fragmentation, in the online resources of this book. Remember that you can reload your Lua scripts at any time to test your latest dissector without restarting Wireshark by clicking  Analyze > Restart Lua>s Plugin.

Development of the DICOM Services scanner for the Nmap script engine

Earlier in this chapter, you learned that DICOM has a ping-like utility called C-Echo-query, which consists of several Type A messages. Next, you created a Lua dissector to analyze these messages with Wireshark. Next, you’ll use Lua to solve another problem: writing a DICOM Services scanner. The scanner remotely identifies DICOM service providers (DSPs) on networks to proactively test their configurations and even launch attacks. Since Nmap is well known for its scanning capabilities, and its scripting engine also runs on Lua, it is the perfect tool for writing such a scanner.

In this exercise, we will focus on a subset of the functions involved in sending a partial C-ECHO request.

Writing an Nmap scripting library for DICOM

Let’s start by building the Nmap Scripting Engine library for our DICOM-related code. We will use the library to store any functions used when creating and deleting sockets, sending and receiving DICOM packets, and operations such as binding and requesting services.

Nmap already includes libraries that help with standard I/O, sockets, and other tasks. Take a moment to browse the library’s collection to see what’s already available. See the documentation for these scripts and libraries at https://nmap.org/nsedoc/.

Typically, the Nmap Scripting Engine libraries can be found in the <installation>/nselib/ directory. Find  this directory and create a file called dicom.lua. In this file, start by declaring other Lua and Nmap Scripting Engine standard libraries to use. Also give the environment a name for the new library:

local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local nsedebug = require "nsedebug"
_ENV = stdnse.module("dicom", stdnse.seeall)

In this case, we will use four different libraries: two Nmap Scripting Engine libraries (nmap and stdnse) and two standard Lua libraries (string and table). Lua’s string and table libraries are, surprisingly, designed for string and table operations. We will mainly use the nmap library’s socket handling, as well as stdnse to read user arguments and print debug statements when needed. In addition, we will use the useful nsedebug library, which displays various types of data in an easy-to-read form.

DICOM codes and constants

Now let’s define some constants for storing PDU codes, UUID values, as well as the minimum and maximum allowable packet sizes. This will allow you to write code that is more readable and easier to maintain. In Lua, we usually define constants with capital letters:

local MIN_SIZE_ASSOC_REQ = 68 -- Минимальный размер запроса ASSOCIATE 
local MAX_SIZE_PDU = 128000 -- Максимальный размер любого PDU
local MIN_HEADER_LEN = 6 -- Минимальная длина заголовка DICOM
local PDU_NAMES = {}
local PDU_CODES = {}
local UID_VALUES = {}
-- Таблица имен PDU для кодов 
PDU_CODES =
{
 ASSOCIATE_REQUEST = 0x01,
 ASSOCIATE_ACCEPT = 0x02,
 ASSOCIATE_REJECT = 0x03,
 DATA = 0x04,
 RELEASE_REQUEST = 0x05,
 RELEASE_RESPONSE = 0x06,
 ABORT = 0x07
}
-- Таблица имен UID для значений
UID_VALUES =
{
 VERIFICATION_SOP = "1.2.840.10008.1.1", -- Проверочный класс SOP
 APPLICATION_CONTEXT = "1.2.840.10008.3.1.1.1", -- Имя контекста приложения DICOM
 IMPLICIT_VR = "1.2.840.10008.1.2", -- Подразумевает VR Little Endian: Синтаксис по 
умолчанию для DICOM
 FIND_QUERY = "1.2.840.10008.5.1.4.1.2.2.1" -- Корень иноформационной модели запрос/ответ -
FIND
}
-- Мы сохраняем имена, используя их коды и ключи для вывода на печать имен типов PDU
for i, v in pairs(PDU_CODES) do
 PDU_NAMES[v] = i
end

Constant values for common DICOM operation codes are defined here. We also defined tables to represent different classes of data using DICOM specific UIDs and packet lengths. Now we are ready to start communicating with the service.

Write socket creation and destruction functions

We will use the nmap scripting engine library to send and receive data. Since creating and destroying sockets is a routine operation, it is recommended to write functions for them in our new library. Let’s write our first function dicom.start_connection(), which creates a socket for the DICOM service:

---
-- start_connection(host, port) открывает сокет для службы DICOM
--
-- @param host Host object
-- @param port Port table
-- @return (status, socket) Если status равен true, сокет возвращает объект 
DICOM
-- Если status равен false, сокет возвращает сообщение 
об ошибке.
---
function start_connection(host, port)
 local dcm = {}
 local status, err
Анализ сетевых протоколов  141
dcm['socket'] = nmap.new_socket()
 status, err = dcm['socket']:connect(host, port, "tcp")
 if(status == false) then
 return false, "DICOM: невозможно подключиться к службе: " .. err
 end
 return true, dcm
end

Definition of functions for sending and receiving DICOM packets

In the same way, we create functions for sending and receiving DICOM packets:

-- send(dcm, data) Отправляет пакет DICOM через открытый сокет
--
-- @param dcm объект DICOM
-- @param data данные для отправки
-- @return status равен True если данные отправлены корректно, и False в случае 
ошибки
function send(dcm, data)
 local status, err
 stdnse.debug2("DICOM: Sending DICOM packet (%d bytes)", #data)
 if dcm["socket"] ~= nil then
 status, err = dcm["socket"]:send(data)
 if status == false then
 return false, err
 end
 else
 return false, "No socket available"
 end
 return true
end
-- receive(dcm) Чтение пакетов DICOM через открытый сокет
--
-- @param dcm DICOM object
-- @return (status, data) возвращает данные, если status равен true, иначе 
возвращает сообщение об ошибке.
function receive(dcm)
local status, data = dcm["socket"]:receive()
 if status == false then
 return false, data
 end
 stdnse.debug2("DICOM: receive() read %d bytes", #data)
 return true, data
end

The send(dcm, data) and receive(dcm) functions use the Nmap socket functions send() and receive() respectively. They refer to the connection handle stored in the dcm[“socket”] variable to read and write DICOM u packets over the socket. Note stdnse.debug[1-9], which is used to print diagnostic statements when Nmap runs the debug (-d) flag. In this case, using stdnse.debug2() will print when the debug level is set to 2 or higher.

Creation of DICOM packet headers

Now that we’ve set up the basic network I/O, let’s create the functions that are responsible for generating DICOM messages. As mentioned earlier, a DICOM PDU uses a header to define its type and length. In the Nmap scripting engine, we use strings to store byte streams and the string.pack() and string.unpack() functions to encode and retrieve information in different formats and byte orders. The way string.pack() and string.unpack() are used, you will need to familiarize yourself with Lua format strings, as you will need to represent data in different formats. You can read about them at https://www.lua. org/manual/5.3/manual.html#6.4.2. Take the time to learn how to record endiante order and common conversions.

---
-- pdu_header_encode(pdu_type, length) кодирует заголовок PDU DICOM
--
-- @param pdu_type тип PDU – беззнаковое целое
-- @param length длина сообщения DICOM
-- @return (status, dcm) если status равен true, возвращает заголовок.
-- если status равен false, dcm содержит сообщение об ошибке
---
function pdu_header_encode(pdu_type, length)
 -- несколько простых проверок; мы не проверяем диапазоны, чтобы позволить пользователю 
создавать некорректные пакеты.
 if not(type(pdu_type)) == "number" then 
 return false, "PDU должен быть беззнаковым целым. Диапазон:0-7"
Анализ сетевых протоколов  143
 end
 if not(type(length)) == "number" then
 return false, "Длина должна быть беззнаковым целым."
 end
 local header = string.pack("<B >B I4",
 pdu_type, -- тип PDU ( 1 байт – беззнаковое целое в формате Big 
Endian )
 0, -- раздел зарезервирован ( 1 байт, должен быть равен 0x0 )
 length) -- длина PDU ( 4 байта – беззнаковое целое в формате 
Little Endian)
 if #header < MIN_HEADER_LEN then
 return false, "Заголовок не должен быть короче 6 байтов. Произошла ошибка."
 end
 return true, header 
end

The pdu_header_encode() function encodes information about the PDU type and length. After performing some simple checks, we define the header variable. To encode a stream of bytes according to byte order and format, we use string. pack() and a string of the format <B> B I4 , where <B is a single byte in Big Endian , and > B I4 is a byte followed by a four-byte unsigned integer price in Little Endian | The function returns a boolean value, the state of the representing operation, and a result.

Writing contextual A-ASSOCIATE message queries

Additionally, we need to write a function that sends and parses A-ASSOCIATE requests and responses. As you saw earlier in this chapter, the A-ASSOCIATE request message contains different types of contexts: application, presentation, and user information. Since this is a longer feature, let’s break it down into parts.

The application context explicitly defines the elements and parameters of the service. In DICOM, you often see Information Object Definitions (IODs) that represent data objects that are managed through a central registry. A complete list of IODs can be found at http://dicom.nema.org/dicom/2013/output/chtml/part06/chapter_A.html. We will read these IODs from the constant definitions we placed at the beginning of our library. Let’s start the DICOM connection and create an application context.

---
-- associate(host, port) Пытается связаться с провайдером службы DICOM путем отправки 
запроса A-ASSOCIATE.
--
-- @param host объект хоста
-- @param port объект порта
-- @return (status, dcm) если status равен true, возвращает объект DICOM
-- если status равен false, dcm содержит сообщение об ошибке
---
function associate(host, port, calling_aet_arg, called_aet_arg)
 local application_context = ""
 local presentation_context = ""
 local userinfo_context = ""
 local status, dcm = start_connection(host, port)
 if status == false then
 return false, dcm
 end
 application_context = string.pack(">B B I2 c" .. #UID_VALUES["APPLICATION_CONTEXT"],
 0x10, -- тип элемента (1 байт)
 0x0, -- зарезервировано ( 1 байт)
 #UID_VALUES["APPLICATION_CONTEXT"], -- длина (2 байта)
 UID_VALUES["APPLICATION_CONTEXT"]) -- контекст приложения

An application context consists of a type (one byte), a reserved field (one byte), a context length (two bytes), and a value represented by OIDs. To represent this structure in Lua, we use a string of the format B B I2 C[#length]. We can drop the line size value by one byte. We create view and user information contexts in a similar way. Here is the view context that defines Abstract Syntax and Transfer Syntax. These are sets of rules for formatting and exchanging objects, and we represent them using IODs.

presentation_context = string.pack(">B B I2 B B B B B B I2 c" .. #UID_VALUES["VERIFICATION_
SOP"] .. "B B I2 c".. #UID_VALUES["IMPLICIT_VR"],
 0x20, -- тип контекста представления ( 1 байт )
 0x0, -- зарезервировано ( 1 байт )
 0x2e, -- длина элемента ( 2 байта )
 0x1, -- идентификатор контекста представления ( 1 байт )
 0x0,0x0,0x0, -- зарезервировано ( 3 байта )
 0x30, -- дерево абстрактного синтаксиса ( 1 байт )
 0x0, -- зарезервировано ( 1 байт )
 0x11, -- длина элемента ( 2 байта )
 UID_VALUES["VERIFICATION_SOP"],
 0x40, -- синтаксис передачи ( 1 байт )
 0x0, -- зарезервировано ( 1 байт )
 0x11, -- длина элемента ( 2 байта )
 UID_VALUES["IMPLICIT_VR"])

Note that there can be multiple view contexts. Next, we define the context of user information:

local implementation_id = "1.2.276.0.7230010.3.0.3.6.2"
 local implementation_version = "OFFIS_DCMTK_362"
 userinfo_context = string.pack(">B B I2 B B I2 I4 B B I2 c" .. #implementation_id .. " B 
B I2 c".. #implementation_version,
 0x50, -- тип 0x50 (1 байт)
 0x0, -- зарезервировано ( 1 байт )
 0x3a, -- длина ( 2 байта )
 0x51, -- тип 0x51 ( 1 байт)
 0x0, -- зарезервировано ( 1 байт )
 0x04, -- длина ( 2 байта )
 0x4000, -- данные ( 4 байта )
 0x52, -- тип 0x52 (1 байт )
 0x0, -- зарезервировано (1 байт )
 0x1b, -- длина (2 байта)
 implementation_id, -- идентификатор реализации ( байты 
#implementation_id)
 0x55, -- тип 0x55 (1 байт)
 0x0, -- зарезервировано (1 байт)
 #implementation_version, -- длина (2 байта)
 implementation_version)

Now we have three variables that contain the contexts: application_context, presentation_context, and userinfo_context.

Reading script arguments in the nmap script engine

Let’s add the contexts we just created to the A-ASSOCIATE header and query. To allow other scripts to pass arguments to our function and use different values for caller names and application callers, we’ll offer two options: an optional argument or user input. In the Nmap script engine, you can read the script arguments provided by –script-args using the Nmap stdnse.get_script_args() function as follows:

local called_ae_title = called_aet_arg or stdnse.get_script_args("dicom.called_aet") or 
"ANYSCP"
 local calling_ae_title = calling_aet_arg or stdnse.get_script_args("dicom.calling_aet") 
or "NMAP-DICOM"
 if #calling_ae_title > 16 or #called_ae_title > 16 then
 return false, "Calling/Called AET field can't be longer than 16 bytes."
 еnd

The structure containing the application component headers must be 16 bytes long, so we use string.rep() to pad the rest of the buffer with spaces:

--Fill the rest of buffer with %20
 called_ae_title = called_ae_title .. string.rep(" ", 16 – #called_ae_title)
 calling_ae_title = calling_ae_title .. string.rep(" ", 16 – #calling_ae_title)

Now we can define our own calls and called application object names using script arguments. We can also use script arguments to write a tool that tries to guess the correct application object, as if we were inputting the password using the iterative method. Defining the A-ASSOCIATE query structure

Let’s build our A-ASSOCIATE query. We define its structure in the same way as in the context:

-- ASSOCIATE request
 local assoc_request = string.pack(">I2 I2 c16 c16 c32 c" .. application_
context:len() .. " c" .. presentation_context:len() .. " c".. userinfo_context:len(),
 0x1, -- версия протокола ( 2 байта )
 0x0, -- зарезервировано ( 2 байта должны быть равны 0x0 )
 called_ae_title, -- заголовок вызываемого AE ( 16 байтов)
 calling_ae_title, -- заголовок вызывающего AE ( 16 байтов)
 0x0, -- зарезервировано ( 32 должны быть равны 0x0 )
 application_context,
 presentation_context,
 userinfo_context)

We start by specifying the protocol version (two bytes), a reserved section (two bytes), the calling program object header (16 bytes), the calling program object header (16 bytes), another reserved section (32 bytes ) and newly created contexts (application, presentation, and user information). Now our A-ASSOCIATE query is missing only the header. Now it’s time to use the dicom.pdu_header_encode() function we defined earlier to generate it:

local status, header = pdu_header_encode(PDU_CODES["ASSOCIATE_REQUEST"], #assoc_request) 
 -- с заголовком может что-то пойти не так
 if status == false then
 return false, header
 end
assoc_request = header .. assoc_request 
 stdnse.debug2("PDU len minus header:%d", #assoc_request-#header)
 if #assoc_request < MIN_SIZE_ASSOC_REQ then
 return false, string.format("запрос ASSOCIATE должен содержать не менее %d байтов и мы 
пробуем послать %d.", MIN_SIZE_ASSOC_REQ, #assoc_request)
 end

We create a header with a PDU type corresponding to the A-ASSOCIATE request value, and then add the message body. We also add error checking logic here. It is now possible to send a complete A-ASSOCIATE request and read the response using the previously defined functions for sending and reading DICOM packets:

status, err = send(dcm, assoc_request)
if status == false then
return false, string.format("Невозможно отправить запрос ASSOCIATE:%s", err)
end
status, err = receive(dcm)
if status == false then
return false, string.format("Невозможно прочитать запрос ASSOCIATE:%s", err)
end
if #err < MIN_SIZE_ASSOC_RESP
then
return false, "Ответ ASSOCIATE слишком короткий."
end

That’s cool! Next, you need to define the type of BRZ that is used to accept or reject the connection.

Analysis of A-ASSOCIATE responses

At this point, it’s just a matter of unpacking  the response with string.unpack(). This is similar  to string.pack() and we use format strings to specify the structure to read. In this case, we read the response type (one byte), the reserved field (one byte), the length (four bytes), and the protocol version (two bytes), according to the format string > B B I4 I2:

local resp_type, _, resp_length, resp_version = string.unpack(">B B I4 I2", err)
stdnse.debug1("PDU тип:%d длина:%d Protocol:%d", resp_type, resp_length, resp_version)

Next, check the response code to see if it matches the PDU to accept or reject the ASSOCIATE:

if resp_type == PDU_CODES["ASSOCIATE_ACCEPT"] then
 stdnse.debug1("Обнаружено сообщение ASSOCIATE ACCEPT!")
 return true, dcm
 elseif resp_type == PDU_CODES["ASSOCIATE_REJECT"] then
 stdnse.debug1("Обнаружено сообщение ASSOCIATE REJECT!")
 return false, "получено ASSOCIATE REJECT "
 else
 return false, "Неопеределенный ответ:" .. resp_type
 end
end -- end of function

If we receive an ASSOCIATE acceptance message, we return true; otherwise,  we will return false.

Creation of the final script

Now that we’ve implemented the service association function, we create a script that loads the library and calls the dicom.associate() function:

description = [[
Пытается обнаружить серверы DICOM (провайдеры служб DICOM) при помощи частичного запроса 
C-ECHO.
Запросы C-ECHO также известны как проверочные пакеты DICOM для проверки соединения.
Обычно проверочный пакет DICOM формируется так:
* Client -> A-ASSOCIATE request -> Server
* Server -> A-ASSOCIATE ACCEPT/REJECT -> Client
* Client -> C-ECHO request -> Server
* Server -> C-ECHO response -> Client
* Client -> A-RELEASE request -> Server
* Server -> A-RELEASE response -> Client
В данном сценарии мы отправляем только запрос A-ASSOCIATE и ищем код успеха в ответе, 
поскольку это очевидный способ обнаружения провайдеров служб DICOM.
]]
---
-- @usage nmap -p4242 --script dicom-ping <target>
-- @usage nmap -sV --script dicom-ping <target>
--
-- @output
-- PORT STATE SERVICE REASON
-- 4242/tcp open dicom syn-ack
-- |_dicom-ping: провайдер служб DICOM обнаружен
---
author = "Paulino Calderon <calderon()calderonpale.com>"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery", "default"}
local shortport = require "shortport"
local dicom = require "dicom"
local stdnse = require "stdnse"
local nmap = require "nmap"
portrule = shortport.port_or_service({104, 2761, 2762, 4242, 11112}, "dicom", "tcp", "open")
action = function(host, port)
 local dcm_conn_status, err = dicom.associate(host, port)
 if dcm_conn_status == false then
 stdnse.debug1("Association failed:%s", err)
 if nmap.verbosity() > 1 then
 return string.format("Association failed:%s", err)
 else
 return nil
 end
 end
 -- Мы убедились, что это DICOM, обновляем имя службы
 port.version.name = "dicom"
 nmap.set_port_version(host, port)
 return "DICOM Service Provider discovered"
end

First, we fill in some mandatory fields, such as description, author, license, categories, and execution rule. We declare the main action script function as a Lua function. You can learn more about script formats by reading the official documentation (https://nmap.org/book/nse-script-format.html) or browsing the official script collection. If the script finds the DICOM service, it returns the following data:

Nmap scan report for 127.0.0.1
PORT STATE SERVICE REASON
4242/tcp open dicom syn-ack
|_dicom-ping: DICOM Service Provider discovered
Final times for host: srtt: 214 rttvar: 5000 to: 100000

Otherwise, the script does not return any results because, by default, Nmap only shows information when it accurately detects a service.

Conclusion

In this chapter, you learned how to work with new network protocols and created tools for the most popular network scanning (Nmap) and traffic analysis (Wireshark) frameworks. You also learned how to perform a number of common operations (creating shared data structures, manipulating strings, and performing network I/O operations) to quickly prototype new network security tools in Lua. Armed with this knowledge, you will be able to solve the problems covered in this chapter and others and sharpen your Lua skills. In the ever-evolving world of the Internet of Things, being able to quickly write new network operations tools is very convenient.

Also, remember to follow the methodology when conducting a security assessment. The topic of this chapter is only a starting point for understanding and detecting anomalies in network protocols. Because the topic is so large, we couldn’t cover all the common tasks involved in protocol analysis, but we strongly recommend that you read James Forshaw’s book mentioned above, Attacking Networks at the Protocol Level.

We used materials from the book “The Definitive Guide to Attacking the Internet of Things” written by Photios Chantsis, Ioannis Stais, Paulino Calderon, Evangelos Deirmentsoglu and Beau Woods.

Other related articles
Found an error?
If you find an error, take a screenshot and send it to the bot.