I had a colleague, who was working on detecting malicious code in AI models (PyTorch was saving model weights using Python's pickle module - a bad idea).
He was working on static analysis of pickle bytecode. As he was leaving the company and was doing a knowledge transfer with me, he suggested that "dynamic analysis" of the code would be a good idea. Static analysis had its share of false positives, but it was false negatives we were concerned with. Running the model and observing whether it did anything suspicious was a good way of getting a definitive answer whether it contained malicious code or not. No ML model should ever try to access http://definitely-not-a-malware.com while loading.
His original idea was to simply run PyTorch in an isolated container under strace and analyze the logs.
As I started thinking about this, it was clear that managing containers, starting and restarting them was a good solution for a POC, but not for production - it was just too slow.
As it often happens, I decided to make my own strace with blackjack and hookers. My idea was "why log it if we can stop it right there?" I'd write my own strace, which could intercept syscalls, analyze them and deny their execution if they did not match the policy. I thought it was brilliant. Forgive me, for I had no idea about the Linux kernel, system programming, or eBPF.
So one weekend I decided that I should try to write it. I started with Go, because I knew it and it was a "system" programming language. By the second day I realized that Go didn't fit the bill. It was easy to run ptrace, trace syscalls, but as soon as the program cloned or started a new thread – all was lost. The Go runtime just didn't allow some signals to pass down to my code.
So I decided to rewrite it in Rust. My lack of knowledge in syscalls, ptrace, signals, and other Linux internals was now compounded by a lack of Rust knowledge.
Nevertheless, a couple of months later I had my litterbox - a kind of sandbox for shitty applications. It does what I set out to do: log all syscalls, analyze their parameters, and keep enough context to know that reading from file descriptor 254 will actually read $HOME/.aws/credentials, or that connecting to socket 55 will reach a server outside the private network — and block those calls.
By that time, though, I knew that this was not the right approach, so I shelved it.
The source code is on GitHub and I don't really recommend using it as a sandbox.
My further shenanigans with Linux programming involved exploring kernel namespaces and writing a container runtime — a journey I covered in my blog.