Buffer overflow adalah kerentanan klasik namun tetap menjadi penyebab utama eksploitasi pada perangkat lunak yang ditulis dalam C. CWE-120 (Buffer Copy without Checking Size of Input) dan CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer) mendaftarkan ribuan kasus baru setiap tahun. Pada chapter ini kita membandingkan beberapa pasangan fungsi berbahaya dengan padanannya yang aman: gets() versus fgets(), serta sprintf() versus snprintf(). Kita juga akan melihat bagaimana snprintf memberi tahu kita jumlah byte yang dibutuhkan sehingga kita dapat mendeteksi pemotongan.
Mengapa gets() Dihapus di C11 #
Fungsi gets() menerima satu argumen—pointer buffer—dan membaca dari stdin sampai menemui newline atau EOF. Karena tidak ada parameter ukuran, gets() tidak tahu kapan harus berhenti. Input yang lebih panjang dari buffer akan menimpa memori di sebelahnya: variabel lokal, return address, atau pointer fungsi. Situasi inilah yang membuat gets() tak dapat diperbaiki—mustahil memakainya dengan aman. ISO C11 akhirnya menghapus gets() dari pustaka standar dan menggantikannya dengan gets_s() (opsional, didukung Annex K).
Pengganti paling portabel adalah fgets():
char buf[BUF_SIZE];
if (fgets(buf, sizeof buf, stdin) == NULL) {
/* EOF atau error */
}fgets membaca paling banyak n-1 byte, menambahkan null terminator, dan mengembalikan NULL bila mencapai EOF sebelum ada karakter. Satu hal yang perlu diingat: fgets menyimpan karakter newline bila masih ada ruang—kita biasanya menghapusnya secara manual.
sprintf vs snprintf #
Sama seperti gets(), sprintf() tidak menerima parameter ukuran. Format string yang dihasilkan bisa lebih panjang dari buffer tujuan dan menimpa memori sekitar. snprintf() menyelesaikan masalah ini dengan parameter ukuran eksplisit dan selalu menghasilkan string yang diakhiri null (selama ukuran > 0).
/* buffer_overflow_prevention.c */
/* Bandingkan sprintf vs snprintf, dan fgets sebagai pengganti gets(). */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define BUF_SIZE 8
/* AMAN: snprintf selalu memotong sesuai kapasitas. */
static void copy_safe(const char *src, char *dst, size_t dst_size) {
if (dst_size == 0) return;
snprintf(dst, dst_size, "%s", src);
/* Jamin terminasi null walau src lebih panjang. */
dst[dst_size - 1] = '\0';
}
/* Demo snprintf langsung: laporkan jumlah byte yang dibutuhkan. */
static void demo_snprintf(const char *src) {
char buf[BUF_SIZE];
int n = snprintf(buf, sizeof buf, "%s", src);
printf("[snprintf] buf='%s' (butuh %d byte, kapasitas %zu)\n",
buf, n, sizeof buf);
}
int main(void) {
const char *samples[] = { "halo", "abcdefg", "abcdef0123456789" };
const size_t n = sizeof(samples) / sizeof(samples[0]);
printf("=== Demonstrasi copy_safe dengan snprintf ===\n");
for (size_t i = 0; i < n; i++) {
char buf[BUF_SIZE];
copy_safe(samples[i], buf, sizeof buf);
printf("input='%s' -> output='%s'\n", samples[i], buf);
}
printf("\n=== Demonstrasi return value snprintf ===\n");
for (size_t i = 0; i < n; i++) {
demo_snprintf(samples[i]);
}
printf("\n=== Catatan gets() vs fgets() ===\n");
printf("gets() dihapus di C11. fgets(buf, n, stdin) aman:\n");
printf(" - membaca paling banyak n-1 byte\n");
printf(" - menambah null terminator\n");
printf(" - mengembalikan NULL pada EOF/error\n");
return 0;
}Output yang Dihasilkan:
=== Demonstrasi copy_safe dengan snprintf ===
input='halo' -> output='halo'
input='abcdefg' -> output='abcdefg'
input='abcdef0123456789' -> output='abcdef0'
=== Demonstrasi return value snprintf ===
[snprintf] buf='halo' (butuh 4 byte, kapasitas 8)
[snprintf] buf='abcdefg' (butuh 7 byte, kapasitas 8)
[snprintf] buf='abcdef0' (butuh 16 byte, kapasitas 8)
=== Catatan gets() vs fgets() ===
gets() dihapus di C11. fgets(buf, n, stdin) aman:
- membaca paling banyak n-1 byte
- menambah null terminator
- mengembalikan NULL pada EOF/errorMari kita telaah baris-baris kode di atas:
- Baris 9-14 (
copy_safe): Fungsi wrapper yang menjamin pemotongan.snprintfsecara kontraktual akan menulis paling banyakdst_size - 1byte plus null terminator. Barisdst[dst_size - 1] = '\0'berfungsi sebagai defense in depth—mengulang terminasi null bila ada implementasisnprintfyang melanggar kontrak. - Baris 17-21 (
demo_snprintf): Demonstrasi sifat menariksnprintf: nilai baliknya adalah jumlah byte yang akan ditulis bila buffer cukup besar, bukan jumlah byte yang benar-benar ditulis. Pada input"abcdef0123456789", fungsi melaporkanbutuh 16 bytemeski buffer hanya berisiabcdef0(7 karakter plus null). Sifat ini memungkinkan kita mendeteksi pemotongan—bilan >= dst_size, berarti data dipotong. - Baris 27-32 (loop pertama): Tiga input diuji. Input pertama (
halo, 4 byte) dan kedua (abcdefg, 7 byte) muat penuh. Input ketiga (abcdef0123456789, 16 byte) dipotong menjadiabcdef0(7 karakter plus null). Tidak ada overflow meski input jauh lebih panjang. - Baris 35-39 (loop kedua): Mengonfirmasi bahwa kita selalu dapat memeriksa apakah pemotongan terjadi: bila
n >= sizeof buf, data input dipotong. Di kode produksi, kita dapat mengembalikan kode error atau mengalokasikan buffer yang lebih besar.
Mendeteksi Pemotongan di Produksi #
Pola pemeriksaan berikut umum dipakai untuk menangani input yang mungkin dipotong:
char buf[BUF_SIZE];
int n = snprintf(buf, sizeof buf, "%s", input);
if (n < 0) {
/* encoding error */
return ERROR_ENCODING;
}
if ((size_t)n >= sizeof buf) {
/* input dipotong; gunakan malloc(n + 1) bila perlu simpan utuh */
return ERROR_TRUNCATED;
}
/* buf berisi data lengkap */Pemeriksaan n < 0 wajib karena snprintf dapat mengembalikan nilai negatif bila terjadi kesalahan encoding.
strncpy dan Null Terminator Manual #
strncpy adalah alternatif lain untuk strcpy yang menerima parameter ukuran, tetapi memiliki footgun halus: bila src lebih panjang dari dst_size - 1, strncpy tidak menambahkan null terminator. Kita wajib menambahkannya secara manual:
/* extra_buffer_overflow.c — strncpy dengan manual null terminator. */
#include <stdio.h>
#include <string.h>
#define BUF 8
static void copy_strncpy(const char *src, char *dst, size_t dst_size) {
if (dst_size == 0) return;
/* strncpy TIDAK menjamin null bila src lebih panjang dari dst_size-1. */
strncpy(dst, src, dst_size - 1);
dst[dst_size - 1] = '\0'; /* wajib tambah manual */
}
int main(void) {
const char *samples[] = { "halo", "1234567", "abcdefghijklmnop" };
const size_t n = sizeof(samples) / sizeof(samples[0]);
for (size_t i = 0; i < n; i++) {
char buf[BUF];
copy_strncpy(samples[i], buf, sizeof buf);
printf("input='%-16s' output='%s' (panjang %zu)\n",
samples[i], buf, strlen(buf));
}
return 0;
}Output yang Dihasilkan:
input='halo ' output='halo' (panjang 4)
input='1234567 ' output='1234567' (panjang 7)
input='abcdefghijklmnop' output='abcdefg' (panjang 7)Baris dst[dst_size - 1] = '\0' wajib ditulis. Tanpa baris ini, panggilan strncpy dengan input yang lebih panjang akan meninggalkan buf tanpa null terminator—string operation berikutnya (strlen, printf("%s"), strcmp) akan membaca melampaui buffer hingga menemui byte nol di memori. Inilah alasan banyak tim beralih ke snprintf(dst, size, "%s", src) yang lebih konsisten.
Kerangka Buffer yang Aman #
Untuk memastikan semua pemakaian buffer aman, pertahankan kerangka berikut:
- Selalu sertakan ukuran saat memanggil fungsi pustaka yang menulis ke buffer:
fgets(buf, sizeof buf, ...),snprintf(buf, sizeof buf, ...). - Periksa nilai balik untuk mendeteksi pemotongan atau error encoding.
- Hindari
strcpy,strcat,sprintf,gets,scanf("%s", ...)tanpa batas lebar. Ganti denganstrncpy+manual null,strncat,snprintf,fgets,scanf("%31s", ...)(lebar ditulis eksplisit). - Aktifkan flag compiler
-D_FORTIFY_SOURCE=2saat build rilis. GCC akan mengganti pemanggilanstrcpykememcpyyang memeriksa batas pada level compile-time bila ukuran diketahui.