Format binary level retro era DOS/EGA (seperti engine Commander Keen atau Wolfenstein 3D) memetakan array integer langsung ke memory buffer. Pendekatan naive fread langsung ke struct rentan terhadap arbitrary memory corruption jika file level dimanipulasi: integer overflow pada dimensi memicu alokasi heap yang terlalu kecil, diikuti buffer overflow atau out-of-bounds write.
Artikel ini membahas mitigasi eksploitasi binary parser tilemap di C++ modern menggunakan bounds checking ketat, verifikasi magic header, dan safe slice view berbasis std::span.
Vektor Kerentanan Parser Binary Tradisional
Parser retro umumnya mengandalkan layout flat binary berikut:
- Magic Bytes (4 bytes): Identifier format file (contoh:
MAP1). - Lebar & Tinggi (2x uint16_t): Dimensi grid.
- Layer Count (uint16_t): Jumlah plane (misal: background, foreground, collision).
- Payload Data: Raw tile IDs (array
uint16_tberukuranwidth * height * layers).
Tiga celah utama pada arsitektur parser lama:
- Integer Overflow saat Perhitungan Ukuran: Mengalikan
width * height * layers * sizeof(uint16_t)dengan tipe integer 16-bit atau 32-bit dapat wrap around ke nilai kecil. Buffer dialokasikan terlalu kecil, namun loop parsing membaca data melebihi batas buffer (Heap-based Buffer Overflow). - Missing Payload Bounds Validation: Nilai dimensi di-header menyatakan 1.000.000 tile, namun payload file berakhir di 100 byte. Melakukan read tanpa mencocokkan sisa ukuran buffer file memicu Out-of-Bounds Read atau Segmentation Fault.
- Unchecked Coordinate Lookup: Fungsi
get_tile(x, y)menggunakan rumusy * width + xtanpa asersi batas koordinat, menyebabkan arbitrary memory read/write pada runtime rendering.
Arsitektur Hardening Parser C++
Gunakan pendekatan zero-copy aman via std::span (tersedia sejak C++20). Raw byte stream divalidasi secara defensif sebelum instansiasi objek map.
#pragma once
#include <cstdint>
#include <vector>
#include <optional>
#include <span>
#include <cstring>
#include <limits>
struct MapHeader {
uint32_t magic;
uint16_t width;
uint16_t height;
uint16_t layers;
};
constexpr uint32_t EXPECTED_MAGIC = 0x3150414D; // ASCII 'MAP1' dalam Little-Endian
constexpr uint16_t MAX_DIMENSION = 4096; // Batas wajar ukuran map
constexpr uint16_t MAX_LAYERS = 8;
class Tilemap {
public:
uint16_t width = 0;
uint16_t height = 0;
uint16_t layers = 0;
std::vector<uint16_t> tiles;
[[nodiscard]] std::optional<uint16_t> get_tile(size_t layer, size_t x, size_t y) const noexcept {
if (layer >= layers || x >= width || y >= height) {
return std::nullopt; // Hard bounds check
}
size_t index = (layer * width * height) + (y * width) + x;
return tiles[index];
}
};
Implementasi Parser Defensif
Parser menerima span memory immutable (misalnya data hasil mapping file via mmap atau buffer dari disk). Setiap segmen divalidasi secara berurutan:
std::optional<Tilemap> parse_tilemap(std::span<const uint8_t> raw_data) {
// 1. Verifikasi ukuran minimal untuk header
if (raw_data.size() < sizeof(MapHeader)) {
return std::nullopt;
}
MapHeader header;
std::memcpy(&header, raw_data.data(), sizeof(MapHeader));
// 2. Verifikasi magic header
if (header.magic != EXPECTED_MAGIC) {
return std::nullopt;
}
// 3. Sanity check dimensi mencegah integer overflow
if (header.width == 0 || header.height == 0 || header.layers == 0) {
return std::nullopt;
}
if (header.width > MAX_DIMENSION || header.height > MAX_DIMENSION || header.layers > MAX_LAYERS) {
return std::nullopt;
}
// Hitung total tile menggunakan uint64_t untuk eliminasi risiko overflow
uint64_t total_tiles = static_cast<uint64_t>(header.width) *
static_cast<uint64_t>(header.height) *
static_cast<uint64_t>(header.layers);
uint64_t payload_bytes = total_tiles * sizeof(uint16_t);
// 4. Verifikasi sisa ukuran payload buffer
uint64_t available_payload = raw_data.size() - sizeof(MapHeader);
if (available_payload < payload_bytes) {
return std::nullopt;
}
// 5. Inisialisasi Tilemap dan salin payload
Tilemap map;
map.width = header.width;
map.height = header.height;
map.layers = header.layers;
map.tiles.resize(total_tiles);
const uint8_t* payload_ptr = raw_data.data() + sizeof(MapHeader);
std::memcpy(map.tiles.data(), payload_ptr, payload_bytes);
return map;
}
Unit Testing: Verifikasi Input Malformed
Gunakan pengujian assert sederhana untuk memverifikasi parser menolak korupsi byte, dimensi manipulatif, dan buffer truncation.
#include <cassert>
#include <iostream>
void run_tests() {
// Test 1: Buffer kosong
assert(!parse_tilemap({}));
// Test 2: Magic bytes salah
uint8_t bad_magic[] = { 'B', 'A', 'D', '!', 0x0A, 0x00, 0x0A, 0x00, 0x01, 0x00 };
assert(!parse_tilemap(bad_magic));
// Test 3: Integer overflow pada dimensi (payload truncated)
// Map 100x100 1 layer butuh 10.000 * 2 = 20.000 bytes payload
MapHeader fake_header = { EXPECTED_MAGIC, 100, 100, 1 };
std::vector<uint8_t> truncated_buf(sizeof(MapHeader) + 50); // Hanya sedia 50 bytes
std::memcpy(truncated_buf.data(), &fake_header, sizeof(MapHeader));
assert(!parse_tilemap(truncated_buf));
// Test 4: Valid payload & bounds checking
MapHeader valid_header = { EXPECTED_MAGIC, 2, 2, 1 }; // 4 tiles = 8 bytes
std::vector<uint8_t> valid_buf(sizeof(MapHeader) + 8);
std::memcpy(valid_buf.data(), &valid_header, sizeof(MapHeader));
uint16_t sample_tiles[4] = { 10, 20, 30, 40 };
std::memcpy(valid_buf.data() + sizeof(MapHeader), sample_tiles, 8);
auto result = parse_tilemap(valid_buf);
assert(result.has_value());
assert(result->get_tile(0, 0, 0) == 10);
assert(result->get_tile(0, 1, 1) == 40);
assert(!result->get_tile(0, 2, 0).has_value()); // Out-of-bounds X
assert(!result->get_tile(1, 0, 0).has_value()); // Out-of-bounds layer
std::cout << "All defensive checks passed.\n";
}
int main() {
run_tests();
return 0;
}
Analisis Trade-Off
Penerapan dynamic check overhead-nya minimal dibandingkan risiko memory corruption:
- Runtime Cost:
get_tile()menambahkan operasi percabangan (branching) per lookup. Dalam hot rendering loop, mitigasi ini dapat dioptimasi dengan iterator pointer langsung setelah loop luar memvalidasi viewport window. - Memory Cost: Pengecekan
uint64_tmencegah alokasi memory tak terduga (OOM DoS attack) sebelum buffer heap dipesan olehstd::vector::resize. - Portabilitas Format: Format binary berbasis struct langsung mengabaikan isu endianness jika engine ditargetkan untuk CPU multi-architecture. Gunakan fungsi utility seperti
std::byteswap(C++23) jika menargetkan format antar arsitektur berlainan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!