sct_config Utility Syntax and Usage Guide

TOC

  1. Making a New Rule
  2. Filter Expressions
    1. Writing Filter Expressions
    2. Filter Expression Variables
    3. Filter Expression Operators and Operator Precedence
    4. Operands
    5. Filter Expression Examples
  3. Action to Take
  4. Specifying 'before' and 'after' Rules
  5. Grand Example
  6. Using Constants and Macros in Filter Expressions
  7. Handling 'struct' System Call Parameters
    1. Matching 'struct' System Call Parameters
    2. Matching Polymorphic 'struct' Parameters
    3. Logging Polymorphic 'struct' Parameters
  8. Defining log format
  9. Using 'sct_config'

Making a New Rule

In order to make a new rule we must first declare it:


rule {
As you can probably guess, this declares a new rule.

Afterwards there's a need to specify the syscall to match. For example:


syscall_name = unlink
This will inform syscalltrack to hijack the 'unlink' system call and track all of its invocations.

As of this writing the following syscalls are supported by syscalltrack: open, link, unlink, chdir, chmod, access, kill, rmdir, mkdir.

The file syscalls.dat in the sct_rules_module subdirectory contains the up to date list of system calls and their parameters. For the full list (including system calls not supported currently) see your kernel sources.

Every rule must have a name (which should probably be unique, but doesn't have to be). For example:


rule_name = unlink_rule1
So, if we sum it up so far we get:

rule
{
	syscall_name = unlink
	rule_name = unlink_rule1
}
So now we have a new rule, which matches 'unlink' system calls.

This is all nice and fancy, but what if we want to spy against system calls according to specific criteria, for example if someone removes the file /etc/passwd ? Read on to find out.

Note: the above rule is not yet complete in another aspect, since an action must be set as well.


Filter Expressions


Writing Filter Expressions

The preferred way to specify which system call invocations to match are by writing 'filter expressions'. To write a filter expression, all you need to do is add a 'filter_expression' directive to the configuration file, and write the filter expression itself, like this:


rule 
{
	...
	filter_expression { PARAMS[1]=="passwd" }
	...
}
This filter will match all invocations of the system call we specified earlier (using the 'syscall_name' directive) where the first parameter is the string 'passwd'.

A basic filter expression has three parts:

A regular filter expression is made of other filter expressions with operators between them, for example:

(PARAMS[2] ~= "string") && (UID == 0 && PID > 100)


Filter Expression Variables

There are two types of filter expression variables:

  1. A system call parameter variable, such as PARAMS[1], and
  2. A process parameter variable, such as UID, PID, COMM and others.
When referring to system call parameter variables, we use the PARAMS array. The first cell in the array, PARAMS[1], refers to the first system call parameter, the second cell in the array, PARAMS[2] refers to the second system call parameter, and so on. To see how to handle system call parameters which are struct, please see section "Matching 'struct' system call parameters".

When referring to process field variables, syscalltrack currently recognizes the following variables:

  • PID - The process id of the process which called the system call.
  • UID - The user id of the owner of the process - the user who executed it.
  • GID - The group id of the owner of the process.
  • EUID - The effective user id of the owner of the process.
  • EGID - The effective group id of the owner of the process.
  • SUID - The saved user id of the owner of the process.
  • SGID - The saved group id of the owner of the process.
  • COMM - The command which was issued (the process command line).

  • Filter Expression Operators and Operator Precedence

    The following operators are supported:

    ==
    equal,
    <
    less than,
    >
    greater than,
    <=
    less or equal than
    >=
    greater or equal than
    !=
    not equal
    +
    binary plus
    -
    binary and unary minus
    <<
    bit shift left
    >>
    bit shift right
    &
    bitwise and
    |
    bitwise or
    ^
    bitwise xor
    &&
    logical and
    ||
    logical or
    ~
    bitwise not
    !
    logical not
    ~=
    string pattern match.
    The operator precedence rules are the same as in the C language, as specified in Kernighan and Ritchie's The C Programming Language. You can use parenthesis to change precedence.


    Operands

    Numerical operands are specified as numbers. Numbers prefixed with '0x' are considered to be in base 16 (hex), like "0x8fd2". Numbers prefixed with '0' are considered to be base 8 (octal), like "0100".

    String operands should be quoted, "like this".

    sct_config tries to deduce the correct type of the operand, based on the parameter you requested a match to. If it fails, a (descriptive, we hope) error message is produced.


    Filter Expression Examples

    
    rule
    {
        syscall_name = settimeofday
        rule_name = zero_settimeofday
        filter_expression 
        {
            PARAMS[2].tz_minuteswest == 0 && PID > 100 && COMM ~= "clock"
        }
        action {
    	type = LOG
        }
    }
    

    Action to Take

    When the rule matches, the syscall tracker kernel module will perform a certain action. Possible actions to take include: LOG, SUSPEND, FAIL, KILL.

    Only LOG and FAIL actions are supported at the moment. In order to declare an action you write, for example:

    
    action{
    	type = LOG
    	/* optional: */
    	log_format {syscall: %pid[%comm]}
    	priority = 4
    }
    
    or
    
    action {
           type = FAIL
           error_code = -22
    }
    
    When specifying a LOG action, you can also specify a 'log_format' and 'priority' for this action (although 'priority' is not currently used). More on the log format in "Defining log format".

    When specifying a FAIL action, you need to specify the return value from the system call invocation. This should be a negative integer, as the kernel always returns negative error codes to glibc. A full list of error codes can be found in /usr/include/asm/errno.h. In the future, we intend to support symbolic ERRNO constants here.


    Specifying 'before' and 'after' Rules

    Rules may be checked either just before a system call is invoked, or just after it was invoked, before returning to the caller. The 'before' rules are useful especially if the tracked system call is going to block for a long time (e.g. a socket call that waits for a client to connect may block for minutes, hours, days...).

    The 'after' rules are useful for testing or logging the return value of a system call (e.g. we want to log all invocations of the 'open' calls of a restricted file, that actually succeed, i.e. have a return value which is NOT -1). The 'after' rules may also be used to track the contents of 'return' parameters (i.e. parameters which are set by the system call, and their value is sent back to the caller for examining. For example, the 'read' system call modifies a buffer that the user can later examine).

    In order to specify whether a rule is a 'before' or an 'after' rule, the 'when' keyword may be used. For a 'before' rule:

    
    when = before
    
    for an 'after' rule:
    
    when = after
    
    The return value of a system call may be accessed (for filtering) only in an 'after' rule, using the variable name 'VT_RETVAL'.


    Grand Example

    After covering all the features, it's now time to put together our unlink example. So here goes:

    
    rule
    {
    	syscall_name = unlink
    	rule_name = unlink_rule1
    	filter_expression {PARAMS[1]=="/etc/passwd" && UID == 0}
    	action {
    	       TYPE = LOG
    	}
    	when = before
    }
    
    This rule will log every attempt to remove /etc/passwd by the root user.


    Using Constants and Macros in Filter Expressions

    Until now, we specified data for fields using raw values - numbers and strings. Often, however, we know this data as symbols. For example, we think of user 'root', not of UID '0'. In order to make the config file more readable, 'sct_config' supports various types of macros. These macros accept some parameter in a readable format, and translate it into the raw data.

    The following macros are currently supported:

    usernametoid("root")
    translates user name (string) to UID.
    groupnametoid("wheel")
    translates group name to GID.
    ipaddr("127.0.0.1")
    translates an IP address (string) to an unsigned long in network byte-order.
    htons(7)
    host to network byte-order for 'short' numbers (useful for various socket syscalls, as will be demonstrated below).
    As an example for using these macros, look at the following rule:
    
    rule {
    .
    .
      filter_expression { UID == usernametoid("root") }
    .
    .
    }
    
    Clearly, this is more readable then comparing 'UID' to '0'. The other macros may be used in a similar manner.

    Constants are a special type of macros, that have no parameters. They are widely used when passing parameters to system calls, to enhance readability. For example, the 'open' system call has a 'flags' parameter, that may be a combination of various options, such as 'O_RDWR', 'O_EXCL', and so on. 'sct_config' will understand these constants, and translate them to their numeric values. For example, in order to check if 'open' was called with the 'O_EXCL' flag, either of the following filter expressions would do:

    
      filter_expression { PARAMS[2] & O_EXCL }
    
    or
    
      filter_expression { PARAMS[2] & 0200 }
    
    Note: the second variation usage an octal number (note the '0' prefix). This value was taken from /usr/include/bits/fcntl.h Read the man page for the 'open' syscall, to find the list of supported flags.

    Note 2: we use the fact that numeric values of filter expressions are automatically cast to boolean values (non-0 values are considered to be 'true', while '0' is considered to be 'false').


    Handling 'struct' System Call Parameters


    Matching 'struct' System Call Parameters

    Some system calls accept pointer to structs as parameters. You can match against fields of these structs. To do this for filter expressions, instead of specifying 'PARAMS[index]', you specify 'PARAMS[index].struct_field_name'. For example, suppose that we want to match any call to 'settimeofday', in which the time-zone is GMT (Greenwich Mean Time). According to 'man settimeofday', the time zone parameter (2nd parameter) of this syscall is of type 'const struct timezone'. This type is defined like this:

    
    	struct timezone {
    		int  tz_minuteswest; /* minutes W of Greenwich */
    		int  tz_dsttime;     /* type of dst correction */
    	};
    
    'GMT' is defined by having the 'tz_minuteswest' equal zero, so we should use the following filter expression:
    
        filter_expression { PARAMS[2].tz_minuteswest == 0 }
    


    Matching Polymorphic 'struct' Parameters

    Some system calls accept polymorphic 'struct' parameters. By that, we mean that the system call's definition has a given struct type, but when actually invoking the syscall, the user passes a different struct type, depending on context (i.e. other parameters).

    An example for such system calls are the socket calls 'bind', 'accept' and 'connect'. These calls are supposed to handle various types of address families - each of which uses a different type of address struct. These system calls are defined to accept a 'struct sockaddr' parameter, defined as follows:

    
    struct sockaddr {
            sa_family_t     sa_family;      /* address family, AF_xxx       */
            char            sa_data[14];    /* 14 bytes of protocol address */
    };
    
    This is a generic structure, which is supposed to be overlay-ed by a address-family specific structure. For example, when using 'bind' with an IP protocol, the struct passed is actually of type 'struct sockaddr_in', defined as follows:
    
    struct sockaddr_in {
            sa_family_t sin_family;
            uint16_t sin_port;                  /* Port number.  */
            struct in_addr sin_addr;            /* Internet address.  */
    
            /* Pad to size of `struct sockaddr'.  */
            unsigned char sin_zero[sizeof (struct sockaddr) -
                                   __SOCKADDR_COMMON_SIZE -
                                   sizeof (uint16_t) -
                                   sizeof (struct in_addr)];
    };
    
    The system call knows that because the 'sa_family' field of the 'sockaddr' struct is set to 'AF_INET' (which is '2').

    When we write a rule that deals with the 'bind' system call, and want to match binding to specific IP addresses, or TCP ports, we need to use a type-cast operator in the rule. We do that by adding the struct type to the parameter's index, as follows:

    
    rule {
      syscall_name = connect
    .
    .
      filter_expression {
        PARAMS[2].sa_family == 2 && PARAMS[2.sockaddr_in].sin_port == htons(7)
      }
    .
    .
    }
    
    As you can see, we first check that the 'sa_family' member of the address struct (second parameter of the 'bind' system call) is '2' ('AF_INET') and then we perform a type-cast to 'sockaddr_in' to compare the port of the address to '7'. Note that we also use the 'htons' macro here, since the port in a 'struct sockaddr_in' is stored in network byte order, not in host byte order.


    Logging Polymorphic 'struct' Parameters

    After we had matched a rule, and know that some struct parameter should be treated as a different struct type, we may wish to use this struct type when logging the syscall invocation. In order to do that, we need to set a special attribute for this parameter, in the 'action' part of the rule, as follows:

    
    rule {
    .
    .
      action {
        type = LOG
        set_param_attr {
          attr_param = 2
          attr_name = var_dyn_type
          attr_val = "sockaddr_in"
        }
      }
    .
    .
    }
    
    This verbose syntax (which will be simplified in future versions) says that as part of the action, the 'var_dyn_type' attribute of the 2nd parameter of the syscall, will be set to "sockaddr_in". This attribute is relevant only for struct parameters, and specifies which type to use when logging them.

    To conclude, a rule that will log any calls to the 'bind' system call with port number '7' would look like this:

    
    rule
    {
      syscall_name = bind
      rule_name = bind_port7_rule
      filter_expression {
        PARAMS[2].sa_family == 2 && PARAMS[2.sockaddr_in].sin_port == htons(7)
      }
      action {
        type = LOG
        set_param_attr {
          attr_param = 2
          attr_name = var_dyn_type
          attr_val = "sockaddr_in"
        }
      }
    }
    
    Or if we wish to log invoctions of the 'connect' syscall, that attempt to connect to the 'localhost' address (127.0.0.1), we will use this rule:
    
    rule
    {
      syscall_name = connect
      rule_name = connect_localhost_rule
      filter_expression {
        PARAMS[2].sa_family == 2 && PARAMS[2.sockaddr_in].sin_addr.s_addr == ipaddr("127.0.0.1")
      }
      action {
        type = LOG
        set_param_attr {
          attr_param = 2
          attr_name = var_dyn_type
          attr_val = "sockaddr_in"
        }
      }
    }
    
    Note: the above works, since the 'sin_addr' field of 'struct sockaddr_in'> is of type 'struct in_addr', defined as follows:
    
    struct in_addr
    {
        uint32_t s_addr;
    };
    
    And the 'ipaddr' macro generates compatible data for this 's_addr' field.


    Defining log format

    In order to define a logging format for logging matched system call invocations, you can use the optional 'log_format' directive. This directive can come on its own in the configuration file, where it defines a "default" log format (possibly a different default for 'before' and 'after' rules), or in a 'LOG action' clause, where it defines the log format for that action. Let us see an example:

    
    log_format 
    {
    	default {syscall: %pid[%comm]: %sid_%sname(%params) (rule %ruleid)}
    }
    
    and another example:
    
    log_format 
    {
    	before {syscall: %pid[%comm]}
    	after {syscall: %pid[%comm] returned %retval}
    }
    
    and here's a log format just for this action clause:
    
    rule {
         ...
         action { 
             type = LOG
    	 log_format {syscall foo: %pid}
        }
    }
    
    The log format is a string, enclosed between '{' and '}' braces, that may contain macros. There are three log formats you can specify, namely 'before', 'after' and 'default'. The 'before' format is used for logging system calls which matched a 'before' rule. The 'after' format is used for logging system calls which matched an 'after' rule. The 'default' format is used for both. It is only allowed to specify both 'before' and 'after' formats, or a single 'default' format. Each log format is composed of several macros.

    Macros are alphanumeric strings prefixed by an '%' sign. Each macro will be replaced by some value, based on the system call's context and the process in which it was invoked. The following macros are currently supported:

    %ruleid
    the numeric id of the rule that matched the syscall.
    %sid
    the numeric ID of the matched syscall.
    %sname
    the name of the matched syscall.
    %params
    the parameters of the syscall.
    %pid
    the ID of the process invoking the syscall.
    %uid
    the ID of the user running this process.
    %euid
    the effective ID of the user running this process.
    %suid
    the saved ID of the user running this process.
    %gid
    the ID of the group running this process.
    %egid
    the effective ID of the group running this process.
    %sgid
    the saved ID of the group running this process.
    %comm
    the name of the command this process is executing.
    %retval
    the return value of a system call. May be only used in an 'after' log format directive.
    Remember that the 'log_format' directive is optional - a config file is still valid without this, and in that case, the module will use its default logging format.


    Using 'sct_config'

    After writing the configuration file, you need inform the kernel that the configuration has changed. The 'sct_config' utility does just that. It reads the configuration file, and passes the rules to the kernel module.

    sct_config has several options:

    ./sct_config print
    Prints the current kernel configuration - all currently registered rules, filters and actions, for all system calls.
    ./sct_config delete
    Deletes all currently registered rules in the kernel.
    ./sct_config upload [file name]
    Parses the configuration file 'file name'. If the configuration is correct, uploads the new rules to the kernel module. If no file name is given, uses the file 'sct_example.conf' in the current working directory.
    ./sct_config check [file name]
    Parses and prints all rules in the configuration file 'file name', but doesn't upload them to the kernel module. Use this option to check your configuration before uploading it to the kernel. If no file name is given, uses the file 'sct_example.conf' in the current working directory.
    ./sct_config count
    Prints to stdout the total number of rules currently defined in the kernel. Use this, for example, to find out if there are any rules defined.
    ./sct_config download
    This command will query the kernel for all of the currently registered rules, and return them to userspace, where sct_config will print them to standard output. Use this command to know which rules are registered.


    Originally Written by: Eli Shemer.