Kerentanan autentikasi pada sistem low-level sering kali tidak bersumber dari buffer overflow murni, melainkan dari kesalahan logika transisi state. Insiden klasik seperti bypass Password Authentication Protocol (PAP) pada daemon PPP OpenBSD memperlihatkan bagaimana parser gagal menangani input tak terduga, sehingga state machine mengizinkan akses tanpa memvalidasi kredensial secara valid. Artikel ini membahas strategi regression testing untuk memverifikasi Finite State Machine (FSM) autentikasi C agar kebal terhadap bypass serupa.

Anatomi State Machine Auth Bypass

Bypass autentikasi pada implementasi C umumnya terjadi akibat kegagalan sinkronisasi antara pemrosesan payload dan mutasi state FSM. Perhatikan skenario rentan berikut:

  • Parser menerima paket autentikasi (misalnya AUTH_REQ).
  • Payload kredensial cacat (misal panjang string bernilai nol atau pointer NULL).
  • Fungsi parsing gagal mengekstrak identitas tetapi mengembalikan kode status ambigu (misalnya 0 alih-alih error negatif).
  • Eksekutor FSM langsung mengevaluasi status tanpa assertion eksplisit dan menggeser state internal ke STATE_AUTHENTICATED.

Ketiadaan penanganan kondisi default yang defensif (fail-safe defaults) mengubah parser data menjadi vektor eksploitasi logic bug.

Strategi Penyusunan Test Vectors

Regression testing FSM autentikasi membutuhkan matriks test vectors deterministik yang mencakup boundary condition dan malformed payload. Vektor pengujian minimal harus mencakup:

  1. Input Kosong (Zero-length Field): Header protokol valid tetapi panjang username/password diatur ke 0 byte.
  2. Embedded Null Byte (\0): Injeksi terminasi string sebelum batas buffer aktual untuk menguji perbedaan penanganan panjang antara integer length field dan fungsi string C (strlen vs memcmp).
  3. String Mismatch Ekstrem: Validasi penolakan password salah dengan panjang acak, menghindari perbandingan waktu (timing attack mitigation).
  4. Flag dan State ID Tak Terduga: Mengirimkan paket valid dalam urutan state yang salah (misalnya mengirim AUTH_SUCCESS_ACK dari sisi klien ke server).

Implementasi Test Harness C Sederhana

Gunakan C test harness mandiri berbasis assert() untuk memvalidasi transisi state secara ketat. Harness ini tidak membutuhkan dependensi eksternal dan dapat dijalankan langsung di CI pipeline.

#include <stdio.h>
#include <string.h>
#include <assert.h>

typedef enum {
    STATE_INIT,
    STATE_AUTHENTICATING,
    STATE_AUTHENTICATED,
    STATE_REJECTED
} AuthState;

typed struct {
    AuthState state;
    const char *expected_user;
    const char *expected_pass;
} AuthContext;

/* Fungsi parser dan transisi FSM */
int handle_auth_packet(AuthContext *ctx, const char *user, size_t ulen, const char *pass, size_t plen) {
    if (!ctx) return -1;

    /* Transisi hanya sah jika berasal dari STATE_INIT */
    if (ctx->state != STATE_INIT) {
        ctx->state = STATE_REJECTED;
        return -1;
    }

    ctx->state = STATE_AUTHENTICATING;

    /* Negative checks: tolak input kosong atau pointer null */
    if (!user || !pass || ulen == 0 || plen == 0) {
        ctx->state = STATE_REJECTED;
        return -1;
    }

    /* Validasi data dengan exact length */
    if (ulen == strlen(ctx->expected_user) &&
        plen == strlen(ctx->expected_pass) &&
        memcmp(user, ctx->expected_user, ulen) == 0 &&
        memcmp(pass, ctx->expected_pass, plen) == 0) {
        ctx->state = STATE_AUTHENTICATED;
        return 0;
    }

    ctx->state = STATE_REJECTED;
    return -1;
}

/* Test Harness */
static void test_auth_success(void) {
    AuthContext ctx = { .state = STATE_INIT, .expected_user = "admin", .expected_pass = "s3cr3t" };
    int rc = handle_auth_packet(&ctx, "admin", 5, "s3cr3t", 6);
    assert(rc == 0);
    assert(ctx.state == STATE_AUTHENTICATED);
}

static void test_auth_null_and_empty(void) {
    AuthContext ctx = { .state = STATE_INIT, .expected_user = "admin", .expected_pass = "s3cr3t" };

    /* Test input panjang 0 */
    int rc = handle_auth_packet(&ctx, "", 0, "s3cr3t", 6);
    assert(rc == -1);
    assert(ctx.state == STATE_REJECTED);

    /* Reset dan test pointer NULL */
    ctx.state = STATE_INIT;
    rc = handle_auth_packet(&ctx, NULL, 5, "s3cr3t", 6);
    assert(rc == -1);
    assert(ctx.state == STATE_REJECTED);
}

static void test_auth_embedded_null_mismatch(void) {
    AuthContext ctx = { .state = STATE_INIT, .expected_user = "admin", .expected_pass = "s3cr3t" };
    
    /* Password berisi byte null di tengah */
    const char bad_pass[] = "s3\0r3t";
    int rc = handle_auth_packet(&ctx, "admin", 5, bad_pass, 6);
    assert(rc == -1);
    assert(ctx.state == STATE_REJECTED);
}

static void test_invalid_initial_state(void) {
    /* Simulasi bypass urutan state */
    AuthContext ctx = { .state = STATE_AUTHENTICATED, .expected_user = "admin", .expected_pass = "s3cr3t" };
    int rc = handle_auth_packet(&ctx, "admin", 5, "s3cr3t", 6);
    assert(rc == -1);
    assert(ctx.state == STATE_REJECTED);
}

int main(void) {
    test_auth_success();
    test_auth_null_and_empty();
    test_auth_embedded_null_mismatch();
    test_invalid_initial_state();
    printf("All FSM auth regression tests passed.\n");
    return 0;
}

Assertion Ketat dan Negative Testing

Kunci mencegah bypass logic bug adalah negative testing: memastikan program gagal secara terprediksi saat menerima input cacat. Praktik evaluasi assertion pada parser FSM:

  • Validasi ganda: Uji nilai balik fungsi (rc == -1) DAN kondisi state pasca-eksekusi (ctx.state == STATE_REJECTED). Mengabaikan evaluasi state membuka peluang bug di mana fungsi me-return error tetapi state internal sudah telanjur berubah menjadi sah.
  • Prinsip Default-Deny: Inisialisasi awal variabel state parser harus selalu mengarah ke failure. Jangan pernah menetapkan status SUCCESS sebelum proses komparasi seluruh parameter berhasil tervalidasi.

Alternatif minimal tanpa harness C terpisah adalah mengintegrasikan test vector tersebut langsung ke automated fuzzing parser (misalnya menggunakan LibFuzzer) jika input buffer melibatkan format framing yang kompleks.