CurveLock
Working of CurveLock Ransomware
This blog post is based on my research article published in IEEE access named - "CurveLock: Exploring Elliptic Curves Implementation in Modern Ransomware".
Introduction
CurveLock 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 ransomware. 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 -
Payload Embedder - It embeds the shellcode in various IDAT sections of a benign PNG file.
CurveLock - The mastermind of the whole operation. It is responsible for everything ranging from EDR evasion to payload execution.
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
Utilizes API Hammering to obfuscate call stack of the ransomware to evade detection from sandbox environments
Creates a random compile time IAT seed to evade static detection
Unhooks NTDLL by creating a suspended process, copying the clean NTDLL from it and replacing out NTDLL in the
.textsectionExtracts process token and checks it's elevation and integrity and determines it it is being run as Admin or not.
If not being run as admin, it exploits CVE-2024-6769 by Fortra to create a new process with high integrity token.
Exploits DcSync using the DCSyncer tool by notsoshant to extract username and NTLM hash combinations from the domain controller and parses it.
Performs self deletion to avoid manual analysis after execution.
Embeds the Ransomware Payload into a
.PNGfile's IDAT sections and encrypts each section with it's own RC4 key.Payload is extracted from the
.PNGfile at runtime and executed.Also provides the decryptor for the encrypted files.
Click on the expandable tabs to learn more about a specific module.
Payload Embedder
Payload Embedder

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:
Ensures required Python packages are installed (
pycryptodome,colorama).Defines helper routines for coloring output, generating random bytes, CRC calculation, and RC4 encryption.
Reads the payload and verifies the target is a valid PNG.
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.
Each IDAT payload chunk is encrypted using it's own RC4 key.
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)
generate_random_bytes(key_length=RC4_KEY_LNG)Purpose: Securely generate a random byte string.
Default:
RC4_KEY_LNG = 16bytes.Use Cases:
Generating RC4 keys.
Creating a random buffer for the special marker IDAT.
2. calculate_chunk_crc(chunk_data)
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)
create_idat_section(buffer)Purpose: Build a valid PNG IDAT chunk containing arbitrary data.
Key Steps:
Size check: Ensures
bufferβ€MAX_IDAT_LNG(8192 bytes).Length field: 4-byte big-endian of
len(buffer).Chunk type: The literal bytes
b'IDAT'.CRC: Over
IDAT+buffer, written as 4-byte big-endian.Assembly:
[length][IDAT][buffer][CRC].
Returns: Tuple
(full_chunk_bytes, crc_bytes).
4. remove_bytes_from_end(file_path, bytes_to_remove)
remove_bytes_from_end(file_path, bytes_to_remove)Purpose: Truncate the last
bytes_to_removebytes of a file.Use Case: Remove the PNGβs IEND footer before appending custom data.
5. encrypt_rc4(key, data)
encrypt_rc4(key, data)Purpose: Encrypts
datausing RC4 and the providedkey.Library:
Crypto.Cipher.ARC4from PyCryptodome.Returns: Encrypted byte string of same length as
data.
6. plant_payload_in_png(ipng_fname, opng_fname, png_buffer)
plant_payload_in_png(ipng_fname, opng_fname, png_buffer)Purpose: Core embedding routine.
Workflow:
Copy original PNG to output.
Strip its IEND footer.
Write a βmarkerβ IDAT chunk with random data of random length (16β256 bytes) β returns its CRC (
special_idat_crc).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.
Re-append the standard IEND footer.
Returns: CRC of the marker IDAT (for detection later).
7. is_png(file_path)
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)
read_payload_from_file(payload_file)Purpose: Load the entire payload from disk into memory.
Errors: Raises if the file is missing.
9. main()
main()Purpose: CLI entry point.
Arguments:
-png/--pngfile: source PNG.-o/--output: desired output filename.-p/--payload: file to embed.
Flow:
Append β.pngβ to output if missing.
Validate input is PNG.
Read payload.
Call
plant_payload_in_png.Print success message and emit a
#definemacro containing the marker CRC (hex) for downstream extraction.
CurveLock Ransomware
Compile-Time IAT Camouflage

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
RandomCompileTimeSeedPurpose: Compute a deterministic βrandomβ integer at compile time, based entirely on the build timestamp (
__TIME__).How it works:
'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.
__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)
Weighting factors
Seconds β
Γ1andΓ10reproduce 0β59Minutes β
Γ60andΓ600reproduce 0β3540Hours β
Γ3600andΓ36000reproduce 0β82800
Result
A unique integer between roughly β2 million and +100 thousand depending on compile time.
2. Helper
HelperPurpose:
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:
HeapAllocUses the default process heap.
HEAP_ZERO_MEMORYensures the entire 0xFF bytes are initially zero.
Null-check
If allocation fails, returns
NULL.
Seeding
Calls
RandomCompileTimeSeed(), takes modulo 255 (fits in a byte).Stores it into the first 4 bytes (only the low byte matters).
Pointer passing
Writes the same
pAddressinto*ppAddress.Returns
pAddressso caller can use whichever convention.
3. IatCamouflage
IatCamouflagePurpose:
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:
Heap buffer & seed
Calls
Helper, stores buffer pointer and reads back*A=(seed % 255).
Impossible
ifChecks
*A > 350β always false (range 0β254).Prevents execution of the API calls at runtime.
Dead-code API calls
Each assignment to
ireferences a different WinAPI.Ensures the linker pulls them into the IAT.
Cleanup
HeapFreeto 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.
API Hammering

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.RUN and Malcore 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 -> Report.
Function-By-Function Analysis
Variable Declarations
szTmpPath: Holds the system temp folder (MAX_PATHwide chars).szPath: Twice as large to concatenateszTmpPath + filename.hWFile,hRFile: Write/read file handles, initialized toINVALID_HANDLE_VALUE.dwNumberOfBytesWritten/Read: I/O byte counts.pRandBuffer: Pointer to heap-allocated buffer.sBufferSize: Set to0xFFFFF(β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
szTmpPathwith your#define TMPFILE L"your.tmp"to form the full path.Example: If
szTmpPath = L"C:\\Temp\\"andTMPFILE = L"vmtest.bin", thenszPath = L"C:\\Temp\\vmtest.bin".
3. Stress Loop
Purpose: Repeat the hammering sequence
dwStresstimes. Higher counts increase detection sensitivity (but also total runtime).
3a. Create & Write the Temp File
CreateFileWOpens/creates the temp file with
GENERIC_WRITE.FILE_ATTRIBUTE_TEMPORARYhints Windows to keep data in cache.
Buffer Allocation & Randomization
HeapAlloc(β¦, HEAP_ZERO_MEMORY, sBufferSize)β ~1 MiB zeroed buffer.Seed
rand()withtime(NULL); pick a byte0β¦254.memsetfills buffer with that byte.
WriteFileWrites entire buffer; verifies the full
sBufferSizewas written.
Cleanup
RtlZeroMemorywipes buffer.CloseHandlecloses the write handle.
3b. Re-Open & Read (with Auto-Delete)
CreateFileWfor ReadOPEN_EXISTINGto open the file you just wrote.FILE_FLAG_DELETE_ON_CLOSEensures the file is removed when the handle is closed.
ReadFileReads back into the same buffer; checks byte count.
Cleanup
Zero the buffer again.
Free the heap allocation.
Close the read handle (deletes the temp file).
4. Return Success
If all
dwStressiterations complete without error, returnsTRUE.
This is run on an infinite loop in a background thread.
NTDLL Unhooking Using Process Hollowing

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
GetNtdllSizeFromBaseAddressPurpose: Given a base pointer to an in-memory module, verify itβs a valid PE image and return its total image size.
Steps:
Cast
pNtdllModuleto aIMAGE_DOS_HEADER*and checke_magic == 'MZ'.Locate the
IMAGE_NT_HEADERSviae_lfanewoffset and checkSignature == 'PE\0\0'.Read
OptionalHeader.SizeOfImage, the span in memory covering all sections.
Failure Modes: Returns
0(NULL) on any header mismatch.
2. FetchLocalNtdllBaseAddress
FetchLocalNtdllBaseAddressPurpose: Retrieve the base address of the loaded
ntdll.dllin the current process by walking the PEBβs module list.Steps:
Read the PEB pointer from the segment register (
GS:[0x60]on x64 orFS:[0x30]on x86).Navigate
InMemoryOrderModuleList.Flinktwice to skip theNULL/self entry andkernel32.dll, landing onntdll.dll.Subtract
0x10to convert from theLIST_ENTRYback to theLDR_DATA_TABLE_ENTRYstart.Return
DllBase, the moduleβs in-memory base.
3. ReadNtdllFromASuspendedProcess
ReadNtdllFromASuspendedProcessPurpose: Launch a fresh copy of e.g.
mspaint.exein a suspended/debug state, read its unmodifiedntdll.dllimage into local memory, then tear down the process.Key Points:
Remote Process Creation: Uses
DEBUG_PROCESSto prevent immediate execution of user code.Size & Allocation: Calls our previous routines to size and allocate a buffer.
Memory Read:
ReadProcessMemorypulls the entirentdll.dllimage.Cleanup: Stops debugging, terminates, and closes handles.
Failure Modes: Cleans up handles and returns
FALSEon any error.
4. ReplaceNtdllTxtSection
ReplaceNtdllTxtSectionPurpose: Overwrite the in-process
.textsection ofntdll.dllwith a clean copy.Steps:
Validate PE Headers of the local
ntdll.dll.Find the
.textsection by iterating section headers and comparing names.Compute base pointers for local vs. remote text using
VirtualAddress.Ensure size and initial bytes match for sanity.
Change memory protection to allow writes.
memcpythe unhooked code over the potentially hooked bytes.Restore protections.
5. UnhookNtDLL
UnhookNtDLLPurpose: High-level orchestration of the unhooking process.
Flow:
Fetch clean image via suspended process.
Apply patch to current processβs
ntdll.dll.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.
Custom GetProcAddress
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
strcmpto check if the current function matcheslpApiName.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
NULLto indicate failure.
Token Manipulation

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:
Open the current processβs token.
Ask whether itβs elevated.
Retrieve and decode its mandatory integrity level (Low, Medium, High, or Unknown).
Function-by-Function Analysis
1. IsTokenElevated
IsTokenElevatedValidate input: Immediately returns
FALSEifhTokenisNULL.Dynamic import: Uses
GetProcAddressQto avoid static linking toNtQueryInformationToken.Token query: Requests the built-in
TokenElevationinformation class.Interpretation:
TknElvtion.TokenIsElevatedis a DWORDβnonzero means the token has admin rights.
2. GetCurrentToken
GetCurrentTokenDynamic import: Fetches
NtOpenProcessTokenat runtime.Self-token handle: Passes
(HANDLE)-1so 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
NULLon failure.
3. QueryTokenIntegrity
QueryTokenIntegrityTwo-pass query: First call to get required buffer size, second to actually retrieve the
TOKEN_MANDATORY_LABEL.SID parsing: Uses
RtlSubAuthorityCountSidandRtlSubAuthoritySidto 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
LocalAllocand frees on exit.
Usage pattern:
Privilege Escalation
CurveLock exploits CVE-2024-6769 by Fortra 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.
Lateral Movement Through DcSync

CurveLock utilizes DCSync using DCSyncer tool by notsoshant 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
GetDomainNamePurpose: Query the local machineβs DNS domain (e.g.,
corp.example.com).Key Steps:
Calls
GetComputerNameExwithComputerNameDnsDomainto filldomainNamebuffer.On success, returns
TRUE; on failure, logsGetLastError()and returnsFALSE.
Error Handling: Prints a descriptive message if the Windows API call fails, but does not otherwise recover.
2. parseDCSyncerResult
parseDCSyncerResultPurpose: Turn the textual output of DCSyncer into an array of
(objectRDN, hashNTLM)pairs.Workflow:
Buffer duplication:
_strdupintoresultCopysostrtokcan modify it.Line iteration:
strtok(..., "\n")to process each line.RDN capture: When a line contains
"Object RDN", split at':'and store the RHS (trimmed) incurrentRDN.Hash capture: On lines with
"Hash NTLM", extract the hash string similarly.Dynamic array growth:
realloctheresultArrayfor one moreKeyValuePair.Copy
currentRDNandhashNTLMinto the new slot.Increment
resultCount.
Cleanup & output:
free(resultCopy).Store the count in
*countand return the allocated array.
Edge Cases:
If no matches occur, returns
NULLwith*count == 0.Does not check
realloc/strcpyfailures.
3. DownloadAndExecuteDCSyncer
DownloadAndExecuteDCSyncerPurpose: Fetch a DCSync helper exe, run it hidden, wait for its textual output, then return that text.
Detailed Steps:
Download:
DownloadFile(url, filePath)(fromurlmon.lib).Execute:
Build
cmd.exe /C dcsync.exe > dcsyncer_output.txt.WinExec(..., SW_HIDE)to suppress a console window.
Polling loop (up to 60 s):
Sleep 1 s per iteration.
Try
CreateFileAon the output file; if exists and nonzero size, proceed.
Read-back:
CreateFileAfor read.malloca buffer sizedfileSize + 1.ReadFilethe entire contents, null-terminate.
Cleanup:
Close handle.
Delete the temp output file.
Error Handling: Logs failures at each stage and returns
NULLon error.
4. DoLateralMovement
DoLateralMovementPurpose: High-level routine that:
Reports the hostβs domain.
Spawns and captures DCSyncer output.
Parses and prints any harvested NTLM hashes.
Flow:
Domain info via
GetDomainName.Tool run via
DownloadAndExecuteDCSyncer.Parsing via
parseDCSyncerResult.Result display in a simple
printfloop.
Resource Management: Frees both the text buffer and the parsed array before returning.
Self Deletion

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
Variable setup
szPath: buffer for full module path (wide chars).Delete: structure to mark file deletion.pRename: pointer to dynamically-builtFILE_RENAME_INFO.NewStream: the target data-stream name (e.g.":DEL").sRename: total size needed forFILE_RENAME_INFO+ stream name.
1. Allocate and initialize rename buffer
Purpose: Build a
FILE_RENAME_INFOblob 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β TellsFileDispositionInfoto delete on close.pRenamefields βFileNameLength: byte-length of the new name. βFileName: copy the wide-stringNewStream(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 | SYNCHRONIZEandFILE_SHARE_READto allow pending deletes.FileRenameInfocall: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 == TRUEensures 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.
Extraction of Payload and Local Mapped Execution

This module automates the end-to-end retrieval, decryption, and execution of encryptor payload stored inside a PNG:
Download a PNG from a remote URL.
Read the PNG into memory and parse its custom βIDATβ chunks to extract and RC4-decrypt the embedded payload.
Allocate executable memory via a local file mapping and copy the decrypted shellcode into it.
Spawn a thread to execute the shellcode in-process.
Function-by-Function Analysis
1. ExtractDecryptedPayload
ExtractDecryptedPayloadSignature 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
IENDCRC.
Marker Detection:
Looks for a special
MARKED_IDAT_HASHCRC to know when to start extracting.
Decryption Logic:
Reads a 16-byte RC4 key from the start of each payload chunk.
RC4-decrypts the remainder into a temp buffer.
Grows a single
pDecPayloadbuffer viaLocalAlloc/LocalReAlloc.Appends each decrypted slice sequentially.
Zeroes and frees the temporary buffer.
Output: Returns the decrypted buffer and its length, or
FALSEif the marker was never found.
2. ReadFileFromDiskA
ReadFileFromDiskAOpen & Validate: Uses
CreateFileAandGetFileSize.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
LocalMappingInjectionFile Mapping: Creates an in-memory RWX region without touching disk.
MapView: Requests both write and execute on the mapped view.
Injection:
memcpycopies the decrypted shellcode into the mapping.Thread-Safety: Returns the injected address for later execution.
4. fetchPayload
fetchPayloadOrchestrator: Calls each helper in sequence.
Networking: Uses
DownloadFile(fromurlmon) 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
Encryptor
ECC with Diffie-Hellman key exchange is used to generate the AES-256 keys.
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 :
Defines elliptic-curve parameters and AES constants via macros and structs.
Performs basic ECC operations (
pointAdd,pointMultiply) over a small prime field.Derives a shared ECDH secret, hashes it with SHA-256 to form a 256-bit AES key.
Stores the AES key in the registry under a per-file subkey.
Encrypts arbitrary files (up to 100 MB) with AES-256-CBC, prepending a 4-byte signature and IV.
Walks a directory tree recursively, encrypting each non-blacklisted file and writing out
<filename>.CurveLock.
Function-by-Function Analysis
1. mod and modInverse
mod and modInversemodComputes
a mod bin the range[0, b-1], fixing Cβs negative-remainder behavior.
modInverseBrute-forces the modular inverse of
amodm: findsxsuch that(a*x) % m == 1.Returns
-1if no inverse exists (rare for prime modulus).
2. pointAdd
pointAddPurpose: 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&modInverseto stay within field Fp\mathbb{F}_pFpβ.
3. pointMultiply
pointMultiplyPurpose: Fast βdouble-and-addβ scalar multiplication nPnPnP.
Algorithm:
Initializes accumulator
Rat infinity (0,0).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)
WriteShellcodeToRegistry (Key Storage)Purpose: Saves binary data (here, the derived AES key) under
HKCU\Control Panel\<lpSubKey>.Steps:
Opens or creates the
Control Panelkey withKEY_SET_VALUE.Writes a
REG_BINARYvalue named<lpSubKey>.
Use: Called in
ReplaceWithEncryptedFileto persist each fileβs AES key.
5. Aes256EncryptBuffer (AES-CBC)
Aes256EncryptBuffer (AES-CBC)Purpose: Encrypts
cbDatabytes inpbDatawith AES-256-CBC+padding.Procedure:
Opens the AES algorithm provider.
Retrieves the internal key-object size.
Allocates and initializes it via
BCryptGenerateSymmetricKey.Calls
BCryptEncryptwith the provided IV (pbIV).
Outputs: Encrypted blob and its length.
6. ReplaceWithEncryptedFile
ReplaceWithEncryptedFileSignature 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
EncryptFilesInGivenDirPurpose: Recursively traverses a directory tree starting at
szDirectoryPath.Behavior:
Builds a wildcard (
β<dir>\*β).For each entry:
Skips
"."/"..".If itβs a directory, recurse.
Otherwise, calls
ReplaceWithEncryptedFileand increments*fileIndex.
Result: Encrypts all non-blacklisted files under the path.
Results
CurveLock





Payload




Acknowledgements
I thank Maldev-Academy for providing me with knowledge to build this proof-of-concept. I would also like to thank notsoshant and Fortra for the DCSyncer and CVE-2024-6769 POC respectively.
Last updated