The Orchids language

Orchids works by detecting patterns, possibly quite complex, in flows of incoming events.  Those patterns are described in rules, typically loaded by the $OCONF/orchids-rules.conf configuration file. In turn, those rules must be written in the Orchids language, described below.


A typical example of a rule would be the following, known as the pid tracker ($OCONF/rules/pid_tracker_audit.rule):

#include "linux64syscall.h"

rule pidtrack synchronize($pid)
  state init
    expect (.auditd.syscall==SYS_clone ||
	    .auditd.syscall==SYS_fork ||
      goto newpid;

  state newpid! {
    $pid = .auditd.exit;
    $uid = .auditd.euid;
    $gid = .auditd.egid;

    case ($uid==0) goto end; /* everything root does is OK */
    else goto wait;

  state wait!
    expect ( == $pid &&
	    .auditd.syscall == SYS_execve &&
	    (.auditd.uid != .auditd.euid || .auditd.gid != .auditd.egid) && /* hack */
	    .auditd.success == "yes")
      goto update_uid_gid;

    expect ( == $pid &&
	    (.auditd.syscall == SYS_setresuid ||
	     .auditd.syscall == SYS_setreuid ||
	     .auditd.syscall == SYS_setuid) &&
	    .auditd.success == "yes")
      goto update_setuid;

    expect ( == $pid &&
	    (.auditd.syscall == SYS_setresgid ||
	     .auditd.syscall == SYS_setregid ||
	     .auditd.syscall == SYS_setgid) &&
	    .auditd.success == "yes")
      goto update_setgid;

    expect ( == $pid &&
	    .auditd.syscall == SYS_exit)
      goto end;

    expect (.auditd.syscall == SYS_kill &&
	    .auditd.a0 == $pid &&
	    .auditd.a1 == 9)
      goto end;

    expect ( == $pid &&
	    (.auditd.euid != $uid || .auditd.egid != $gid))
      goto alert;

  state update_uid_gid!
    $uid = .auditd.euid;
    $gid = .auditd.egid;

    case ($uid==0) goto end; /* everything root does is OK */
    else goto wait;

  state update_setuid!
    case (.auditd.egid != $gid) goto alert;
    else goto update_uid_gid;

  state update_setgid!
    case (.auditd.euid != $uid) goto alert;
    else goto update_uid_gid;

  state alert!
    $newuid = .auditd.euid;
    $newgid = .auditd.egid;
    print_string ("Alert report :\n");

    print_string ("Privilege escalation by uid=");
    print_string (str_from_uint($uid));
    print_string (", now ");
    print_string (str_from_uint($newuid));
    print_string (", gid=");
    print_string (str_from_uint($gid));
    print_string (", now ");
    print_string (str_from_uint($newgid));
    print_string (", pid=");
    print_string (str_from_uint($pid));
    print_string ("\n");

  state end!
    /* all went well */

This reads, roughly, as follows.  We assume that Orchids reads from a source of events provided by the Linux auditd daemon, configured thanks to the auditd module.  Each new event received will have Orchids launch a new thread for rule pidtrack, starting at state init. This expects to see a Linux system call that creates a new process.

Once such an Orchids thread has found one, it goes to state newpid. There is stores the pid of the created process, which happens to be the exit code .auditd.exit of the fork or clone primitive, into a local variable $pid. It also stores the current effective user id and group id into appropriate variables. If the effective user id is not root, it then goes to the main state of that rule, wait.

When in the wait state, the Orchids thread will wait upon one of 6 possible kinds of events, each described by an expect clause. For example, the first one expects the monitored process (with pid stored in $pid) to call the Linux primitive execve() with a modified effective user id or a modified group id. This is legitimate, and happens in case we execute processes with the setuid or setgid bit set. If that happens, that thread will go to state update_uid_gid, which will update the values of the variables $uid and $gid that hold our current view of what the current effective user id and group id of process $pid are.

The next 4 expect clauses describe other legitimate cases where the effective user id or the group id of process $pid changes.

The final expect clause detects cases where a system call has been made with unexpected effective user id or group id. In that case, the Orchids thread goes to state alert, which prints a message and calls report() to produce alert reports in a variety of formats.

If  a state, such as wait, has several expect clauses, then it will wait on all of them.  The most effective way to understand what it does is to imagine that Orchids will fork as many subthreads as there are expect clauses, each one waiting on an event satisfying the boolean condition immediately following the expect keyword.   Once one is found, the Orchids thread goes to the state mentioned after the goto keyword.

States marked with a ! sign are commit states.  When you enter such a state, all the other subthreads waiting on alternative expect conditions are killed, and only the current one is kept.  Let us be more precise.  Each thread takes part in a thread group.  For each new event read, for each rule whose first state expects a condition that matches that event, a new thread group is created, with just one thread in it.  When a thread reaches a state with multiple expect clauses, it forks as many subthreads as there are expect clauses.  Those subthreads are in the same thread group as the parent thread.   (As a special case, any state with no expect will simply fork no subthread, and terminate.  This is what states alert and end do.) When a thread reaches a commit state, it first kills all the other threads that are in the same thread group: it commits to the path of events that it has just found, and disregards all other candidate paths.

Finally, our example rule exhibits another peculiar feature: the synchronized($pid) phrase after the rule name, right at the beginning. That instructs Orchids to only keep one thread group, among all with the same rule, whose threads will have the same value of $pid. In other words, imagine Orchids starts monitoring events through the pidtrack rule and discovers that $pid should be set to, say, 102 (when it sets it, in state newpid).  Imagine it already had an active thread group that already monitors pid number 102.  Then Orchids will kill the newer thread group, and only keep the older one.


The grammar of the Orchids language is given in Backus-Naur Form, with some standard extensions.  Tokens are given in typewriter script between single quotes, e.g., ‘rule‘.  Productions are given in italic, e.g., rule.  A star means repetition of zero, one or more items, while a plus sign means repetition of one of more items.  Parenthesis are used to group symbols or productions.  Square brackets denote optional sequences.


orchids-file ::= rule*

rule ::= ‘rule‘ SYMNAME [‘synchronize‘  ‘(‘  sync_var_list  ‘)‘ ]   ‘{‘  state+  ‘}

sync_var_list ::= VARIABLE (‘,‘ VARIABLE)*

state ::= ‘state‘ SYMNAME ‘ state_option* {‘  statement* actions ‘}

state_option ::=  ‘!

statement ::= expr;| conditional | split-regexp
conditional ::= ‘ifexprthenstatement [‘elsestatement] ‘;
split-regexp ::= ‘split‘ STRING ‘/optional_var_list/expr;

actions ::= transition* | cases

optional_var_list ::=

transition ::=
expect‘ ‘(expr)‘ ‘goto‘ SYMNAME
| ‘expect‘ ‘(expr)‘ ‘{statement* actions ‘}

cases ::= [‘case‘ ‘(‘ expr ‘)‘ ‘goto‘ SYMNAME ‘;‘ ‘else‘]* ‘goto‘ SYMNAME


SYMNAME:           [az_][AZaz09_.]*
used for rule names, state names, function names

VARIABLE:           $[AZaz_][AZaz09_.]*
variable names; note that they start with ‘$

LOGICAL:              [AZ][AZaz09_.]*
logical variable names, as used in database comprehensions; can also be used just like variables, but cannot be assigned to

string constant, enclosed in double quotes, as in C

  • newlines are forbidden—use \n to insert one
  • octal codes \[07]{1,3} allowed (including \0)
  • other special characters: \n (newline), \t (tab), \r (carriage return), \b (backspace), \f (form feed)
  • backslash otherwise escapes following character, as in \"


Other tokens are described on the relevant pages, notably that of expressions.  For example, field names start with a dot, and are of the form .module.field; precisely, they obey the regular expression .[AZaz09_]+.[AZaz09_.]*