'before'
and 'after'
Rules
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.
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:
PARAMS[2]
, UID
,
PID
.
'=='
, '~='
,
'<'
and '!'
.
(PARAMS[2] ~= "string") && (UID == 0 && PID > 100)
There are two types of filter expression variables:
PARAMS[1]
, and
UID
, PID
,
COMM
and others.
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).
The following operators are supported:
==
<
>
<=
>=
!=
+
-
<<
>>
&
|
^
&&
||
~
!
~=
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.
rule
{
syscall_name = settimeofday
rule_name = zero_settimeofday
filter_expression
{
PARAMS[2].tz_minuteswest == 0 && PID > 100 && COMM ~= "clock"
}
action {
type = LOG
}
}
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
}
or
action {
type = KILL
}
or
action {
type = SUSPEND
}
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 'kill' or 'suspend' action, you can also specify a PID
("pid = 1000") to perform the action on. The default is the current
process, that is, the process that executed the system call.
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.
'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'
.
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.
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")
UID
.
groupnametoid("wheel")
GID
.
ipaddr("127.0.0.1")
htons(7)
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.
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 }
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.
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.