Decoy machines may be among the simplest decoys used because they do not rely on any other primitive decoys. Despite their simplicity, decoy machines are highly effective. While there are many commercially available decoy machines, building one from scratch provides an opportunity to understand how and why they work the way they do.
What is a machine?
From the perspective of another machine on a network, a machine is a collection of services running on their respective ports. Interacting with the services on another machine typically involves sending traffic to an IP address and port combination. Many times, a program abstracts the transmission of traffic away from the user, who can focus instead on what they intend to accomplish.
For example, the secure shell (SSH) service allows a user to remotely log into and run commands on another computer. In order to log into a computer running SSH, a user runs the command ssh <user>@<ip_address>
. Both the computer that the user is currently on and the computer they are trying to log into know that SSH runs on port 22 by default, so the program establishes a connection to the target IP address on port 22 and prompts the user for their password.
A basic decoy machine
Since (from a network perspective) a machine is a collection of services running on ports, a decoy machine is a collection of decoy services running on ports. The simplest decoy service is one that accepts connections on a predefined port, and alerts any time something connects to it. The Python code that follows is an example of such a service:
import socket
# Listen from any IP address
ANY_IP = "0.0.0.0"
# Listen on port 22
SERVICE_PORT = 22
# Create a socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Bind the socket to an address and port
service_addr = (ANY_IP, SERVICE_PORT)
sock.bind(service_addr)
# Listen for incoming connections
sock.listen(1)
# Indicate that the program is running
print('Waiting for a connection...')
# Accept a connection
_, client_addr = sock.accept()
# Send an alert - for demonstration purposes this will suffice
print(f"Connection from: {client_addr}")
# Close the connection
sock.close()
A machine on a network running this service is the most basic version of a decoy machine; it will alert a security team if another machine on the network connects to that port in an attempt to use or gather information about the service. Below, nmap (short for Network Mapper), scans the decoy machine’s port 22 to determine if there is a service running.
While the basic example is a decoy service that can be run on a decoy machine, it isn’t as convincing as it probably could (or should) be. Normally, an nmap scan run on an SSH server will report a service name “SSH”. As seen below, that is not the case for the basic decoy.
Diving deeper
All services speak their own language; a real SSH server will only understand traffic in the “SSH language”, and can only respond “in SSH”. Right now, the basic decoy doesn’t speak SSH. To understand what the basic decoy does speak, a network protocol analyzer tool like Wireshark can capture and display the conversation between the two machines that resulted from the gif above.
After the first three packets (the TCP handshake, which will be present in all “conversations”), the scanning computer sends a RST packet and the conversation ends. With no other information available to decide what language was being spoken, nmap decided that the most likely candidate was “tcpwrapped”. In order to convince nmap that the language is SSH, it helps to first look at a conversation with a real SSH server.
After the TCP handshake and RST packet like before, the computer sends another TCP handshake. This is the equivalent of having a quick conversation, saying goodbye, and then saying “oh wait, one more thing”. In this case, that “one more thing” is an exchange of server information, during which the SSH server tells the computer that it runs SSH (specifically “SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10” as seen in packet number 15). In order to convince nmap that it is speaking SSH, the decoy will need to repeat this portion of the conversation.
A more realistic version
When a service sends data to a computer immediately after the TCP handshake is complete, that is called a “banner.” The code that follows builds upon the basic decoy service by giving it the ability to send a banner to a computer after receiving a connection.
import socket
# Listen from any IP address
ANY_IP = "0.0.0.0"
# Listen on port 22
SERVICE_PORT = 22
# Create a socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Bind the socket to an address and port
service_addr = (ANY_IP, SERVICE_PORT)
sock.bind(service_addr)
# Listen for incoming connections
sock.listen(1)
# Indicate that the program is running
print('Waiting for a connection...')
try:
while True:
try:
# Accept a connection
conn, client_addr = sock.accept()
# Send an alert - for demonstration purposes this will suffice
print(f"Connection from: {client_addr}")
# Send the SSH banner
banner = b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10\n"
conn.sendall(banner)
# Close the connection
conn.close()
except ConnectionResetError:
# Suppress the error that would otherwise occur here
pass
except KeyboardInterrupt:
# Suppress the error and report that the service is shutting down
print("Shutting down decoy service")
finally:
# Close the connection
sock.close()
With these code upgrades, the decoy service is able to fool nmap into reporting that a real SSH service is running on port 22 of the decoy machine even though that is not the case.
Inspecting the network traffic shows that conversation between the computer and the decoy service is nearly identical to the conversation between the computer and the real SSH service above.
Putting it all together
Decoy machines often have more than one service running on them, but details will depend on the environment the machine is placed in and how realistic the decoy should be. Decoy machines that are not very realistic may catch the attention of any attacker, but more sophisticated ones might steer clear of a decoy that looks too good to be true. Realistic decoys might only grab the attention of sophisticated attackers, but because it is difficult to distinguish from a real machine, it will likely detect the presence of those who find it.
In order to mimic a computer that “makes sense” on a given network, its designer should take care to run similar-looking services on the same ports that legitimate machines on that network do. Some services will require more complicated code than the SSH example above to make a convincing decoy; in these cases, inspecting network traffic from one computer to another running the real service with a tool like Wireshark will shed light on what has to be copied to make a credible decoy.
As a final (for now) note on decoy machines, developing fake services with built-in alerting capabilities is not the only way to construct a decoy machine. Other methods may be the topic of a future blog post.