Membuat Plugin MySQL Yang Menulis Hasil Audit Ke Windows Event Log


Kode program untuk artikel ini juga bisa dibaca di https://github.com/JockiHendry/mysql_windows_audit_plugin.

Pada artikel Memahami Authentication dan Auditing Di MySQL Server, saya menggunakan MariaDB Audit Plugin di MySQL Server untuk melakukan auditing dan menuliskan hasilnya dalam bentuk file. Bila saya memberikan perintah seperti SET GLOBAL server_audit_output_type = 'SYSLOG' maka MariaDB Audit Plugin akan menulis hasil audit ke dalam syslog. Sayangnya ini hanya berlaku khusus untuk sistem operasi yang mendukung syslog seperti Linux. Fitur ini tidak akan menulis ke Windows Event Log!

Menampilkan hasil audit di Event Viewer akan memberikan lebih banyak flekibilitas dibandingkan dengan melihat log dalam bentuk file. Selain itu, dari sisi keamanan, file bisa di-edit secara leluasa oleh penyerang (misalnya menghilangkan baris yang mencurigakan). Hal ini tidak terjadi di Event Viewer karena log bersifat read-only dan hanya bisa dihapus secara global (log yang tiba-tiba hilang semua pasti mencurigakan!).

Apakah mungkin membuat MySQL Server menulis hasil audit ke Event Viewer? Yup, ini bisa dilakukan karena MySQL Server dapat di-extend dengan mekanisme plugin. Saya hanya perlu membuat sebuah auditing plugin untuk MySQL Server. Saya sudah menuliskan contoh kode program yang memakai Windows Event Log di artikel Membuat Program Visual C++ Yang Menulis Ke Windows Event Log.

Saya akan mulai dengan membuka Visual C++ 2010 dan membuat sebuah Win32 Project baru yang saya beri nama sebagai mysql_windows_audit. Pada kotak dialog yang muncul, saya memilih jenis aplikasi DLL dan Empty Project seperti yang terlihat pada gambar berikut ini:

Membuat proyek baru.

Membuat proyek baru.

Saya perlu menambahkan sebuah file kode program C dengan men-klik kanan pada Source Files dan memilih menu Add, New Item…. Pada kotak dialog yang muncul, saya memilih C++ File (.cpp). Agar jelas bahwa saya ingin memakai bahasa pemograman C, saya mengisi nama file dengan windows_audit.c sebelum men-klik tombol Add.

Sebelum mulai membuat kode program, saya perlu melakukan beberapa pengaturan. Untuk itu, saya men-klik kanan nama proyek dan memilih Properties. Setelah itu, saya beralih ke konfigurasi Release. Pada bagian C/C++, General, saya perlu menyertakan header file dari MySQL. Secara default, lokasi ini adalah C:\Program Files\MySQL\MySQL Server 5.6\include:

Melakukan pengaturan proyek.

Melakukan pengaturan proyek.

Selanjutnya, pada bagian C/C++, Preprocessor, saya menambahkan MYSQL_DYNAMIC_PLUGIN di bagian Preprocessor Definitions. Saya juga memastikan bahwa saya pada C/C++, Precompiled Headers, nilai untuk Precompiled Header adalah Not Using Precompiled Headers. Setelah itu, saya men-klik tombol OK untuk menutup dialog.

Karena memakai Windows Event Log, saya perlu membuat instrumentation manifest, misalnya sebuah file XML dengan nama message.man yang isinya seperti berikut ini:

<?xml version="1.0" encoding="UTF-16"?>
<instrumentationManifest xsi:schemaLocation="http://schemas.microsoft.com/win/2004/08/events eventman.xsd" xmlns="http://schemas.microsoft.com/win/2004/08/events" xmlns:win="http://manifests.microsoft.com/win/2004/08/windows/events" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:trace="http://schemas.microsoft.com/win/2004/08/events/trace">
    <instrumentation>
        <events>
            <provider name="MySQLWindowsAuditProvider" guid="{566E77CB-DC32-4B2D-A0CA-01CDF9BEC031}" symbol="MYSQL_WINDOWS_AUDIT_PROVIDER" resourceFileName="C:\Program Files\MySQL\MySQL Server 5.6\lib\plugin\mysql_windows_audit.dll" messageFileName="C:\Program Files\MySQL\MySQL Server 5.6\lib\plugin\mysql_windows_audit.dll">
                <events>
                    <event symbol="Connect" value="1" version="1" channel="MySqlWindowsAudit" level="win:Informational" template="ConnectionTemplate" message="$(string.MySQLWindowsAuditProvider.event.1.message)">
                    </event>
                    <event symbol="Disconnect" value="2" version="1" channel="MySqlWindowsAudit" level="win:Informational" template="ResultTemplate" message="$(string.MySQLWindowsAuditProvider.event.2.message)">
                    </event>
                    <event symbol="Change" value="3" version="1" channel="MySqlWindowsAudit" level="win:Informational" template="ConnectionTemplate" message="$(string.MySQLWindowsAuditProvider.event.3.message)">
                    </event>
                    <event symbol="Error" value="4" version="1" channel="MySqlWindowsAudit" level="win:Error" template="ConnectionTemplate" message="$(string.MySQLWindowsAuditProvider.event.4.message)">
                    </event>
                    <event symbol="ActiveChanged" value="5" version="1" channel="MySqlWindowsAudit" level="win:Warning" template="BooleanUpdateTemplate" message="$(string.MySQLWindowsAuditProvider.event.5.message)">
                    </event>
                </events>
                <levels>
                </levels>
                <channels>
                    <channel name="MySqlWindowsAudit" chid="MySqlWindowsAudit" symbol="MYSQL_WINDOWS_AUDIT" type="Operational" enabled="true" message="$(string.MySQLWindowsAudit.channel.MYSQL_WINDOWS_AUDIT.message)">
                    </channel>
                </channels>
                <templates>
                    <template tid="ConnectionTemplate">
                        <data name="status" inType="win:Int32" outType="xs:int">
                        </data>
                        <data name="user" inType="win:AnsiString" outType="xs:string">
                        </data>
                        <data name="host" inType="win:AnsiString" outType="xs:string">
                        </data>
                        <data name="ip" inType="win:AnsiString" outType="xs:string">
                        </data>
                        <data name="database" inType="win:AnsiString" outType="xs:string">
                        </data>
                    </template>
                    <template tid="ResultTemplate">
                        <data name="status" inType="win:Int32" outType="xs:int">
                        </data>
                    </template>
                    <template tid="BooleanUpdateTemplate">
                        <data name="var" inType="win:AnsiString" outType="xs:string">
                        </data>
                        <data name="active" inType="win:Boolean" outType="xs:boolean">
                        </data>
                    </template>
                </templates>
            </provider>
        </events>
    </instrumentation>
    <localization>
        <resources culture="en-US">
            <stringTable>
                <string id="level.Warning" value="Warning">
                </string>
                <string id="level.Informational" value="Information">
                </string>
                <string id="level.Error" value="Error">
                </string>
                <string id="MySQLWindowsAuditProvider.event.5.message" value="%1 has been changed to %2.">
                </string>
                <string id="MySQLWindowsAuditProvider.event.4.message" value="Connection error for %2 (%4) at database %5 at %3.  Status: %1.">
                </string>
                <string id="MySQLWindowsAuditProvider.event.3.message" value="Change user for %2 (%4) at database %5 at %3.  Status: %1.">
                </string>
                <string id="MySQLWindowsAuditProvider.event.2.message" value="User disconnect with the following status: %1.">
                </string>
                <string id="MySQLWindowsAuditProvider.event.1.message" value="Connection from %2 (%4) to database %5 at %3.  Status: %1.">
                </string>
                <string id="MySQLWindowsAudit.channel.MYSQL_WINDOWS_AUDIT.message" value="MySQL Connection Audit">
                </string>
            </stringTable>
        </resources>
    </localization>
</instrumentationManifest>

Saya mengisi nilai resourceFileName dan messageFileName dengan "C:\Program Files\MySQL\MySQL Server 5.6\lib\plugin\mysql_windows_audit.dll". Ini adalah file DLL yang akan dihasilkan oleh proyek yang sedang saya buat. Bila seandainya plugin di-install di tempat yang terpisah, saya perlu mengubah nilai tersebut.

File di atas mendefinisikan 5 jenis event yang bisa dihasilkan oleh program. Event Connect, Disconnect dan Change mewakili aktifitas koneksi, diskoneksi dan perubahan user. Ketiga event ini memiliki level Information. Event Error akan terjadi terdapat kesalahan yang berhubungan dengan koneksi (misalnya pengguna login dengan password yang salah atau mengakses database yang salah). Dan terakhir, event ActiveChanged yang memiliki level Warning akan terjadi bila pengguna mematikan atau mengaktifkan fasilitas logging dari plugin ini.

Setelah memberikan perintah MC.exe, saya segera menambahkan referensi ke file yang dihasilkan sehingga struktur proyek terlihat seperti pada gambar berikut ini:

Struktur proyek

Struktur proyek

Sekarang, saatnya untuk menulis kode program! Karena tutorial menulis plugin MySQL tidak begitu banyak, saya akan memakai kode program MariaDB Audit Plugin sebagai sumber referensi. Kode program MariaDB Audit Plugin bisa dilihat di https://github.com/MariaDB/server/blob/10.1/plugin/server_audit/server_audit.c. Selain itu, saya memakai proyek audit_null yang bisa dilihat di https://github.com/mysql/mysql-server/tree/5.7/plugin/audit_null sebagai skeleton awal. Tentu saja karena saya membuat fitur yang hanya jalan di Windows, saya bisa memakai API khusus Windows dan tidak perlu memikirkan kode program yang cross-platform.

Sebagai contoh, saya membuat file windows_audit.c yang isinya seperti berikut ini:

#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include "message.h"
#include <mysql/plugin.h>
#include <mysql/plugin_audit.h>

static char active = 1;
static long connection_errors;
static int internal_stop_logging = 0;
static CRITICAL_SECTION cs;
static const char* nullString = "null";

static struct st_mysql_show_var audit_status[] = {  
    {"windows_audit_connection_errors", (char*) &connection_errors, SHOW_LONG},
    {0,0,0}
};

static void update_active(MYSQL_THD thd, struct st_mysql_sys_var *var, void *var_ptr, const void *save) {   
    char new_is_active = *(char*) save;
    if (new_is_active == active) return;
    EnterCriticalSection(&cs);
    internal_stop_logging = 1;
    active = new_is_active; 
    EventWriteActiveChanged("windows_audit_active", active);
    internal_stop_logging = 0;
    LeaveCriticalSection(&cs);
}

static MYSQL_SYSVAR_BOOL(active, active, PLUGIN_VAR_OPCMDARG, "Turn on/off the logging.", NULL, update_active, 1);

static struct st_mysql_sys_var* vars[] = {
    MYSQL_SYSVAR(active),
    NULL
};

static int windows_audit_plugin_init(void *arg)
{   
    if (EventRegisterMySQLWindowsAuditProvider() != ERROR_SUCCESS) {
        fwprintf(stderr, L"Can't register Windows log provider.n");
        return -1;
    }
    InitializeCriticalSection(&cs); 
    connection_errors = 0;
    fwprintf(stderr, L"Windows Audit Plugin STARTED.n");  
    return 0;
}

static int windows_audit_plugin_deinit(void *arg)
{   
    if (EventUnregisterMySQLWindowsAuditProvider() != ERROR_SUCCESS) {
        fwprintf(stderr, L"Can't unregister Windows log provider.n");
        return -1;
    }
    DeleteCriticalSection(&cs);
    fwprintf(stderr, L"Windows Audit Plugin STOPPED.n");
    return 0;
}

static const char* new_cstr(const char* str, ULONG size) {      
    char* result = (char*) malloc(sizeof(char) * ((size==0) ? sizeof(nullString) : size) + 1);
    memcpy(result, (size==0) ? nullString : str, ((size==0) ? sizeof(nullString) : size) + 1);  
    return result;
}

static void event_log(PCEVENT_DESCRIPTOR descriptor, EVENT_DATA_DESCRIPTOR* data, const struct mysql_event_connection* connEvent) {
    const char* user, *host, *ip, *database;
    EventDataDescCreate(&data[0], &(connEvent->status), sizeof(const signed int)  );

    user = new_cstr(connEvent->user, connEvent->user_length);                                             
    EventDataDescCreate(&data[1], user, strlen(user)+1);

    host = new_cstr(connEvent->host, connEvent->host_length);
    EventDataDescCreate(&data[2], host, strlen(host)+1);

    ip = new_cstr(connEvent->ip, connEvent->ip_length);
    EventDataDescCreate(&data[3], ip, strlen(ip)+1);

    database = new_cstr(connEvent->database, connEvent->database_length);                                                         
    EventDataDescCreate(&data[4], database, strlen(database)+1);

    EventWrite(MySQLWindowsAuditProviderHandle, descriptor, 5, data);

    free((void*) user);
    free((void*) host);
    free((void*) ip);
    free((void*) database);
}

static void windows_audit_notify(MYSQL_THD thd, unsigned int event_class, const void *e)
{ 
    const struct mysql_event_connection *connEvent; 
    EVENT_DATA_DESCRIPTOR eventData[5];

    if (internal_stop_logging || !active) return;   
    EnterCriticalSection(&cs);  

    if (event_class == MYSQL_AUDIT_CONNECTION_CLASS) {
        internal_stop_logging = 1;  
        connEvent = (const struct mysql_event_connection *) e;

        if (connEvent->status > 0) {
            event_log(&Error, eventData, connEvent);
            connection_errors++;
        } else {
            switch (connEvent->event_subclass) {
                case MYSQL_AUDIT_CONNECTION_CONNECT:
                    event_log(&Connect, eventData, connEvent);
                    break;
                case MYSQL_AUDIT_CONNECTION_DISCONNECT:      
                    EventWriteDisconnect(connEvent->status);
                    break;
                case MYSQL_AUDIT_CONNECTION_CHANGE_USER:     
                    event_log(&Change, eventData, connEvent);
                    break;
                default:                    
                    break;          
            }
        }
        internal_stop_logging = 0;
    }

    LeaveCriticalSection(&cs);
}


/*
  Plugin type-specific descriptor
*/

static struct st_mysql_audit windows_audit_descriptor=
{
  MYSQL_AUDIT_INTERFACE_VERSION,                       /* interface version    */
  NULL,                                                /* release_thd function */
  windows_audit_notify,                                /* notify function      */
  { (unsigned long) MYSQL_AUDIT_CONNECTION_CLASSMASK } /* class mask           */
};

/*
  Plugin library descriptor
*/

mysql_declare_plugin(windows_audit)
{
  MYSQL_AUDIT_PLUGIN,                       /* type                            */
  &windows_audit_descriptor,                /* descriptor                      */
  "WINDOWS_AUDIT",                            /* name                            */
  "Jocki Hendry",                         /* author                          */
  "Audit connections to Windows Log",     /* description                     */
  PLUGIN_LICENSE_GPL,
  windows_audit_plugin_init,                /* init function (when loaded)     */
  windows_audit_plugin_deinit,              /* deinit function (when unloaded) */
  0x0001,                                   /* version                         */
  audit_status,                             /* status variables                */
  vars,                                     /* system variables                */
  NULL,
  0,
}
mysql_declare_plugin_end;

Bagian yang diapit oleh macro mysql_declare_plugin(windows_audit) dan mysql_declare_plugin_end; berisi informasi mengenai plugin yang saya buat. Disini saya juga mendaftarkan function windows_audit_plugin_init() sebagai function yang akan dikerjakan saat plugin dimulai (misalnya saat database dinyalakan) dan function windows_audit_plugin_deinit() sebagai function yang akan dikerjakan saat plugin dimatikan. Selain itu, saya juga mendaftarkan audit_status sebagai status yang bisa dilihat melalui perintah SHOW STATUS. Saya juga mendaftarkan vars sebagai variabel yang bisa diatur melalui perintah SET GLOBAL.

MySQL mendukung beberapa jenis plugin. Khusus untuk audit plugin, saya perlu membuat descriptor dengan tipe st_mysql_audit. Sebagai contoh, pada kode program saya, ini diwakili oleh variabel windows_audit_descriptor. Pada descriptor ini, saya mendaftarkan function windows_audit_notify() sebagai function yang akan dipanggil setiap kali ada aktifitas audit. Saya hanya memakai MYSQL_AUDIT_CONNECTION_CLASSMASK sebagai class mask sehingga hanya informasi yang berkaitan dengan koneksi saja yang akan di-audit.

Bagian kode program yang mencatat ke Windows Event Log terdapat di windows_audit_notify(). Pada function ini, saya akan memperoleh informasi koneksi yang sedang di-audit dalam bentuk argumen bertipe mysql_event_connection. Variabel tersebut mengandung informasi seperti status kesalahan (atau sukses), nama user, ip user, database yang hendak diakses dan lokasi host database.

Seusai membuat kode program, saya perlu men-build proyek untuk menghasilkan file DLL. Setelah itu, saya perlu memanggil Wevtutil.exe dan men-copy file DLL ke lokasi plugin di instalasi MySQL Server. Karena langkah-langkah ini akan selalu saya kerjakan setiap kali melihat hasil perubahan program, maka saya membuat sebuah batch file bernama install.bat untuk mengotomatisasikannya:

@echo off

:shutdown_mysql
tasklist | find "mysqld.exe" > nul
if errorlevel 1 goto :uninstall_manifest
echo Shutting down mysqld.exe.
taskkill /f /im mysqld.exe

:uninstall_manifest
wevtutil gp MySQLWindowsAuditProvider 2> nul | find ":" > nul
if errorlevel 1 goto :install_manifest
echo Uninstall information manifest for MySQLWindowsAuditProvider
wevtutil um message.man

:install_manifest
echo Install manifest for MysQLWindowsAuditProvider
wevtutil im message.man

:copy dll file to plugin location
set mysql_plugin_dir="C:\Program Files\MySQL\MySQL Server 5.6\lib\plugin"
echo Copy %1 to %mysql_plugin_dir%
copy %1 %mysql_plugin_dir% > nul

Setelah itu, saya men-klik kanan nama proyek dan memilih Properties. Pada dialog yang muncul, saya memilih Configuration Properties, Build Events, Post-Build Event. Setelah itu, saya mengisi bagian Command Line dengan nilai seperti berikut ini:

Mengatur build event di proyek Visual Studio

Mengatur build event di proyek Visual Studio

Sekarang, Visual Studio akan secara otomatis mengerjakan install.bat seusai menghasilkan file DLL. Untuk menghasilkan file DLL, saya segera memilih menu Build, Build Solution.

Untuk menguji plugin yang telah dibuat, saya segera menjalankan MySQL Server dan men-install plugin dengan memberikan perintah SQL seperti berikut ini (hanya perlu dilakukan bila belum pernah di-install sebelumnya!):

INSTALL PLUGIN WINDOWS_AUDIT SONAME 'mysql_windows_audit.dll';

Untuk memastikan plugin sudah aktif, saya dapat menggunakan perintah SHOW PLUGINS seperti berikut ini:

mysql> SHOW PLUGINS;
+----------------------------+----------+--------------------+-------------------------+---------+
| Name                       | Status   | Type               | Library                 | License |
+----------------------------+----------+--------------------+-------------------------+---------+
| binlog                     | ACTIVE   | STORAGE ENGINE     | NULL                    | GPL     |
| mysql_native_password      | ACTIVE   | AUTHENTICATION     | NULL                    | GPL     |
| WINDOWS_AUDIT              | ACTIVE   | AUDIT              | mysql_windows_audit.dll | GPL     |
+----------------------------+----------+--------------------+-------------------------+---------+

Bila saya membuka Windows Event Viewer, saya akan menjumpai channel baru bernama MySQLWindowsAuditProvider. Semua hasil audit akan tersimpan di channel ini, seperti yang terlihat pada gambar berikut ini:

Contoh log yang dihasilkan di Event Viewer

Contoh log yang dihasilkan di Event Viewer

Selain itu, saya juga bisa melihat jumlah koneksi yang gagal dilakukan sejak database dinyalakan dengan memberikan perintah SQL seperti berikut ini:

mysql> SHOW STATUS LIKE 'windows_audit_%';
+---------------------------------+-------+
| Variable_name                   | Value |
+---------------------------------+-------+
| windows_audit_connection_errors | 1     |
+---------------------------------+-------+

Saya juga bisa mematikan proses audit yang dilakukan oleh plugin ini dengan memberikan perintah SQL seperti berikut ini:

mysql> SET GLOBAL windows_audit_active = 0;
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW GLOBAL VARIABLES LIKE 'windows_audit_%';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| windows_audit_active | OFF   |
+----------------------+-------+

Perubahan pada variabel ini juga akan menimbulkan sebuah log baru di Event Viewer, seperti yang terlihat pada gambar berikut ini:

Log yang muncul jika status audit berubah.

Log yang muncul jika status audit berubah.

Berkat fasilitas plugin dari MySQL Server, saya memperoleh kemampuan auditing sesuai dengan yang saya butuhkan secara mudah🙂

Perihal Solid Snake
I'm nothing...

One Response to Membuat Plugin MySQL Yang Menulis Hasil Audit Ke Windows Event Log

  1. Ping-balik: Memakai Full-Text Search Di MySQL Server | Programming Logic And Technology

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: