Format string vulnerability adalah kelas bug yang khas di C: bila pengguna dapat mengendalikan argumen pertama printf, pengguna tersebut dapat membaca memori (%x, %s), menulis memori (%n), atau bahkan mengeksekusi kode arbitrer. CWE-134 (Use of Externally-Controlled Format String) mencatat banyak kasus bersejarah—dari worm WU-FTPD tahun 2000 hingga CVE-2021-3156 di sudo. Pada chapter ini kita mendemonstrasikan dua versi pencetakan input pengguna: printf(user_input) yang berbahaya, dan printf("%s", user_input) yang aman. Kita akan melihat bagaimana input berisi %x dapat membocorkan nilai stack tanpa pemanggil fungsi sadar.

Mengapa printf(user_input) Berbahaya #

Fungsi printf(fmt, ...) membaca argumen pertama sebagai format string dan mengonsumsi argumen variadik sesuai specifier yang ditemui. Bila pemanggil menulis printf(user_input) tanpa argumen tambahan, tetapi user_input mengandung %x, printf tetap akan mencoba mengonsumsi argumen berikutnya—yang tidak ada. Pada kenyataannya, ia membaca register dan stack sebagai sumber argumen tersebut, membocorkan nilai-nilai internal seperti pointer, alamat return, dan isi variabel lokal.

c
/* format_string_vulnerabilities.c */
/* Bandingkan printf(user_input) yang VULNERABLE vs printf("%s", user_input) yang AMAN. */
#include <stdio.h>
#include <string.h>

/* AMAN: format string adalah literal konstan, user_input menjadi argumen. */
static void print_safe(const char *user_input) {
    printf("[SAFE]  ");
    printf("%s", user_input);
    printf("\n");
}

/* VULNERABLE: user_input menjadi format string. Bila user menulis
   "%x %x %x" program akan membaca stack secara membabi buta. */
static void print_vulnerable(const char *user_input) {
    printf("[VULN]  ");
    /* Sengaja di-print untuk demonstrasi; jangan ditiru di produksi. */
    printf(user_input);
    printf("\n");
}

int main(void) {
    const char *clean = "Halo Budi";
    const char *attack = "AA %x %x %x %x";

    printf("=== Kasus 1: input normal ===\n");
    print_safe(clean);
    print_vulnerable(clean);

    printf("\n=== Kasus 2: input berisi specifier format ===\n");
    print_safe(attack);
    /* Berikut akan membaca nilai acak dari stack: */
    print_vulnerable(attack);

    printf("\n=== Kasus 3: input berisi %%n (penulis tertulis dalam %%n) ===\n");
    /* Untuk keamanan, kita hanya menampilkan input yang berisi %n sebagai string. */
    const char *attack_n = "lewat %n di sini";
    print_safe(attack_n);
    /* Tidak menjalankan print_vulnerable dengan %n karena %n menulis memori. */
    printf("[VULN]  (dilewati - %%n menulis memori, berbahaya)\n");

    return 0;
}

Output yang Dihasilkan:

text
=== Kasus 1: input normal ===
[SAFE]  Halo Budi
[VULN]  Halo Budi

=== Kasus 2: input berisi specifier format ===
[SAFE]  AA %x %x %x %x
[VULN]  AA 63e1a5c0 0 0 73

=== Kasus 3: input berisi %n (penulis tertulis dalam %n) ===
[SAFE]  lewat %n di sini
[VULN]  (dilewati - %n menulis memori, berbahaya)

Mari kita telaah baris-baris kode di atas:

  1. Baris 7-11 (print_safe): Format string "%s" adalah literal konstan di segmen .rodata. Input pengguna diperlakukan sebagai argumen biasa—string yang akan dicetak apa adanya. Karakter %x, %s, atau %n di input tidak memiliki efek khusus karena printf hanya memproses specifier yang ada di format string, bukan di argumen.
  2. Baris 14-19 (print_vulnerable): Input pengguna langsung menjadi format string. printf membaca argumen pertama sebagai format dan mulai mengonsumsi argumen variadik yang sebenarnya tidak ada. Hasilnya: nilai-nilai dari register dan stack dibocorkan. Output Kasus 2 memperlihatkan empat nilai heksa—dua 0, satu nilai alamat 63e1a5c0, dan satu byte 0x73 (huruf s) yang merupakan sisa dari string "Halo Budi" di stack.
  3. Baris 22 (Kasus 1): Input normal tidak memicu specifier. Kedua versi menghasilkan output identik—bila tidak ada serangan, kerentanan tidak terlihat. Sifat inilah yang membuat bug ini sulit dideteksi tanpa fuzzing atau audit kode.
  4. Baris 27-30 (Kasus 2): Versi print_safe menampilkan input apa adanya: AA %x %x %x %x. Versi print_vulnerable mengeksekusi specifier %x empat kali, membocorkan empat kata 32-bit dari stack. Penyerang dapat memakai pola ini untuk information disclosure—mencari alamat kanal return-to-libc atau mengalahkan ASLR.
  5. Baris 33-37 (Kasus 3): Specifier %n tidak mencetak, melainkan menulis jumlah karakter yang telah dicetak ke alamat yang ditunjuk oleh argumen berikutnya. Karena print_vulnerable tidak menyediakan argumen pointer yang sah, %n akan menulis ke lokasi acak di memori—biasanya crash, kadang arbitrary write yang dieksploitasi untuk eksekusi kode.

Deteksi dengan Compiler Flag #

Compiler GCC dan Clang dapat mendeteksi pola printf(non_literal) pada saat compile. Simpan file pendek berikut:

c
/* fmt_vulnerable.c — demonstrasi -Wformat-security */
#include <stdio.h>
int main(void) {
    const char *user_input = "test %x";
    printf(user_input);  /* VULNERABLE */
    return 0;
}

Kompilasi dengan -Wformat-security:

bash
gcc -std=c11 -Wall -Wextra -Wformat-security -o fmt_vulnerable fmt_vulnerable.c

Output yang Dihasilkan:

text
fmt_vulnerable.c: In function 'main':
fmt_vulnerable.c:4:5: warning: format not a string literal and no format arguments [-Wformat-security]
    4 |     printf(user_input);  /* VULNERABLE */
      |     ^~~~~~

Compiler mendeteksi bahwa argumen pertama printf bukan literal string dan tidak ada argumen format lainnya—pola klasik format string vulnerability. Tambahkan -Werror=format-security pada build produksi agar warning ini menjadi error fatal yang memblokir build.

Aturan Sederhana: Selalu Pakai Format Literal #

Solusi praktis: argumen pertama printf (dan kerabatnya fprintf, sprintf, snprintf, syslog) selalu berupa string literal atau konstanta yang dipercaya. Bila kita ingin mencetak string pengguna, gunakan specifier %s:

c
/* AMAN */ printf("%s", user_input);
/* AMAN */ fprintf(stderr, "error: %s\n", user_msg);
/* AMAN */ snprintf(buf, sizeof buf, "%s = %d", key, value);
/* AMAN */ syslog(LOG_INFO, "login user=%s", user);

/* BERBAHAYA */ printf(user_input);
/* BERBAHAYA */ sprintf(buf, user_input);
/* BERBAHAYA */ syslog(LOG_INFO, user_input);

Mendeteksi dengan Compiler dan Sanitizer #

GCC dan Clang menyediakan dua lapis perlindungan otomatis:

  1. Compile-time warning -Wformat-security: Mendiagnosis panggilan printf(non_constant) dengan peringatan. Pasang dengan -Werror=format-security agar warning ini menjadi error fatal.
  2. Format sanitizer -fsanitize=format: Memeriksa tipe argumen vs specifier saat runtime. Bila tipe tidak cocok, program dibatalkan.
bash
gcc -std=c11 -Wall -Wextra -Wformat -Wformat-security \
    -fsanitize=format -o demo demo.c

Daftar Fungsi yang Perlu Diawasi #

Tabel berikut merangkum fungsi pustaka standar yang menggunakan format string:

FungsiSumber FormatCatatan
printf(fmt, ...)argumen pertamaWajib literal
fprintf(f, fmt, ...)argumen keduaWajib literal
sprintf(buf, fmt, ...)argumen keduaWajib literal; ganti snprintf
snprintf(buf, n, fmt, ...)argumen ketigaWajib literal
vprintf(fmt, ap)argumen pertamaWajib literal
syslog(prio, fmt, ...)argumen keduaWajib literal
err(status, fmt, ...)argumen keduaWajib literal
errx(status, fmt, ...)argumen keduaWajib literal