Skip to content

Notes on Correctness

Daniel Roethlisberger edited this page Aug 18, 2018 · 2 revisions

Quick links:


Script shebang and interpreter handling

Consider a script file hello.sh:

#!/bin/sh
echo 'Hello world!'

Running sh ./hello.sh will lead to a execve("/bin/sh") syscall, while running it as ./hello.sh will lead to a execve("./hello.sh") syscall. However, in both cases, the executable image that is activated in the process is actually the Bourne shell interpreter:

90199 ttys004    0:00.00 /bin/sh ./script.sh
xnumon
For consistency, xnumon always shows the actually loaded Mach-O executable as image object in image exec events, subject image and ancestry, in this case the interpreter /bin/sh. For script executions, where the kernel gets to see the script in the syscall and loads the interpreter from the shebang, xnumon will additionally show the script file as script object in image exec events, subject image and ancestry.

Reparenting (ppid at exec != pid at fork)

Consider ppid.c:

int main() {
    printf("parent pid at fork: %i\n", getpid());
    if (fork() == 0) {
        sleep(1);
        printf("child ppid at exec: %i\n", getppid());
        execl("/bin/sh", "sh", "-c", "# malicious script", 0);
    }
}

Running this will reveal that the parent's pid at fork is not the same as the child's ppid at exec:

% ./ppid
parent pid at fork: 12345
child ppid at exec: 1

The reason for this is reparenting. The parent exits before the child calls exec, therefore the child is reparented by the kernel to pid 1, which is launchd.

xnumon
In order to provide accurate executable image relationship information, xnumon therefore tracks fork(2) and exec(2) syscalls and only resorts to using the ppid for processes whose exec was missed or happened before xnumon started. Process information acquired from the live system is marked in image exec events with a "reconstructed": true field.

Short-living executable images on disk

macOS allows deletion of image files while they are being executed in a process:

int main(int argc, char *argv[]) {
        unlink(argv[0]);
        /* do nefarious deeds */

unlink(2) only removes the directory entry, not the actual file content, while vfs still has an active vnode from execution. The inodes containing the executable file will be marked free only after the process terminates and the executable image is unmapped from memory. However, the executable file will not be accessible after unlinking because it does not have any directory entry anymore.

macOS also allows modifications to image files while they are being executed in a process:

int main(int argc, char *argv[]) {
        long r = random();
        int fd = open(argv[0], O_RDWR);
        pwrite(fd, &r, sizeof(r), 0x1000);
        close(fd);
        /* do nefarious deeds */

This is potentially unsafe but very possible behaviour on macOS. Mapped memory from a file is conceptually like a cache for the file content.

Both of these possible behaviours lead to the requirement of acquiring the image file as it presents itself during exec and not at some point later on.

xnumon
To counter unlinking, xnumon makes sure to open a file descriptor on the executable image as quickly as possible; this allows the acquisition of hashes later on even if the file was unlinked in the meantime. To counter self-modification, xnumon tries hard to detect self-modification and discard hashes and code signature information if the executable image on disk was modified after execution. Even without kext, there should never be any wrong information logged. However, self-modification and unlinking can lead to missing information. With the kext and an appropriate kextlevel configuration, xnumon can delay the execution of processes until e.g. the file was opened (open) or hashes were acquired (hash).