nark: Canaries for Linux
Thinkst Canary is an interesting security product which allows defenders to determine when their infrastructure is compromised by installing realistic looking “canary” hardware/software on their networks. When an attacker attempts to access this canary, it will immediately alert the defenders. Theoretically this should have no false positives because no legitimate user should know the system exists on the network, let alone try to access it, but an attacker scanning the network trying to gain access to more servers and data will likely attempt to investigate it. They support all different types of hardware and software: Windows file servers, routers, API keys, you name it.
In the backend, they use DNS tunneling to send data in and out of the network [1]. This is useful because DNS is unlikely to be blocked at the network layer and it is supported by basically every network. You can achieve encrypted data transfer using base32’d data in the DNS queries.
I thought it would be an interesting idea to extend the canary concept to a single Linux system by monitoring file accesses. If an attacker were to get on the system, they would likely prod around and look for keys, wallets, or API tokens. Thinkst’s canaries seem to have some support for honeypot files, though they rely on the attacker actually using the file’s content. For example, they have support for fake AWS keys but that will alert only when the keys are used.
I thought I could do a bit better: Linux has eBPF, which means I can monitor all file accesses. I could plant a bunch of canary files and watch to see if they are ever accessed - juicy-looking files like:
/root/.ssh/id_rsa/home/user/important_project/.env.production/home/user/shop/customer_credit_card.db/home/user/.aws/credentials/home/user/.wallet.dat
Obviously you would want to tailor this list of files specifically to your machine. Don’t include the AWS one if you actually use AWS credentials or else you will get a bunch of false positive alerts.
SEC("lsm/file_open")
int BPF_PROG(canary_file_open, struct file* file) {
__u64 inode = BPF_CORE_READ(file, f_inode, i_ino);
__u8* mask = bpf_map_lookup_elem(&target_inodes, &inode);
if (!mask || !(*mask & 1)) {
return 0;
}
struct event* e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e) {
return 0;
}
e->inode = inode;
e->pid = bpf_get_current_pid_tgid();
e->uid = bpf_get_current_uid_gid();
e->op = 1;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0);
return 0;
}
The eBPF code is super simple: watch lsm/file_open and check if the passed-in inode matches a file you requested monitoring for. If it matches, someone has tried to access your canary! eBPF code running in the kernel is somewhat limited, so there needs to be a separate user-space daemon running to handle the sending of alert notifications. I used a BPF_MAP_TYPE_RINGBUF map to pass a structure from the kernel to the daemon which has information about who attempted the access, the parent PID, the name of the file, etc. For sending the notifications, I just set up a simple ntfy.sh topic and use libcurl to send a request from the daemon because DNS tunneling felt a bit overkill.
I named the project nark. You can configure the list of files to monitor, the name of the daemon in the process list, and the URL to fetch when an alert triggers.
You can check out the project on GitHub.
[1] https://help.canary.tools/hc/en-gb/articles/360002425837-What-is-DNS-tunnelling