TT Lab
Get started
Learn Learning paths Courses

Cron Ran curl at Three in the Morning

Catch it at the syscall and it cannot lie

Continue in TT Lab

In one line

Falco is a runtime detection engine that receives syscalls from the kernel and matches them against rules. One rule consists of five items, rule, desc, condition, output, and priority, and repeated conditions are pulled out into a macro and lists of values into a list for reuse.

Why you need this

Log-based detection has one structural limit. A log contains only what the program decided to write. If an intruder moves in a way that leaves no logs — for example, dropping a binary into /tmp and running it immediately — no application records it.

Syscalls are different. Whether you create a process, open a file, or connect a socket, you have to go through the kernel, and you cannot deceive the kernel. So as a place to ask "what actually happened," syscalls are the lowest layer. Kernel audit (auditd) also looks at the same place, but where auditd is a recording device that decides rule by rule whether to leave a record, Falco is a judging device that evaluates conditions and decides whether to raise an alert right now. The two are not substitutes but a pair.

How it works

The basic elements of a rule are summarized as follows. The required ones are five: rule (a short, unique name), desc (what it catches), condition (a filter expression to apply to events), output (the sentence to emit on a match), and priority (severity), and the optional ones are enabled, tags, source, and exceptions. There are eight values you can use for priority — EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFORMATIONAL, and DEBUG.

- list: outbound_clients
  items: [curl, wget, nc]

- macro: from_scheduler
  condition: (proc.aname[2]=cron or proc.aname[3]=cron)

- rule: Scheduled job made an outbound request
  desc: 예약 작업 밑에서 외부 요청 도구가 실행됐다
  condition: spawned_process and proc.name in (outbound_clients) and from_scheduler
  output: "예약 작업이 바깥으로 (user=%user.name proc=%proc.name cmd=%proc.cmdline)"
  priority: WARNING
  tags: [labhub, cron]

list and macro have an ordering constraint. Both can refer only to things defined before themselves. So it becomes a habit to write in a file in the order list → macro → rule. A condition that appears as a bare name, like spawned_process, is one of the default macros that Falco ships with. When I measured it on this lab VM, giving only my own rule file to falco --validate failed with Undefined macro 'spawned_process', and it passed only when I also gave the default rules file — which means validation has to be done under the same conditions as loading.

The field names usable in conditions are in the supported fields list. What is especially useful in the process family is the ancestor name. proc.pname is just the one parent, but proc.aname[2] points to the grandparent. In the structure where cron starts a shell and that shell calls curl, the parent is sh, so you have to look at ancestors to say "it came from a scheduled job."

Where alerts go is decided by the output settings. The default is standard output, so under systemd it goes into the journal, but if you want a machine to count them in the morning, it is better to switch to JSON and drop them into a file too. Placeholders such as %proc.cmdline inside the output sentence are listed in the output format documentation.

The place to reduce false positives is exceptions (exceptions documentation). If you write the fields to look at in fields, the comparison methods in comps, and the bundles of values in values, Falco appends them after the condition as and not (…). The result is similar to editing the condition string directly, but two things differ — the exception is separated from the rule, so you can later change only the values by overriding, and what was removed and why is visible.

What it looks like in the field

The most common failure is melting an exception into the condition. If you attach and not proc.cmdline contains backup to the end of the condition, six months later nobody knows what that fragment was for. And an exception attached that way is usually too broad — every command that contains the word backup goes quiet.

The second is turning a rule on and not looking at the output. If you leave the default settings on this lab VM, alerts flow only to the journal, and when the VM disappears they disappear with it. Detection whose alerts do not go to a file or a collector gives only the reassurance that "it is running."

The third is priority inflation. If you emit every rule as CRITICAL, the grade stops being information. Separating what should wake a person at dawn from what can be gathered and reviewed in the morning is half of writing rules.

What you will do in the next lab

In the lab you write a rule yourself on a VM with Falco installed. You split the list and the macro, turn the output into a JSON file, actually plant the incident in the title (a scheduled job makes a request outside every minute), and confirm in the file that the rule catches it. Then you create one more normal job that matches the same rule, remove only that one with exceptions, and confirm again that the bad one is still caught. Syscall instrumentation is a kernel feature, so this lab also runs in a VM.