Thursday, April 13, 2017

Writing a basic audispd plugin

In the last post we looked at how to write a basic auparse program. The program was suitable for searching historical records but was not up to par for realtime analysis. In this post we will look at the changes needed to make a program ready for realtime.

Realtime in no time
The audit system can transfer events in realtime to a dispatcher program. This program, audispd, can be thought of as a multiplexor. It takes one event record at a time and gives it to the plugins.

The plugins have to be designed a certain way.

  • They have to read events from stdin
  • They have to handle SIGHUP and SIGTERM
  • They must dequeue events as fast as possible
  • They must provide a plugin config file so that audispd knows how to start it.

The plugin config file has a specific format:

active = no
direction = out
path = /sbin/audisp-example
type = always
args = test
format = string

You can tell audispd if the plugin should be started by setting active to yes. The direction should always be out. (In the past there was the idea of being able to chain plugins together to transform events. But support for that has not become a priority.) Then you tell audispd what the path is to your plugin. You can tell it if the plugin type should always be running or its a builtin plugin. Since we are making our own, it should be always. You can pass up to 2 arguments. They are hard coded meaning that audispd does not do any kind of lookup but instead passes exactly what is on this line. It can be used to point to the location of a config file or anything else you can think up. And lastly, you can tell audispd what format the plugin expects its events to be in. There are 2 formats, the native audit structure and string. For any program that is designed to use auparse, always tell it string.

The design requirement for audispd plugins to read from stdin also provides the way to debug your application. During the development you may need to test different things. But if its running under a daemon how do you see what its doing?

The way I debug audispd programs is to capture some audit events in the raw format to a file. Then I "cat" the file into plugin on the bash command line.

# ausearch --start recent --raw > test.log
# cat test.log | ./audisp-example


OK. Let's get started. When events come in during realtime, it is one record at a time. This means you can't do anything until all records in the event have landed. So, the records need to be accumulated and the program must go back to waiting for the next piece so that it can process a whole event. The nice thing is that auparse has an event feed interface that takes care of this for you. The feed API is configured with a callback function that will be called as soon as an event is detected as complete. The callback function is handed an auparse_state_t variable that has one complete event in it. You cannot loop across events because there is only one. You can loop across records and fields.

The basic structure is:

Init auparse and tell it the source will be the feed API
Init the feed API by registering a callback function
Setup asynchronous reading via select, poll, or epoll
Read in the one record
Push record into feed API
If stdin has error, cleanup and exit

Let's make simple test program that simply writes the record type and how many fields that record contains to stdout. All error checking and signal support has been removed for clarity. In a real program these must be handled.

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/select.h>
#include <string.h>
#include <errno.h>
#include <libaudit.h>
#include <auparse.h>


static void handle_event(auparse_state_t *au,
                auparse_cb_event_t cb_event_type, void *user_data)
{
        if (cb_event_type != AUPARSE_CB_EVENT_READY)
                return;

        auparse_first_record(au);
        do {
                int type = auparse_get_type(au);
                printf("record type %d(%s) has %d fields\n",
                        auparse_get_type(au),
                        audit_msg_type_to_name(auparse_get_type(au)),
                        auparse_get_num_fields(au));
        } while (auparse_next_record(au) > 0);
}

int main(int argc, char *argv[])
{
        auparse_state_t *au = NULL;
        char tmp[MAX_AUDIT_MESSAGE_LENGTH+1];

        /* Initialize the auparse library */
        au = auparse_init(AUSOURCE_FEED, 0);
        auparse_add_callback(au, handle_event, NULL, NULL);
        do {
                int retval = -1;
                fd_set read_mask;

                FD_ZERO(&read_mask);
                FD_SET(0, &read_mask);

                do {
                        retval = select(1, &read_mask, NULL, NULL, NULL);
                } while (retval == -1 && errno == EINTR);

                /* Now the event loop */
                 if (retval > 0) {
                        if (fgets_unlocked(tmp, MAX_AUDIT_MESSAGE_LENGTH,
                                stdin)) {
                                auparse_feed(au, tmp,
                                        strnlen(tmp, MAX_AUDIT_MESSAGE_LENGTH));
                        }
                } else if (retval == 0)
                        auparse_flush_feed(au);
                if (feof(stdin))
                        break;
        } while (1);

        /* Flush any accumulated events from queue */
        auparse_flush_feed(au);
        auparse_destroy(au);

        return 0;
}


You can ave the program as audisp-example.c and compile it like this:

gcc -o audisp-example audisp-example.c -lauparse -laudit


Now run the program as stated earlier by cat'ing some captured raw events out of a file.

# cat test.log | ./audisp-example
record type 1300(SYSCALL) has 27 fields
record type 1307(CWD) has 2 fields
record type 1302(PATH) has 11 fields
record type 1327(PROCTITLE) has 2 fields
record type 1300(SYSCALL) has 27 fields
record type 1307(CWD) has 2 fields
record type 1302(PATH) has 4 fields
record type 1327(PROCTITLE) has 2 fields


OK, great we have a working program. But its not too useful since what its doing is written to stdout. How about if we make a real program that sends a notification to the desktop anytime someone logs in? Wouldn't that be fun?


Login Alert Plugin
The first thing to know is that you cannot send a notification as root because it tries to connect to the system bus. Any program launched by audispd is started as root. So, here's the first challenge. Instead what you have to do is send it to the user's session bus that is logged in and you have to do it using their uid. We are going to cheat because working this out adds code and removes clarity of the auparse portion. You can google around for the missing pieces if you really want to develop this further. But we will cheat by creating a define that holds our account number. The program will not work unless you have the exact account number you are currently logged in with. On a default install, you are most likely to be account 1000. Adjust the define accordingly.


#define _GNU_SOURCE
#include <stdio.h>
#include <sys/select.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <libaudit.h>
#include <auparse.h>
#include <libnotify/notify.h>

#define MY_ACCOUNT 1000

const char *note = "Login Alert";

static void handle_event(auparse_state_t *au,
                auparse_cb_event_t cb_event_type, void *user_data)
{
        if (cb_event_type != AUPARSE_CB_EVENT_READY)
                return;

        auparse_first_record(au);
        do {
                int type = auparse_get_type(au);
                if (type == AUDIT_USER_LOGIN) {
                        char msg[256],  *name = NULL;
                        const char *res;

                        /* create a message */
                        if (!auparse_find_field(au, "acct")) {
                                auparse_first_record(au);
                                if (auparse_find_field(au, "auid"))
                                    name = strdup(auparse_interpret_field(au));
                        } else
                                name = strdup(auparse_interpret_field(au));
                        res = auparse_find_field(au, "res");
                        snprintf(msg, sizeof(msg), "%s log in %s",
                                name ? name : "someone", res);

                        /* send a message */
                        NotifyNotification *n = notify_notification_new(note,
                                        msg, NULL);
                        notify_notification_set_urgency(n, NOTIFY_URGENCY_NORMAL);
                        notify_notification_set_timeout(n, 3000); //3 seconds
                        notify_notification_show (n, NULL);
                        g_object_unref(G_OBJECT(n));

                        free(name);
                        return;
                }
        } while (auparse_next_record(au) > 0);
}

int main(int argc, char *argv[])
{
        auparse_state_t *au = NULL;
        char tmp[MAX_AUDIT_MESSAGE_LENGTH+1], bus[32];

        /* Initialize the auparse library */
        au = auparse_init(AUSOURCE_FEED, 0);
        auparse_add_callback(au, handle_event, NULL, NULL);

        /* Setup the notification stuff */

        notify_init(note); 
        snprintf(bus, sizeof(bus), "unix:path=/run/user/%d/bus", MY_ACCOUNT);
        setenv("DBUS_SESSION_BUS_ADDRESS", bus, 1);
        if (setresuid(MY_ACCOUNT, MY_ACCOUNT, MY_ACCOUNT))
                return 1;

        do {
                int retval;
                fd_set read_mask;

                FD_ZERO(&read_mask);
                FD_SET(0, &read_mask);

                do {
                        retval = select(1, &read_mask, NULL, NULL, NULL);
                } while (retval == -1 && errno == EINTR);

                /* Now the event loop */
                 if (retval > 0) {
                        if (fgets_unlocked(tmp, MAX_AUDIT_MESSAGE_LENGTH,
                                stdin)) {
                                auparse_feed(au, tmp,
                                        strnlen(tmp, MAX_AUDIT_MESSAGE_LENGTH));
                        }
                } else if (retval == 0)
                        auparse_flush_feed(au);
                if (feof(stdin))
                        break;
        } while (1);

        /* Flush any accumulated events from queue */
        auparse_flush_feed(au);
        auparse_destroy(au);

        return 0;
}


Save it as audisp-example2.c and compile as follows. You may need to install libnotify-devel and gdk-pixbuf2-devel before compiling:

gcc -o audisp-example2 audisp-example2.c \
 `pkg-config --cflags glib-2.0` `pkg-config --cflags gdk-pixbuf-2.0` \
 `pkg-config --libs glib-2.0` `pkg-config --libs gdk-pixbuf-2.0` \
 -lauparse -laudit -lnotify



Let's talk a bit about what the program is doing. If you look at the main function, you will see that reading events is almost exactly the same as in the first example program. We have to put the bus location into the environment for libnotify and change to the logged in user's account. But that's it for changes. The biggest difference from the previous program is in the callback function.

The callback function looks at the event type. If it is the user_login event, then we process it. If you used ausearch to grab a couple user_login events, you will notice that the event is different on success and failure. On failure we have an "acct" field with the user's account. But on success we have an "auid" field that has this information. This creates a problem that I'll show in the next blog post how to solve in a nicer way. I wanted to point this out now so that when we do this nicer in the next blog post it informs why we needed the auparse_normalizer interface.

But for now we'll look for "acct" using auparse_find_field(). If it fails, we'll reset to the beginning and scan for the "auid "field. It should be noted that if you scan for a field using auparse_find_field() and it fails to find the field, the internal cursor will have run "off the end". Auparse_find_field() will iterate across all fields and all records in an event. It will not cross the event boundary, though. Since this is a 1 record event, we can just call auparse_first_record() to place the internal cursor back at the beginning.

Creating a notice is very simple. We just call  notify_notification_new() with the message and associate it with the note that we initialized with. We can also add an icon if we wanted, but for simplicity we'll skip that. We set the urgency, give it a length of time to be shown, and then we show it. After that we have to clean up by releasing allocated memory.

In any event, let's give the program a spin. Copy it to /sbin and fill out a plugin config file as follows (we don't need args so comment it out):

active = yes
direction = out
path = /sbin/audisp-example2
type = always
#args = 1
format = string


The plugin config file should be copied to /etc/audisp/plugins.d/audisp-example2.conf. Then disable selinux enforcement by setenforce 0. Selinux doesn't know about our plugin and will block changing to your account and sending the notification. Stop auditd service and start it. Check its status.

[root@x2 ~]# setenforce 0
[root@x2 ~]# service auditd stop
Stopping logging:                                          [  OK  ]
[root@x2 ~]# service auditd start
Redirecting to /bin/systemctl start  auditd.service
[root@x2 ~]# service auditd status
Redirecting to /bin/systemctl status  auditd.service
● auditd.service - Security Auditing Service
   Loaded: loaded (/usr/lib/systemd/system/auditd.service; enabled; vendor prese
   Active: active (running) since Thu 2017-04-13 09:17:07 EDT; 5s ago
     Docs: man:auditd(8)
           https://github.com/linux-audit/audit-documentation
  Process: 6289 ExecStartPost=/sbin/augenrules --load (code=exited, status=0/SUC
  Process: 6283 ExecStart=/sbin/auditd (code=exited, status=0/SUCCESS)
 Main PID: 6284 (auditd)
    Tasks: 6 (limit: 4915)
   CGroup: /system.slice/auditd.service
           ├─6284 /sbin/auditd
           ├─6286 /sbin/audispd
           ├─6288 /sbin/audisp-example2
           └─6290 /usr/sbin/sedispatch


OK, looks good. Now ssh localhost and see what happens.

Conclusion
Once you understand the recipe of creating a realtime plugin, there are many things you can do. The audit system really is easy to work with. But as I hinted there are quirks that you have to know that perhaps you wished you didn't have to know. This is why the auparse_normalizer interface was created. Next blog post we will see how to use it in an audispd plugin to simplify event processing by creating a plugin that will send an email whenever it sees an event with a specific key.

2 comments:

SWAPSSS said...

Hi Steve, great post!

Is it possible to use the Feed API if audispd is configured to forward events to a unix socket? So essentially using the unix socket input as the data for the feed?

Steve Grubb said...

You can open a unix domain socket and pass it to auparse_init with the source argument being AUSOURCE_DESCRIPTOR. That should work fine.