Dynamic dispatch pada C++ umumnya diimplementasikan via tabel fungsi virtual (vtable). Pada sistem runtime seperti interpreter bahasa dinamis, evaluasi kode yang fleksibel sering mengekspos objek polimorfik ke alokasi memori dinamis berskala masif. Jika terjadi celah Use-After-Free (UAF) atau buffer overflow, penyerang dapat menimpa pointer vtable (vptr) untuk mengalihkan alur eksekusi instruksi CPU (Control Flow Hijacking).
Anatomi Vektor Serangan: Vtable Hijacking dan Use-After-Free
Sebuah objek polimorfik C++ menyimpan pointer tersembunyi bernama _vptr di awal layout memorinya yang menunjuk ke array pointer fungsi virtual di segmen read-only. Serangan vtable hijacking terjadi ketika penyerang memanipulasi pointer ini ke tabel virtual palsu (fake vtable) yang telah diisi alamat shellcode atau gadget ROP (Return-Oriented Programming).
// Layout memori objek rentan
struct ASTNode {
virtual ~ASTNode() = default;
virtual void evaluate() = 0;
};
// Vektor serangan via heap spraying / UAF:
// 1. Objek dibebaskan: free(node);
// 2. Penyerang mengalokasikan payload pada alamat heap yang sama.
// 3. node->_vptr kini menunjuk ke memori yang dikontrol penyerang.
// 4. node->evaluate() memicu lompatan tidak langsung (indirect call) ke payload.Kasus ini sering muncul pada arsitektur interpreter parsing ekspresi, di mana evaluasi makro atau evaluasi dinamis (seperti FEXPR) mengubah struktur pohon sintaksis secara runtime saat simpul masih dieksekusi.
Pelajaran dari Isolasi FEXPR vs Vtable
Pada arsitektur interpreter seperti LispE, evaluasi ekspresi dinamis yang tidak mengevaluasi argumen secara otomatis (FEXPR) dipisahkan dari hirarki virtual standar. Penggunaan vtable secara bebas pada tipe evaluasi dinamis meningkatkan risiko korupsi memori karena lifetime objek AST menjadi non-deterministik.
Mengisolasi mekanisme eksekusi fungsi dinamis dari dereferensi vtable mentah mencegah skenario di mana metadata internal interpreter dapat dimanipulasi dari skrip eksternal.
Substitusi Dynamic Dispatch: Closed Polymorphism via std::variant
Langkah eliminasi paling efektif adalah mengganti polimorfisme terbuka (vtable) dengan tipe terisolasi tertutup berbasis std::variant dan std::visit. Pendekatan ini menghilangkan _vptr dari layout objek, sehingga serangan vtable overwrite mustahil dilakukan.
#include <variant>
#include <memory>
#include <iostream>
struct Literal { int value; };
struct Variable { std::string name; };
struct Operation;
using ASTElement = std::variant<Literal, Variable, std::unique_ptr<Operation>>;
struct Operation {
char op;
ASTElement left;
ASTElement right;
};
struct Evaluator {
void operator()(const Literal& lit) const {
std::cout << "Literal: " << lit.value << '\n';
}
void operator()(const Variable& var) const {
std::cout << "Variable: " << var.name << '\n';
}
void operator()(const std::unique_ptr<Operation>& op) const {
std::cout << "Operator: " << op->op << '\n';
}
};
// Eksekusi tanpa vptr dereference; jump table dihasilkan statis oleh compiler
void executeNode(const ASTElement& node) {
std::visit(Evaluator{}, node);
}std::visit memetakan eksekusi menggunakan jump table tetap yang disimpan pada segmen teks (.text) atau branch jump kondisional internal, bukan pointer fungsi pada heap yang dapat ditimpa.
Proteksi Hardware dan Compiler
Jika penggunaan class berbasis virtual tidak dapat dihindari, terapkan proteksi tingkat compiler dan CPU:
1. Control Flow Guard (CFG) dan Clang Clang CFI
Aktifkan Control Flow Integrity (CFI) pada Clang untuk memvalidasi bahwa target indirect call berasal dari vtable yang sah:
clang++ -fsanitize=cfi-vcall -fvisibility=hidden -flto source.cpp -o interpreterFlag -fsanitize=cfi-vcall menginstruksikan compiler menyisipkan verifikasi runtime sebelum pemanggilan virtual dilakukan. Jika alamat target tidak berada dalam daftar vtable valid milik kelas yang bersangkutan, program langsung memicu trap (SIGILL/SIGSEGV abort).
2. Pointer Authentication Codes (ARM64 PAC)
Pada arsitektur ARMv8.3-A ke atas, manfaatkan Pointer Authentication (PAC) untuk menandatangani pointer vtable dan return address menggunakan cryptographic salt.
clang++ -mbranch-protection=pac-ret+b-key+bti source.cpp -o interpreterJika penyerang menimpa _vptr di heap, instruksi AUTIA atau AUTDA akan gagal memvalidasi tanda tangan kriptografi sebelum dereferensi fungsi, menghentikan eksekusi sebelum payload aktif.
Memindahkan Vtable ke Read-Only Memory
Pastikan struktur tabel virtual berada pada segment read-only (.rodata). Jangan pernah mengalokasikan array pointer fungsi virtual di heap secara dinamis. Jika runtime membutuhkan dispatch table yang dapat dimodifikasi saat parsing kode baru, lindungi alokasi tersebut dengan pembatasan hak akses halaman sistem operasi:
#include <sys/mman.h>
#include <unistd.h>
void freezeDispatchTable(void* tableAddress, size_t size) {
long pageSize = sysconf(_SC_PAGESIZE);
uintptr_t addr = reinterpret_cast<uintptr_t>(tableAddress);
void* alignedAddr = reinterpret_cast<void*>(addr & ~(pageSize - 1));
// Kunci tabel dispatch setelah populasi simbol selesai
mprotect(alignedAddr, size, PROT_READ);
}Dengan menyetel hak akses halaman memori ke PROT_READ, setiap upaya eksploitasi yang mencoba menulis ulang entry point dispatcher akan memicu crash seketika (kernel memory violation), mencegah eksekusi arbitrary code.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!