Description
AHHHHHHHH!!!! Vegetable CTF hit us with ransomware! Thank goodness I got a memory dump while the process was running… Can you help me recover my projects?
Recon
Okay, it’s time to recover the files. VegetibleCTF will not suceed on our watch. Normally, I would start with Volatility, but at least by default, I can’t get symbols to load for this memory dump. Instead, let’s look for interesting strings that might tell us what we’re looking at. We know it’s ransomware, so there’s probably a note. Running strings with grep to look for ransom notes, we find the following:
*** Your files have been encrypted ***
All files under your home directory have been encrypted using AES-256.
The random encryption key has been encrypted with my public key and stored at:
To recover your files, you will need that key file along with the decryptor that I will send you when you pay the million bitcoin ransom, muwahaha!
This program will remain running while this window stays open.
Your Key? My Key
The proper thing to do would be to extract the ransomware process, and search it’s memory to see if the key is still contained in RAM. However… There’s only 8gb of RAM, and we have known filetypes. We know that AES-256 was used, so we can just try every possible 8 byte aligned key, check if it decrypts the pdf file header properly, and if so, use the key to decrypt our target files. This is not elegant, but does work. We do have to guess the cipher mode, but for the sake of brevity, we’ll assume that we figured out it was OFB.
Since we need the solve to run fast, we wrote it in C, and ran it against the UAP document in the projects folder.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <limits.h>
#include <openssl/evp.h>
#define IV_LEN 16
#define KEY_LEN 32
#define STRIDE 8
#define AES_BLOCK 16
#define PDF_MAGIC "%PDF-"
#define PDF_MAGIC_LEN 5
#define DEFAULT_MAX (8ULL * 1024 * 1024 * 1024)
#define PROGRESS_EVERY 50000000
/* Decrypt only the first block (16 bytes) to check for PDF header. No Final. */
static int try_key_first_block(EVP_CIPHER_CTX *ctx, const unsigned char *key, const unsigned char *iv,
const unsigned char *ct, unsigned char *plain16)
{
if (EVP_DecryptInit_ex(ctx, EVP_aes_256_ofb(), NULL, key, iv) != 1)
return -1;
int out_len;
if (EVP_DecryptUpdate(ctx, plain16, &out_len, ct, AES_BLOCK) != 1)
return -1;
return (out_len >= PDF_MAGIC_LEN) ? 0 : -1;
}
static int try_key(EVP_CIPHER_CTX *ctx, const unsigned char *key, const unsigned char *iv,
const unsigned char *ct, size_t ct_len,
unsigned char *plain, size_t *plain_len)
{
if (EVP_DecryptInit_ex(ctx, EVP_aes_256_ofb(), NULL, key, iv) != 1)
return -1;
int out_len;
if (EVP_DecryptUpdate(ctx, plain, &out_len, ct, (int)ct_len) != 1)
return -1;
*plain_len = (size_t)out_len;
int fin_len;
if (EVP_DecryptFinal_ex(ctx, plain + out_len, &fin_len) != 1)
return -1;
*plain_len += (size_t)fin_len;
return 0;
}
int main(int argc, char **argv)
{
const char *dump_path;
const char *veg_path;
char out_buf[PATH_MAX];
const char *out_path = NULL;
unsigned long long max_offset = DEFAULT_MAX;
unsigned long long progress_every = PROGRESS_EVERY;
if (argc < 3) {
fprintf(stderr, "Usage: %s <dump> <file.veg> [output_path] [max_offset] [progress_interval]\n", argv[0]);
return 1;
}
dump_path = argv[1];
veg_path = argv[2];
if (argc >= 4 && argv[3][0] != '\0')
out_path = argv[3];
if (argc >= 5)
max_offset = strtoull(argv[4], NULL, 0);
if (argc >= 6)
progress_every = strtoull(argv[5], NULL, 0);
if (out_path == NULL) {
size_t len = strlen(veg_path);
if (len > 4 && strcmp(veg_path + len - 4, ".veg") == 0) {
snprintf(out_buf, sizeof(out_buf), "%.*s", (int)(len - 4), veg_path);
out_path = out_buf;
} else {
snprintf(out_buf, sizeof(out_buf), "%s.dec", veg_path);
out_path = out_buf;
}
}
/* Load .veg file */
FILE *fv = fopen(veg_path, "rb");
if (!fv) {
perror(veg_path);
return 1;
}
fseek(fv, 0, SEEK_END);
long veg_size = ftell(fv);
rewind(fv);
if (veg_size < (long)(IV_LEN + AES_BLOCK)) {
fprintf(stderr, "Veg file too short\n");
fclose(fv);
return 1;
}
unsigned char *veg = malloc((size_t)veg_size);
if (!veg) {
fclose(fv);
return 1;
}
if (fread(veg, 1, (size_t)veg_size, fv) != (size_t)veg_size) {
fprintf(stderr, "Failed to read veg file\n");
free(veg);
fclose(fv);
return 1;
}
fclose(fv);
const unsigned char *iv = veg;
const unsigned char *ct = veg + IV_LEN;
size_t ct_len = (size_t)veg_size - IV_LEN;
/* Allocate plaintext buffer */
unsigned char *plain = malloc(ct_len + 32);
if (!plain) {
free(veg);
return 1;
}
/* Open dump and mmap */
int fd = open(dump_path, O_RDONLY);
if (fd < 0) {
perror(dump_path);
free(plain);
free(veg);
return 1;
}
struct stat st;
if (fstat(fd, &st) != 0) {
perror("fstat");
close(fd);
free(plain);
free(veg);
return 1;
}
size_t dump_size = (size_t)st.st_size;
if (dump_size > SIZE_MAX) dump_size = SIZE_MAX;
if (dump_size <= KEY_LEN) {
fprintf(stderr, "Dump too small\n");
free(plain);
free(veg);
return 1;
}
if (max_offset > dump_size - KEY_LEN)
max_offset = dump_size - KEY_LEN;
void *dump = mmap(NULL, dump_size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);
if (dump == MAP_FAILED) {
perror("mmap");
free(plain);
free(veg);
return 1;
}
fprintf(stderr, "Dump: %s\n", dump_path);
fprintf(stderr, "Veg: %s\n", veg_path);
fprintf(stderr, "Out: %s\n", out_path);
fprintf(stderr, "Max offset: 0x%llx (%llu), stride %d\n", (unsigned long long)max_offset, (unsigned long long)max_offset, STRIDE);
fprintf(stderr, "Running...\n");
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
munmap(dump, dump_size);
free(plain);
free(veg);
return 1;
}
unsigned char first_block[AES_BLOCK];
unsigned long long tried = 0;
for (unsigned long long offset = 0; offset <= max_offset - KEY_LEN; offset += STRIDE) {
const unsigned char *key = (const unsigned char *)dump + offset;
if (try_key_first_block(ctx, key, iv, ct, first_block) != 0)
continue;
if (memcmp(first_block, PDF_MAGIC, PDF_MAGIC_LEN) != 0) {
tried++;
if (progress_every && tried % progress_every == 0)
fprintf(stderr, " tried %llu keys (offset 0x%llx)\n", tried, offset);
continue;
}
/* Match: do full decrypt and write */
size_t plain_len;
if (try_key(ctx, key, iv, ct, ct_len, plain, &plain_len) != 0)
continue;
fprintf(stdout, "\nSUCCESS at offset 0x%llx (%llu)\n", offset, offset);
fprintf(stdout, "Key (hex): ");
for (int i = 0; i < KEY_LEN; i++)
fprintf(stdout, "%02x", key[i]);
fprintf(stdout, "\n");
FILE *fo = fopen(out_path, "wb");
if (fo) {
if (fwrite(plain, 1, plain_len, fo) == plain_len)
fprintf(stdout, "Decrypted file written to: %s\n", out_path);
fclose(fo);
}
EVP_CIPHER_CTX_free(ctx);
munmap(dump, dump_size);
free(plain);
free(veg);
return 0;
}
fprintf(stderr, "No key found.\n");
EVP_CIPHER_CTX_free(ctx);
munmap(dump, dump_size);
free(plain);
free(veg);
return 1;
}
Running it gives us our decrypted file, and the flag!
./brute_decrypt_from_dump memdump.raw encrypted_files/Projects/UAP/UAP_SAUCER_HARDWARE_SPEC__Sophia_Walker__rev3.pdf.veg