Anatomi Serangan ROP dan Keterbatasan Stack Canary

Return-Oriented Programming (ROP) membajak alur kontrol program dengan memanfaatkan instruksi mesin yang sudah ada di memori biner (dikenal sebagai gadgets), yang diakhiri dengan instruksi ret. Penyerang menyusun alamat gadget tersebut pada stack melalui kerentanan buffer overflow atau stack pivoting untuk mengeksekusi operasi berbahaya tanpa perlu menginjeksi shellcode executable baru.

Mitigasi tradisional seperti Stack Canaries (-fstack-protector-strong) bekerja dengan menempatkan nilai guard acak sebelum return address. Namun, mekanisme ini memiliki keterbatasan:

  • Dapat dilewati (bypassed) jika penyerang memiliki kerentanan pembacaan memori acak (arbitrary memory read / info leak) untuk membocorkan nilai canary.
  • Tidak memitigasi korupsi pointer fungsi lokal atau objek C++ (vtable pointers) yang berada sebelum canary.
  • Tidak mencegah teknik non-linear write yang melompati posisi stack canary langsung ke saved frame pointer atau return address.

Contoh Kerentanan Buffer Overflow pada Backend C++

Kode backend berikut memproses paket data mentah dengan unsafe buffer operation, memungkinkan penulisan di luar batas buffer stack:

// vulnerable_server.cpp
#include <iostream>
#include <cstring>
#include <unistd.h>

struct PacketHandler {
    virtual void process() {
        std::cout << "[+] Memproses paket normal.\n";
    }
    virtual ~PacketHandler() = default;
};

void parse_payload(int fd) {
    char buffer[64];
    // Kerentanan buffer overflow: membaca hingga 256 byte ke buffer 64 byte
    ssize_t bytes_read = read(fd, buffer, 256);
    if (bytes_read > 0) {
        std::cout << "[+] Diterima: " << bytes_read << " bytes.\n";
    }
}

int main() {
    PacketHandler handler;
    parse_payload(STDIN_FILENO);
    handler.process();
    return 0;
}

Pada arsitektur konvensional, overflow pada array buffer menimpa pointer vtable milik handler dan saved return address fungsi parse_payload, membuka celah eksekusi ROP chain.

Arsitektur Pertahanan: SafeStack, Shadow Call Stack, dan Clang CFI

1. Clang SafeStack

SafeStack (tersedia melalui flag -fsanitize=safe-stack) membagi stack thread tunggal menjadi dua struktur independen:

  • Safe Stack: Menyimpan return address, register ter-spill, dan variabel lokal yang terbukti aman secara statis (variabel yang tidak pernah diambil alamat referensinya/non-address-taken). Safe stack dilindungi dari buffer overrun dan dialokasikan melalui register khusus (misalnya FS segment pada x86_64).
  • Unsafe Stack: Menyimpan buffer array, struktur berukuran dinamis, dan variabel lokal yang alamat memorinya diakses secara pointer. Jika terjadi buffer overflow, korupsi memori hanya terjadi di dalam unsafe stack dan tidak dapat langsung menimpa return address.

2. Shadow Call Stack (SCS)

Berbeda dengan SafeStack, Shadow Call Stack (-fsanitize=shadow-call-stack) khusus menyimpan return address fungsi ke alokasi terpisah yang dilindungi register khusus (seperti x18 pada AArch64). SCS memiliki overhead runtime yang sangat minimal, namun terbatas pada arsitektur tertentu yang memiliki register cadangan khusus (umum di AArch64/RISC-V, tidak didukung native di x86_64 karena keterbatasan jumlah general-purpose registers).

3. Control Flow Integrity (CFI)

Clang CFI (-fsanitize=cfi) memvalidasi integritas target transfer kontrol tidak langsung (indirect calls/jumps). CFI menghentikan penyerang yang mencoba membelokkan virtual call table (vcall) atau function pointer call ke gadget ROP di luar alur yang sah:

  • Forward-edge protection: Memastikan target indirect call/jump berada di dalam tabel fungsi yang valid dengan signature tipe yang identik.
  • Backward-edge protection: Mengamankan return address (ditangani bersamaan oleh SafeStack/Shadow Call Stack).

Kompilasi Hardening Biner Menggunakan LLVM/Clang

CFI membutuhkan Link-Time Optimization (LTO) agar compiler dapat membangun grafik call graph global dari seluruh compilation unit. Perintah kompilasi biner dengan mitigasi SafeStack dan CFI:

clang++ -O2 -flto -fvisibility=hidden \
    -fsanitize=safe-stack \
    -fsanitize=cfi \
    -fstack-protector-strong \
    -Wl,-z,relro,-z,now \
    vulnerable_server.cpp -o hardened_server

Catatan: Flag -fvisibility=hidden wajib digunakan bersama Clang CFI agar compiler dapat menerapkan validasi pointer vtable secara ketat di internal translation unit biner.

Verifikasi Disassembly dan Inspeksi Instruksi

Jalankan objdump untuk memverifikasi pemisahan alokasi memori oleh SafeStack:

llvm-objdump -d -M intel hardened_server

Disassembly potongan fungsi parse_payload menunjukkan pemisahan stack pointer:

parse_payload(int):
  push   rbp
  mov    rbp, rsp
  # Mengambil pointer unsafe stack dari Thread Local Storage (FS segment)
  mov    rax, qword ptr fs:[0x0]
  mov    rcx, qword ptr [rax - 0x10]
  lea    rdx, [rcx - 0x40]             # Alokasi buffer 64 byte pada unsafe stack
  mov    qword ptr [rax - 0x10], rdx
  
  # Operasi I/O menggunakan rdx (unsafe stack pointer)
  mov    rsi, rdx
  call   read@PLT

  # Deallokasi unsafe stack dan return via safe stack
  mov    qword ptr [rax - 0x10], rcx
  pop    rbp
  ret                                  # Target ret tersimpan utuh di safe stack

Instruksi ret menggunakan safe stack yang ditunjuk oleh rsp asli, sedangkan operasi read mengeksekusi buffer yang dialokasikan di thread segment unsafe stack (rdx). Overflow pada buffer tidak dapat mencapai frame stack milik safe stack.

Trade-off dan Analisis Latensi Eksekusi

Menerapkan mitigasi biner tingkat compiler memperkenalkan overhead performa dan memori yang harus diukur secara terencana:

  • SafeStack: Penurunan throughput CPU backend berkisar antara 0.1% hingga 1%. Konsumsi memori bertambah sekitar 2x lipat untuk alokasi stack per thread, yang perlu diperhitungkan pada backend dengan pool ribuan thread.
  • Clang CFI (LTO + vcall check): Menghasilkan overhead latensi eksekusi antara 1% hingga 3% pada virtual function intensive workloads. Waktu proses kompilasi (build time) meningkat signifikan akibat dependency LTO antar-objek.
  • Kompatibilitas Library: Biner yang dikompilasi dengan CFI strict tidak dapat menerima dynamic library (DSO/plugins) eksternal secara acak jika signature tipe kelas C++ melintasi batas DSO tanpa konfigurasi CFI ignorelist.