CurveLock

Working of CurveLock Ransomware

circle-info

This blog post is based on my research article published in IEEE access named - "CurveLock: Exploring Elliptic Curves Implementation in Modern Ransomwarearrow-up-right".

Introduction

CurveLockarrow-up-right is a modern ransomware designed from scratch to infect faster and encrypt target contents using Elliptical Curve Cryptography (ECC) algorithm.

Why create a ransomware you ask? Well because I wanted to learn and show how Elliptic curve cryptography could be used in tandem with AES-256 to encrypt victim data. Last major ransomware that employed ECC was LockBit with their LockBit-4.0 ransomwarearrow-up-right. While ECC isn't widely used Yet, ransomware groups are increasingly leveraging it to move away from the tried-and-tested (and heavily signatured) AES+RSA combination. CurveLock aims to show one implementation of the AES+ECC combination in ransomware.

The CurveLock ransomware attack chain consists of three main parts -

  1. Payload Embedder - It embeds the shellcode in various IDAT sections of a benign PNG file.

  2. CurveLock - The mastermind of the whole operation. It is responsible for everything ranging from EDR evasion to payload execution.

  3. Payload - Finally the proverbial "bullet" of the CurveLock weapon. It is responsible for employing ECC and AES to encrypt victim data.

Now you can expand and read the below sections to know more about the inner workings of CurveLock.


Features Of CurveLock

  1. Utilizes API Hammering to obfuscate call stack of the ransomware to evade detection from sandbox environments

  2. Creates a random compile time IAT seed to evade static detection

  3. Unhooks NTDLL by creating a suspended process, copying the clean NTDLL from it and replacing out NTDLL in the .text section

  4. Extracts process token and checks it's elevation and integrity and determines it it is being run as Admin or not.

  5. If not being run as admin, it exploits CVE-2024-6769arrow-up-right by Fortra arrow-up-rightto create a new process with high integrity token.

  6. Exploits DcSync using the DCSyncer arrow-up-righttool by notsoshant arrow-up-rightto extract username and NTLM hash combinations from the domain controller and parses it.

  7. Performs self deletion to avoid manual analysis after execution.

  8. Embeds the Ransomware Payload into a .PNG file's IDAT sections and encrypts each section with it's own RC4 key.

  9. Payload is extracted from the .PNG file at runtime and executed.

  10. Also provides the decryptor for the encrypted files.

circle-info

Click on the expandable tabs to learn more about a specific module.


Payload Embedder

chevron-rightPayload Embedderhashtag

It takes an input PNG file and a separate payload (shellcode), encrypts the payload in RC4-encrypted chunks, and hides those chunks inside custom IDAT sections of a copy of the PNG. It:

  1. Ensures required Python packages are installed (pycryptodome, colorama).

  2. Defines helper routines for coloring output, generating random bytes, CRC calculation, and RC4 encryption.

  3. Reads the payload and verifies the target is a valid PNG.

  4. Removes the original PNG’s end marker, appends a β€œmarker” IDAT, then embeds the RC4 encrypted payload in multiple IDAT chunks, and finally re-appends the PNG footer.

  5. Each IDAT payload chunk is encrypted using it's own RC4 key.

  6. Prints out the special marker CRC for later extraction in a companion C program

Following is the code analysis of Payload Embedder

1. generate_random_bytes(key_length=RC4_KEY_LNG)

  • Purpose: Securely generate a random byte string.

  • Default: RC4_KEY_LNG = 16 bytes.

  • Use Cases:

    • Generating RC4 keys.

    • Creating a random buffer for the special marker IDAT.


2. calculate_chunk_crc(chunk_data)

  • Purpose: Compute the CRC32 checksum of PNG chunk data.

  • Details:

    • Uses zlib.crc32.

    • Masks to 32 bits (& 0xffffffff).


3. create_idat_section(buffer)

  • Purpose: Build a valid PNG IDAT chunk containing arbitrary data.

  • Key Steps:

    1. Size check: Ensures buffer ≀ MAX_IDAT_LNG (8192 bytes).

    2. Length field: 4-byte big-endian of len(buffer).

    3. Chunk type: The literal bytes b'IDAT'.

    4. CRC: Over IDAT + buffer, written as 4-byte big-endian.

    5. Assembly: [length][IDAT][buffer][CRC].

  • Returns: Tuple (full_chunk_bytes, crc_bytes).


4. remove_bytes_from_end(file_path, bytes_to_remove)

  • Purpose: Truncate the last bytes_to_remove bytes of a file.

  • Use Case: Remove the PNG’s IEND footer before appending custom data.


5. encrypt_rc4(key, data)

  • Purpose: Encrypts data using RC4 and the provided key.

  • Library: Crypto.Cipher.ARC4 from PyCryptodome.

  • Returns: Encrypted byte string of same length as data.


6. plant_payload_in_png(ipng_fname, opng_fname, png_buffer)

  • Purpose: Core embedding routine.

  • Workflow:

    1. Copy original PNG to output.

    2. Strip its IEND footer.

    3. Write a β€œmarker” IDAT chunk with random data of random length (16–256 bytes) β†’ returns its CRC (special_idat_crc).

    4. Iterate over the payload buffer in slices of up to (MAX_IDAT_LNG – RC4_KEY_LNG) bytes:

      • Generate a fresh 16-byte RC4 key.

      • Prepend the key to the encrypted slice.

      • Package into an IDAT section and append.

    5. Re-append the standard IEND footer.

  • Returns: CRC of the marker IDAT (for detection later).


7. is_png(file_path)

  • Purpose: Validate that a file is a PNG by checking its 8-byte signature.

  • Errors: Raises if the path doesn’t exist; catches and reports I/O errors.


8. read_payload_from_file(payload_file)

  • Purpose: Load the entire payload from disk into memory.

  • Errors: Raises if the file is missing.


9. main()

  • Purpose: CLI entry point.

  • Arguments:

    • -png / --pngfile: source PNG.

    • -o / --output: desired output filename.

    • -p / --payload: file to embed.

  • Flow:

    1. Append β€œ.png” to output if missing.

    2. Validate input is PNG.

    3. Read payload.

    4. Call plant_payload_in_png.

    5. Print success message and emit a #define macro containing the marker CRC (hex) for downstream extraction.


CurveLock Ransomware

chevron-rightCompile-Time IAT Camouflagehashtag

CurveLock abuses compile-time constants and a dead-code API import path to hide Import Address Table (IAT) entries in a binary without ever actually calling them at runtime. At build time it generates a pseudo-random seed based on the compiler’s __TIME__ macro, writes it into a heap buffer, then references a bunch of β€œwhitelisted” WinAPI functions behind an impossible if so they become linked but never executed.

Function-by-Function Analysis

1. RandomCompileTimeSeed

  • Purpose: Compute a deterministic β€œrandom” integer at compile time, based entirely on the build timestamp (__TIME__).

  • How it works:

    1. '0' * -40271

      • '0' is ASCII 0x30 (48); multiplying by –40271 yields a constant offset of –1,932, (–193,? actually –48 Γ— 40271 = –1,932,? check) that ensures the final sum isn’t trivially just the time.

    2. __TIME__ indexing

      • __TIME__ is a string literal "HH:MM:SS".

      • Indexes:

        • [7] β†’ last second digit

        • [6] β†’ first second digit

        • [4] β†’ last minute digit

        • [3] β†’ first minute digit

        • [1] β†’ last hour digit (ones place)

        • [0] β†’ first hour digit (tens place)

    3. Weighting factors

      • Seconds β†’ Γ—1 and Γ—10 reproduce 0–59

      • Minutes β†’ Γ—60 and Γ—600 reproduce 0–3540

      • Hours β†’ Γ—3600 and Γ—36000 reproduce 0–82800

    4. Result

      • A unique integer between roughly –2 million and +100 thousand depending on compile time.


2. Helper

  • Purpose:

    • Allocate a 255-byte zeroed buffer on the process heap.

    • Write a byte-sized compile-time seed into its first 4 bytes.

    • Provide the allocated pointer back via two routes (return value and out-param).

  • Step-by-step:

    1. HeapAlloc

      • Uses the default process heap.

      • HEAP_ZERO_MEMORY ensures the entire 0xFF bytes are initially zero.

    2. Null-check

      • If allocation fails, returns NULL.

    3. Seeding

      • Calls RandomCompileTimeSeed(), takes modulo 255 (fits in a byte).

      • Stores it into the first 4 bytes (only the low byte matters).

    4. Pointer passing

      • Writes the same pAddress into *ppAddress.

      • Returns pAddress so caller can use whichever convention.


3. IatCamouflage

  • Purpose:

    • Trigger linker to resolve (β€œimport”) a suite of WinAPI functions without ever executing them at runtime.

    • Hide these imports in the IAT as β€œcamouflaged” calls.

  • Behavior:

    1. Heap buffer & seed

      • Calls Helper, stores buffer pointer and reads back *A = (seed % 255).

    2. Impossible if

      • Checks *A > 350 β†’ always false (range 0–254).

      • Prevents execution of the API calls at runtime.

    3. Dead-code API calls

      • Each assignment to i references a different WinAPI.

      • Ensures the linker pulls them into the IAT.

    4. Cleanup

      • HeapFree to avoid leaking the 0xFF-byte allocation.


Since the impossible branch never executes, there’s no runtime evidence of these calls. Thus CurveLock's import table now contains your chosen β€œwhitelisted” APIs, even though they never run. Also The seed value in the heap buffer and thus the single-byte stored changes whenever you recompile at a different time, but has no practical effect on the camouflage logic. This is done to induce randomness of the IAT imports.

chevron-rightAPI Hammeringhashtag

API-Hammering is an anti-sandbox technique that involves repeatedly calling some benign WinAPIs to obfuscate the call stack of the malware. Sandbox measures such as ANY.RUNarrow-up-right and Malcorearrow-up-right get confused upon seeing the obfuscated call stack and mark the binary as benign. I analyzed CurveLock in malcore and it mistakenly marks it as benign. See the report here -> Reportarrow-up-right.


Function-By-Function Analysis

  1. Variable Declarations

    • szTmpPath: Holds the system temp folder (MAX_PATH wide chars).

    • szPath: Twice as large to concatenate szTmpPath + filename.

    • hWFile, hRFile: Write/read file handles, initialized to INVALID_HANDLE_VALUE.

    • dwNumberOfBytesWritten/Read: I/O byte counts.

    • pRandBuffer: Pointer to heap-allocated buffer.

    • sBufferSize: Set to 0xFFFFF (β‰ˆ1 MiB).

    • Random: Temporary for byte-value seed.


1. Get the Temp Folder Path

  • Purpose: Query Windows for the current user’s temp directory (C:\Users\<…>\AppData\Local\Temp\).

  • On Failure: Logs the error code and aborts (FALSE).


2. Build the Full Temp File Path

  • Purpose: Concatenate szTmpPath with your #define TMPFILE L"your.tmp" to form the full path.

  • Example: If szTmpPath = L"C:\\Temp\\" and TMPFILE = L"vmtest.bin", then szPath = L"C:\\Temp\\vmtest.bin".


3. Stress Loop

  • Purpose: Repeat the hammering sequence dwStress times. Higher counts increase detection sensitivity (but also total runtime).


3a. Create & Write the Temp File

  1. CreateFileW

    • Opens/creates the temp file with GENERIC_WRITE.

    • FILE_ATTRIBUTE_TEMPORARY hints Windows to keep data in cache.

  2. Buffer Allocation & Randomization

    • HeapAlloc(…, HEAP_ZERO_MEMORY, sBufferSize) β†’ ~1 MiB zeroed buffer.

    • Seed rand() with time(NULL); pick a byte 0…254.

    • memset fills buffer with that byte.

  3. WriteFile

    • Writes entire buffer; verifies the full sBufferSize was written.

  4. Cleanup

    • RtlZeroMemory wipes buffer.

    • CloseHandle closes the write handle.


3b. Re-Open & Read (with Auto-Delete)

  1. CreateFileW for Read

    • OPEN_EXISTING to open the file you just wrote.

    • FILE_FLAG_DELETE_ON_CLOSE ensures the file is removed when the handle is closed.

  2. ReadFile

    • Reads back into the same buffer; checks byte count.

  3. Cleanup

    • Zero the buffer again.

    • Free the heap allocation.

    • Close the read handle (deletes the temp file).


4. Return Success

  • If all dwStress iterations complete without error, returns TRUE.


This is run on an infinite loop in a background thread.

chevron-rightNTDLL Unhooking Using Process Hollowinghashtag

NTDLL unhooking is required for CurveLock to run smoothly as EDR's often hook the ntdll.dll functions and monitor API usage. Manually importing an ntdll.dll from disk is extremely risky as it is very unusual for processes to do that and EDRs will definitely catch that. An alternative method to unhook ntdll.dll involves reading it from a suspended process. This works because EDRs require a running process to install their hooks and therefore a process created in a suspended state, will contain a clean ntdll.dll image allowing for the text section of the current process to be substituted with that of the suspended one. This process is called process hollowing and CurveLock does just that.


Function-by-Function Analysis

1. GetNtdllSizeFromBaseAddress

  • Purpose: Given a base pointer to an in-memory module, verify it’s a valid PE image and return its total image size.

  • Steps:

    1. Cast pNtdllModule to a IMAGE_DOS_HEADER* and check e_magic == 'MZ'.

    2. Locate the IMAGE_NT_HEADERS via e_lfanew offset and check Signature == 'PE\0\0'.

    3. Read OptionalHeader.SizeOfImage, the span in memory covering all sections.

  • Failure Modes: Returns 0 (NULL) on any header mismatch.


2. FetchLocalNtdllBaseAddress

  • Purpose: Retrieve the base address of the loaded ntdll.dll in the current process by walking the PEB’s module list.

  • Steps:

    1. Read the PEB pointer from the segment register (GS:[0x60] on x64 or FS:[0x30] on x86).

    2. Navigate InMemoryOrderModuleList.Flink twice to skip the NULL/self entry and kernel32.dll, landing on ntdll.dll.

    3. Subtract 0x10 to convert from the LIST_ENTRY back to the LDR_DATA_TABLE_ENTRY start.

    4. Return DllBase, the module’s in-memory base.


3. ReadNtdllFromASuspendedProcess

  • Purpose: Launch a fresh copy of e.g. mspaint.exe in a suspended/debug state, read its unmodified ntdll.dll image into local memory, then tear down the process.

  • Key Points:

    1. Remote Process Creation: Uses DEBUG_PROCESS to prevent immediate execution of user code.

    2. Size & Allocation: Calls our previous routines to size and allocate a buffer.

    3. Memory Read: ReadProcessMemory pulls the entire ntdll.dll image.

    4. Cleanup: Stops debugging, terminates, and closes handles.

  • Failure Modes: Cleans up handles and returns FALSE on any error.


4. ReplaceNtdllTxtSection

  • Purpose: Overwrite the in-process .text section of ntdll.dll with a clean copy.

  • Steps:

    1. Validate PE Headers of the local ntdll.dll.

    2. Find the .text section by iterating section headers and comparing names.

    3. Compute base pointers for local vs. remote text using VirtualAddress.

    4. Ensure size and initial bytes match for sanity.

    5. Change memory protection to allow writes.

    6. memcpy the unhooked code over the potentially hooked bytes.

    7. Restore protections.


5. UnhookNtDLL

  • Purpose: High-level orchestration of the unhooking process.

  • Flow:

    1. Fetch clean image via suspended process.

    2. Apply patch to current process’s ntdll.dll.

    3. Free the temporary buffer and report success.


In the end, any user-mode hooks or patches in CurveLock’s ntdll.dll are overwritten with original bytes, restoring expected system call stubs.

chevron-rightCustom GetProcAddresshashtag

GetProcAddressQ is a low-level manual API resolver used to locate the address of an exported function in a loaded module (DLL or EXE) without using Windows API. Thus it is highly suspicious if a binary has it in it's IAT import. In order to avoid static detection, I had to create a manual GetProcAddress function. Here is it's implementation.


1. Initial Setup & Header Parsing

  • Converts the base address into a byte pointer.

  • Verifies the DOS and NT headers (MZ, PE\0\0) to ensure a valid PE image.


2. Locate the Export Directory

  • Retrieves the export table directory entry from the optional header.

  • Converts its Relative Virtual Address (RVA) to a pointer in memory.


3. Resolve Export Table Components

  • The export directory contains:

    • Function names β†’ an array of RVAs to null-terminated strings.

    • Ordinals β†’ mapping of name indices to function index.

    • Function addresses β†’ actual function RVA values.

  • Converts each of these arrays into pointers for traversal.


4. Loop Through All Exported Functions

  • Iterates through the function names (not all exported functions, only named ones).

  • Uses strcmp to check if the current function matches lpApiName.

  • Resolves the function address using FunctionOrdinalArray[i].

  • On match: prints debug info and returns the absolute address.


5. Return NULL If Not Found

  • If no function name matched, return NULL to indicate failure.

chevron-rightToken Manipulationhashtag

CurveLock uses token manipulation to determine whether the current process is running with elevated (Administrator) privileges and to query its integrity level. They dynamically resolve native NTDLL calls (NtQueryInformationToken, NtOpenProcessToken, and SID-helper routines) to avoid static imports, then use them to:

  1. Open the current process’s token.

  2. Ask whether it’s elevated.

  3. Retrieve and decode its mandatory integrity level (Low, Medium, High, or Unknown).


Function-by-Function Analysis

1. IsTokenElevated

  • Validate input: Immediately returns FALSE if hToken is NULL.

  • Dynamic import: Uses GetProcAddressQ to avoid static linking to NtQueryInformationToken.

  • Token query: Requests the built-in TokenElevation information class.

  • Interpretation: TknElvtion.TokenIsElevated is a DWORDβ€”nonzero means the token has admin rights.


2. GetCurrentToken

  • Dynamic import: Fetches NtOpenProcessToken at runtime.

  • Self-token handle: Passes (HANDLE)-1 so NTDLL opens the caller’s own process.

  • Desired access: TOKEN_QUERY, enough to query elevation & integrity.

  • Result: Returns a valid token handle on success, or NULL on failure.


3. QueryTokenIntegrity

  • Two-pass query: First call to get required buffer size, second to actually retrieve the TOKEN_MANDATORY_LABEL.

  • SID parsing: Uses RtlSubAuthorityCountSid and RtlSubAuthoritySid to pick out the last sub-authority (the integrity level RID).

  • Integrity mapping:

    • < LOW_RID β†’ Unknown

    • < MEDIUM_RID β†’ Low

    • < HIGH_RID β†’ Medium

    • β‰₯ HIGH_RID β†’ High

  • Resource management: Allocates with LocalAlloc and frees on exit.


Usage pattern:

chevron-rightPrivilege Escalationhashtag

CurveLock exploits CVE-2024-6769arrow-up-right by Fortraarrow-up-right for privilege escalation. Here is how it works as per Fortra -

CVE-2024-6769 is a two-stage local privilege escalation vulnerability affecting multiple versions of Microsoft Windows, including Windows 10, Windows 11, and Windows Server editions 2016, 2019, and 2022.

The first stage of the CVE-2024-6769 exploit involves a clever abuse of Windows object namespace manipulation. It uses NtCreateSymbolicLinkObject to create a symbolic link that remaps the root of a drive to a directory fully controlled by the attacker. This drive remapping fools the operating system into resolving file paths incorrectly, making it believe that system files or DLLs reside in the attacker-controlled directory. Once the redirection is in place, the attacker plants a malicious DLL with the same name as a legitimate system DLL expected by a privileged application. When that application attempts to load the DLL, it instead loads the attacker’s version from the fake drive root, resulting in arbitrary code execution under the context of the targeted process, typically at a medium integrity level. This forms the foundation for escalating privileges further.

In the second stage, the exploit targets the Activation Context cache, which is managed by the Client/Server Runtime Subsystem (CSRSS). This cache is used by Windows to optimize the loading of manifest-based applications by storing metadata that dictates how executables should behave in different contexts. The attacker, already executing code via DLL hijacking, poisons this cache by injecting crafted entries that manipulate integrity levels. Through this manipulation, the attacker tricks the system into misclassifying the privilege level of certain operations, effectively allowing a process launched at medium integrity to behave as though it were at high integrity. This technique bypasses User Account Control (UAC), enabling privilege escalation without prompting the user. The result is full administrative privileges on the system, achieved with no user interaction and without triggering traditional defenses.

chevron-rightLateral Movement Through DcSynchashtag

CurveLock utilizes DCSync using DCSyncer arrow-up-righttool by notsoshantarrow-up-right to extract NTLM hashes from the domain controller. CurveLock assumes that the user account context in which it is being executed is an domain admin or has DCSync rights. The extracted NTLM hashes and username combinations are parsed and stored in a structure along with the domain name for later use.


Function-by-Function Analysis

1. GetDomainName

  • Purpose: Query the local machine’s DNS domain (e.g., corp.example.com).

  • Key Steps:

    1. Calls GetComputerNameEx with ComputerNameDnsDomain to fill domainName buffer.

    2. On success, returns TRUE; on failure, logs GetLastError() and returns FALSE.

  • Error Handling: Prints a descriptive message if the Windows API call fails, but does not otherwise recover.


2. parseDCSyncerResult

  • Purpose: Turn the textual output of DCSyncer into an array of (objectRDN, hashNTLM) pairs.

  • Workflow:

    1. Buffer duplication: _strdup into resultCopy so strtok can modify it.

    2. Line iteration: strtok(..., "\n") to process each line.

    3. RDN capture: When a line contains "Object RDN", split at ':' and store the RHS (trimmed) in currentRDN.

    4. Hash capture: On lines with "Hash NTLM", extract the hash string similarly.

    5. Dynamic array growth:

      • realloc the resultArray for one more KeyValuePair.

      • Copy currentRDN and hashNTLM into the new slot.

      • Increment resultCount.

    6. Cleanup & output:

      • free(resultCopy).

      • Store the count in *count and return the allocated array.

  • Edge Cases:

    • If no matches occur, returns NULL with *count == 0.

    • Does not check realloc/strcpy failures.


3. DownloadAndExecuteDCSyncer

  • Purpose: Fetch a DCSync helper exe, run it hidden, wait for its textual output, then return that text.

  • Detailed Steps:

    1. Download: DownloadFile(url, filePath) (from urlmon.lib).

    2. Execute:

      • Build cmd.exe /C dcsync.exe > dcsyncer_output.txt.

      • WinExec(..., SW_HIDE) to suppress a console window.

    3. Polling loop (up to 60 s):

      • Sleep 1 s per iteration.

      • Try CreateFileA on the output file; if exists and nonzero size, proceed.

    4. Read-back:

      • CreateFileA for read.

      • malloc a buffer sized fileSize + 1.

      • ReadFile the entire contents, null-terminate.

    5. Cleanup:

      • Close handle.

      • Delete the temp output file.

  • Error Handling: Logs failures at each stage and returns NULL on error.


4. DoLateralMovement

  • Purpose: High-level routine that:

    1. Reports the host’s domain.

    2. Spawns and captures DCSyncer output.

    3. Parses and prints any harvested NTLM hashes.

  • Flow:

    • Domain info via GetDomainName.

    • Tool run via DownloadAndExecuteDCSyncer.

    • Parsing via parseDCSyncerResult.

    • Result display in a simple printf loop.

  • Resource Management: Frees both the text buffer and the parsed array before returning.

chevron-rightSelf Deletionhashtag

CurveLock automatically renames the running executable into an alternate NTFS data stream and then marks it for deletion. By using SetFileInformationByHandle with FileRenameInfo and FileDispositionInfo, it ensures the binary cleans up after itself once its handle is closed, without spawning any external process.


Step-by-Step Analysis

  1. Variable setup

    • szPath: buffer for full module path (wide chars).

    • Delete: structure to mark file deletion.

    • pRename: pointer to dynamically-built FILE_RENAME_INFO.

    • NewStream: the target data-stream name (e.g. ":DEL").

    • sRename: total size needed for FILE_RENAME_INFO + stream name.


1. Allocate and initialize rename buffer

  • Purpose: Build a FILE_RENAME_INFO blob with space for the new stream name.

  • HEAP_ZERO_MEMORY: Zeroes all fields, avoiding uninitialized data.


2. Prepare delete flag and stream name

  • Delete.DeleteFile = TRUE – Tells FileDispositionInfo to delete on close.

  • pRename fields – FileNameLength: byte-length of the new name. – FileName: copy the wide-string NewStream (including the leading colon).


3. Get own executable’s path

  • GetModuleFileNameW(NULL, …) returns the full path of the running EXE.


4. Rename the file into the new stream

  • Open with DELETE | SYNCHRONIZE and FILE_SHARE_READ to allow pending deletes.

  • FileRenameInfo call:

    • Atomically renames C:\path\app.exe β†’ C:\path\app.exe:NewStream, effectively hiding the main data.

  • Closing the handle finalizes the rename.


5. Mark the stream-renamed file for deletion

  • Re-open the same path (now referring to the unnamed primary stream).

  • FileDispositionInfo:

    • Delete.DeleteFile == TRUE ensures the file is removed when this handle closes.

  • No external processes are needed; Windows garbage-collects the file once all handles are gone.


6. Cleanup and return

  • Free the heap-allocated rename info.

  • Return success if all operations completed without error.

chevron-rightExtraction of Payload and Local Mapped Executionhashtag

This module automates the end-to-end retrieval, decryption, and execution of encryptor payload stored inside a PNG:

  1. Download a PNG from a remote URL.

  2. Read the PNG into memory and parse its custom β€œIDAT” chunks to extract and RC4-decrypt the embedded payload.

  3. Allocate executable memory via a local file mapping and copy the decrypted shellcode into it.

  4. Spawn a thread to execute the shellcode in-process.


Function-by-Function Analysis

1. ExtractDecryptedPayload

  • Signature Check: Confirms the first 4 bytes match the PNG magic.

  • Chunk Parsing Loop:

    • Reads each chunk’s big-endian length, 4-byte type, data, and CRC.

    • Stops at the standard IEND CRC.

  • Marker Detection:

    • Looks for a special MARKED_IDAT_HASH CRC to know when to start extracting.

  • Decryption Logic:

    1. Reads a 16-byte RC4 key from the start of each payload chunk.

    2. RC4-decrypts the remainder into a temp buffer.

    3. Grows a single pDecPayload buffer via LocalAlloc/LocalReAlloc.

    4. Appends each decrypted slice sequentially.

    5. Zeroes and frees the temporary buffer.

  • Output: Returns the decrypted buffer and its length, or FALSE if the marker was never found.


2. ReadFileFromDiskA

  • Open & Validate: Uses CreateFileA and GetFileSize.

  • Buffer Allocation: Zero-initialized via HeapAlloc.

  • Full Read: Ensures the exact byte count is read.

  • Cleanup Label: Centralized error path that closes the handle and frees the buffer if needed.


3. LocalMappingInjection

  • File Mapping: Creates an in-memory RWX region without touching disk.

  • MapView: Requests both write and execute on the mapped view.

  • Injection: memcpy copies the decrypted shellcode into the mapping.

  • Thread-Safety: Returns the injected address for later execution.


4. fetchPayload

  • Orchestrator: Calls each helper in sequence.

  • Networking: Uses DownloadFile (from urlmon) to grab the PNG.

  • Execution: Spawns a thread to run the injected shellcode, then waits indefinitely.

  • Resource Cleanup: Frees memory and closes handles after execution.


Local mapped injection is done through a sacrificial thread so that in the rare event it gets detected, the thread can be killed mid-execution later. This is an optional implementation that I haven't done yet.


Payload

chevron-rightEncryptorhashtag
  1. ECC with Diffie-Hellman key exchange is used to generate the AES-256 keys.

  2. Each file in encrypted with it's own AES-256 key and the keys are stored in the registry under - "HKCU_CURRENT_USER\CONTROL PANEL"


Elliptic Curve Cryptography (ECC) Operations

ECC is used in the code to securely generate a shared secret between two parties (na and nb). This secret is then used to derive the AES key.

The elliptic curve is defined by the equation:


So, the encryptor implements :

  1. Defines elliptic-curve parameters and AES constants via macros and structs.

  2. Performs basic ECC operations (pointAdd, pointMultiply) over a small prime field.

  3. Derives a shared ECDH secret, hashes it with SHA-256 to form a 256-bit AES key.

  4. Stores the AES key in the registry under a per-file subkey.

  5. Encrypts arbitrary files (up to 100 MB) with AES-256-CBC, prepending a 4-byte signature and IV.

  6. Walks a directory tree recursively, encrypting each non-blacklisted file and writing out <filename>.CurveLock.


Function-by-Function Analysis

1. mod and modInverse

  • mod

    • Computes a mod b in the range [0, b-1], fixing C’s negative-remainder behavior.

  • modInverse

    • Brute-forces the modular inverse of a mod m: finds x such that (a*x) % m == 1.

    • Returns -1 if no inverse exists (rare for prime modulus).


2. pointAdd

  • Purpose: Implements EC point addition over curve y2=x3+ax+bβ€Šmodβ€Špy^2 = x^3 + ax + b \bmod py2=x3+ax+bmodp.

  • Doubling: If P == Q, uses the tangent slope Ξ»=(3x2+a)/(2y)\lambda = (3x^2 + a) / (2y)Ξ»=(3x2+a)/(2y).

  • Addition: Otherwise, Ξ»=(yQβˆ’yP)/(xQβˆ’xP)\lambda = (y_Q - y_P)/(x_Q - x_P)Ξ»=(yQβ€‹βˆ’yP​)/(xQβ€‹βˆ’xP​).

  • Uses mod & modInverse to stay within field Fp\mathbb{F}_pFp​.


3. pointMultiply

  • Purpose: Fast β€œdouble-and-add” scalar multiplication nPnPnP.

  • Algorithm:

    1. Initializes accumulator R at infinity (0,0).

    2. For each bit of n:

      • If bit is 1, R = R + Q.

      • Then Q = Q + Q.

  • Result: Returns the EC point nβ‹…Pn \cdot Pnβ‹…P.


4. WriteShellcodeToRegistry (Key Storage)

  • Purpose: Saves binary data (here, the derived AES key) under HKCU\Control Panel\<lpSubKey>.

  • Steps:

    1. Opens or creates the Control Panel key with KEY_SET_VALUE.

    2. Writes a REG_BINARY value named <lpSubKey>.

  • Use: Called in ReplaceWithEncryptedFile to persist each file’s AES key.


5. Aes256EncryptBuffer (AES-CBC)

  • Purpose: Encrypts cbData bytes in pbData with AES-256-CBC+padding.

  • Procedure:

    1. Opens the AES algorithm provider.

    2. Retrieves the internal key-object size.

    3. Allocates and initializes it via BCryptGenerateSymmetricKey.

    4. Calls BCryptEncrypt with the provided IV (pbIV).

  • Outputs: Encrypted blob and its length.


6. ReplaceWithEncryptedFile

  • Signature Check: Avoids encrypting files that already start with 'CVLK'.

  • ECDH Exchange: Generates two random scalars, computes shared point, hashes it to derive the AES key.

  • Registry Key: Persists each key under "CurveLock_<index>".

  • Binary Format:


7. EncryptFilesInGivenDir

  • Purpose: Recursively traverses a directory tree starting at szDirectoryPath.

  • Behavior:

    1. Builds a wildcard (β€œ<dir>\*”).

    2. For each entry:

      • Skips "."/"..".

      • If it’s a directory, recurse.

      • Otherwise, calls ReplaceWithEncryptedFile and increments *fileIndex.

  • Result: Encrypts all non-blacklisted files under the path.


Results

CurveLock

Execution In Unprivileged Context
Privilege Escalation
Credential Dumping Using DCSyncer
Decryptor Download Success/Failure
Attacker Server Output

Payload

Before Execution Of Payload
After Execution Of Payload
Payload dealing with BlackListed extension
Decryptor Output

Acknowledgements

I thank Maldev-Academyarrow-up-right for providing me with knowledge to build this proof-of-concept. I would also like to thank notsoshantarrow-up-right and Fortraarrow-up-right for the DCSyncerarrow-up-right and CVE-2024-6769arrow-up-right POC respectively.

Last updated