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.
/* 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:
=== 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:
- 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%ndi input tidak memiliki efek khusus karenaprintfhanya memproses specifier yang ada di format string, bukan di argumen. - Baris 14-19 (
print_vulnerable): Input pengguna langsung menjadi format string.printfmembaca 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—dua0, satu nilai alamat63e1a5c0, dan satu byte0x73(hurufs) yang merupakan sisa dari string"Halo Budi"di stack. - 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.
- Baris 27-30 (Kasus 2): Versi
print_safemenampilkan input apa adanya:AA %x %x %x %x. Versiprint_vulnerablemengeksekusi specifier%xempat 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. - Baris 33-37 (Kasus 3): Specifier
%ntidak mencetak, melainkan menulis jumlah karakter yang telah dicetak ke alamat yang ditunjuk oleh argumen berikutnya. Karenaprint_vulnerabletidak menyediakan argumen pointer yang sah,%nakan 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:
/* 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:
gcc -std=c11 -Wall -Wextra -Wformat-security -o fmt_vulnerable fmt_vulnerable.cOutput yang Dihasilkan:
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:
/* 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:
- Compile-time warning
-Wformat-security: Mendiagnosis panggilanprintf(non_constant)dengan peringatan. Pasang dengan-Werror=format-securityagar warning ini menjadi error fatal. - Format sanitizer
-fsanitize=format: Memeriksa tipe argumen vs specifier saat runtime. Bila tipe tidak cocok, program dibatalkan.
gcc -std=c11 -Wall -Wextra -Wformat -Wformat-security \
-fsanitize=format -o demo demo.cDaftar Fungsi yang Perlu Diawasi #
Tabel berikut merangkum fungsi pustaka standar yang menggunakan format string:
| Fungsi | Sumber Format | Catatan |
|---|---|---|
printf(fmt, ...) | argumen pertama | Wajib literal |
fprintf(f, fmt, ...) | argumen kedua | Wajib literal |
sprintf(buf, fmt, ...) | argumen kedua | Wajib literal; ganti snprintf |
snprintf(buf, n, fmt, ...) | argumen ketiga | Wajib literal |
vprintf(fmt, ap) | argumen pertama | Wajib literal |
syslog(prio, fmt, ...) | argumen kedua | Wajib literal |
err(status, fmt, ...) | argumen kedua | Wajib literal |
errx(status, fmt, ...) | argumen kedua | Wajib literal |