Vektor Serangan DNS Covert Tunnel dan Keterbatasan Conntrack Netfilter
TEXT
7b2264625f70617373223a22736563726574227d.stage1.attacker-c2.example.com- Resolver membaca root hints untuk menemukan root name server (
.) - Root name server merespons dengan delegasi Top-Level Domain (TLD)
.com - TLD name server mengarahkan query ke authoritative server milik penyerang untuk zona
attacker-c2.example.com - Authoritative server milik penyerang membaca subdomain
7b2264625f..., mendekode data rahasia tersebut, dan mengembalikan respons DNS sintesis (biasanya record TXT, CNAME, atau A dummy).
BASH
# conntrack -L -p udp --dport 53
udp 17 29 src=192.0.2.10 dst=198.51.100.53 sport=54213 dport=53 [UNREPLIED] src=198.51.100.53 dst=192.0.2.10 sport=53 dport=54213 mark=0 use=1nf_conntrack_udp_timeout = 30) belum kedaluwarsa. Sistem tidak memiliki visibilitas terhadap struktur paket setelah header transport layer UDP berakhir. Paket data melewati gateway tanpa peringatan, meskipun muatan QNAME membawa data sensitif yang dieksfiltrasi.
Implementasi firewall konvensional tidak dapat melakukan parsing payload DNS pada kecepatan multi-gigabit tanpa menghabiskan siklus CPU secara masif jika dialihkan ke user-space daemon (misalnya melalui NFQUEUE). Untuk memahami bagaimana pendekatan eBPF menangani paket pada interface jaringan di level kernel secara efisien, arsitektur serupa pernah dibahas pada konteks routing di Validasi BGP Route Origin dan Filtrasi Prefix Menggunakan eBPF XDP pada Gateway Hosting Multi-Tenant.
Arsitektur Egress Traffic Control: Posisi clsact vs eXpress Data Path (XDP)
struct sk_buff dialokasikan oleh kernel. Namun, XDP memiliki batasan fundamental: secara historis dan arsitektural, XDP dioptimalkan murni untuk jalur ingress. Meskipun terdapat opsi transmit ulang (XDP_TX atau redirect ke interface lain), XDP tidak dapat memfilter paket yang dihasilkan secara lokal (locally generated traffic) oleh resolver DNS yang berjalan pada host itu sendiri sebelum paket tersebut keluar ke jaringan eksternal.
Paket yang diinisiasi oleh resolver rekursif lokal dibuat melalui syscall sendto() atau sendmsg() yang mengalokasikan socket buffer (struct sk_buff), melintasi routing engine kernel, dan masuk ke antrean egress interface via fungsi dev_queue_xmit(). Titik ini dilewati oleh eBPF TC melalui pseudo-qdisc clsact.
| Parameter / Fitur | eXpress Data Path (XDP) | Traffic Control (eBPF TC / clsact) |
|---|---|---|
| Hook Location | Driver level (RX ring buffer) | Antara IP stack dan Qdisc (sch_clsact) |
| Arah Trafik yang Didukung | Ingress saja (egress sangat terbatas via devmap) | Ingress dan Egress penuh |
| Struktur Data Input | struct xdp_md (raw packet pointer) | struct __sk_buff (metadata skb lengkap) |
Alokasi sk_buff | Belum dialokasikan (overhead sangat rendah) | Sudah dialokasikan oleh networking stack |
| Kemampuan Manipulasi L2-L4 | Parsing byte langsung | Memerlukan bpf_skb_pull_data jika linear buffer terpotong |
| Kompatibilitas Egress Resolver | Tidak mendukung locally generated traffic | Mendukung locally generated socket egress |
egress qdisc clsact, program eBPF dieksekusi di context softirq (NET_TX_SOFTIRQ) sesaat sebelum paket diserahkan ke driver ring buffer (TX ring). Pada titik ini, program eBPF memiliki hak kontrol untuk mengizinkan (TC_ACT_OK), memodifikasi, atau membuang paket seketika (TC_ACT_SHOT), mencegah paket egress mencapai kabel fisik.
Struktur Wire Format DNS dan Kendala eBPF Verifier
TEXT
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+0x00 (root null label), yang kemudian diikuti oleh 2 byte QTYPE dan 2 byte QCLASS.
Dalam domain eBPF, verifier kernel memberlakukan validasi ketat:
- Boundary Checking: Setiap pointer dereference ke buffer paket (
skb->datahinggaskb->data_end) harus diverifikasi secara eksplisit terhadap batas atas sebelum dibaca. Kelalaian pengecekan batas akan menyebabkan program ditolak saat proses loading (verifier log: invalid access to packet). - Bounded Loops: Parsing string nama domain melibatkan iterasi parsing label dinamis. Kernel modern (>= 5.3) mendukung bounded loop, namun jumlah iterasi instruksi harus terbukti terbatas dan deterministik agar verifier dapat memastikan program berhenti (halting problem).
- Non-linear Buffer (
skb_is_nonlinear): Pada paket egress yang dialokasikan oleh kernel stack, sebagian payload data mungkin berada di paged memory (skb_shinfo(skb)->frags). Sebelum parsing payload aplikasi, helperbpf_skb_pull_data()harus dipanggil untuk melinearisasi data ke buffer utama jika data yang dibutuhkan berada di luarskb_headlen.
Implementasi Program eBPF C (cls_bpf) untuk Deteksi Exfiltration
- Deteksi Anomali Panjang Label: Label tunggal yang mendekati batas maksimal RFC (panjang >= 50 byte) secara berulang merupakan indikator kuat encoding payload.
- Rate Limiting Frekuensi Transaksi Per Root Domain: Penegakan algoritma token bucket menggunakan
BPF_MAP_TYPE_LRU_HASHkernel map untuk mendeteksi burst query ke basis domain yang sama.
dns_egress_filter.bpf.c:
C
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#define MAX_DNS_NAME_LEN 255
#define MAX_LABEL_LEN 63
#define SUSPICIOUS_LABEL_LEN 50
#define TOKEN_BUCKET_CAPACITY 10
#define TOKEN_REFILL_NS 1000000000ULL // 1 detik per token refill
struct dns_hdr {
__be16 id;
__be16 flags;
__be16 qdcount;
__be16 ancount;
__be16 nscount;
__be16 arcount;
};
struct bucket_entry {
__u64 last_update_ns;
__u32 tokens;
};
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 16384);
__type(key, __u32); // Hash sederhana dari domain basis
__type(value, struct bucket_entry);
} domain_rate_map SEC(".maps");
SEC("tc_egress_dns")
int filter_egress_dns(struct __sk_buff *skb) {
void *data_end = (void *)(long)skb->data_end;
void *data = (void *)(long)skb->data;
// 1. Parsing Ethernet Header
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return TC_ACT_OK;
// 2. Parsing IPv4 Header
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return TC_ACT_OK;
if (ip->protocol != IPPROTO_UDP)
return TC_ACT_OK;
__u32 ip_hdr_len = ip->ihl * 4;
if (ip_hdr_len < sizeof(struct iphdr))
return TC_ACT_OK;
// 3. Parsing UDP Header
struct udphdr *udp = (void *)((char *)ip + ip_hdr_len);
if ((void *)(udp + 1) > data_end)
return TC_ACT_OK;
// Filter khusus traffic DNS egress (tujuan UDP port 53)
if (udp->dest != bpf_htons(53))
return TC_ACT_OK;
// Pastikan seluruh data UDP payload terlinearisasi dalam skb->data
__u32 offset = (void *)(udp + 1) - data;
__u32 total_len = bpf_ntohs(udp->len);
if (total_len < sizeof(struct udphdr) + sizeof(struct dns_hdr))
return TC_ACT_OK;
if (bpf_skb_pull_data(skb, offset + (total_len - sizeof(struct udphdr))) < 0) {
return TC_ACT_OK;
}
// Re-evaluasi pointer setelah bpf_skb_pull_data mengubah memory layout
data = (void *)(long)skb->data;
data_end = (void *)(long)skb->data_end;
eth = data;
ip = (void *)(eth + 1);
udp = (void *)((char *)ip + ip_hdr_len);
if ((void *)(udp + 1) > data_end)
return TC_ACT_OK;
struct dns_hdr *dns = (void *)(udp + 1);
if ((void *)(dns + 1) > data_end)
return TC_ACT_OK;
// Cek flag QR (0 = Query, 1 = Response). Hanya periksa Query.
__u16 flags = bpf_ntohs(dns->flags);
if ((flags & 0x8000) != 0)
return TC_ACT_OK;
// Hanya tangani query dengan qdcount == 1
if (bpf_ntohs(dns->qdcount) != 1)
return TC_ACT_OK;
// 4. Parsing Question Section (QNAME)
unsigned char *ptr = (void *)(dns + 1);
__u32 name_len = 0;
__u32 max_label_found = 0;
__u32 domain_hash = 0;
#pragma unroll
for (int i = 0; i < 5; i++) { // Membaca hingga maksimal 5 label pertama
if ((void *)(ptr + 1) > data_end)
break;
unsigned char len = *ptr;
if (len == 0) {
ptr++; // Null terminator ditemukan
break;
}
if (len > MAX_LABEL_LEN) {
// Kompresi pointer atau format ilegal dalam QNAME
return TC_ACT_SHOT;
}
if (len > max_label_found)
max_label_found = len;
ptr++;
if ((void *)(ptr + len) > data_end)
return TC_ACT_OK; // Packet terpotong, serahkan ke stack
// Akumulasi hash sederhana dari payload subdomain
for (int j = 0; j < 4; j++) {
if (j < len && (void *)(ptr + j + 1) <= data_end) {
domain_hash = ((domain_hash << 5) + domain_hash) + *(ptr + j);
}
}
ptr += len;
name_len += (len + 1);
if (name_len >= MAX_DNS_NAME_LEN)
break;
}
// 5. Eksekusi Rule Deteksi Exfiltration
// Kasus A: Label tunggal membawa muatan data masif
if (max_label_found >= SUSPICIOUS_LABEL_LEN) {
// Drop paket: anomali panjang label terindikasi Base64 payload
return TC_ACT_SHOT;
}
// Kasus B: Rate-limit transaksi query per hash domain via token bucket
__u64 now = bpf_ktime_get_ns();
struct bucket_entry *bucket = bpf_map_lookup_elem(&domain_rate_map, &domain_hash);
if (!bucket) {
struct bucket_entry new_bucket = {
.last_update_ns = now,
.tokens = TOKEN_BUCKET_CAPACITY - 1,
};
bpf_map_update_elem(&domain_rate_map, &domain_hash, &new_bucket, BPF_ANY);
return TC_ACT_OK;
}
// Hitung penambahan token berdasarkan selisih waktu
__u64 elapsed = now - bucket->last_update_ns;
__u32 tokens_to_add = (__u32)(elapsed / TOKEN_REFILL_NS);
if (tokens_to_add > 0) {
bucket->tokens += tokens_to_add;
if (bucket->tokens > TOKEN_BUCKET_CAPACITY)
bucket->tokens = TOKEN_BUCKET_CAPACITY;
bucket->last_update_ns = now;
}
if (bucket->tokens == 0) {
// Kapasitas token habis: burst query tidak wajar ke target yang sama
return TC_ACT_SHOT;
}
bucket->tokens--;
return TC_ACT_OK;
}
char _license[] SEC("license") = "GPL";#pragma unroll untuk membatasi iterasi parser. Hal ini merupakan syarat mutlak agar compiler clang menghasilkan set instruksi BPF linear yang dapat diverifikasi oleh kernel verifier tanpa menyebabkan infinite loop state explosion.
Kompilasi, Loading, dan Attachment pada Interface Egress
clang, llvm, serta iproute2 (tc). Program dikompilasi menjadi BPF Executable and Linkable Format (ELF) object file, kemudian dimuat ke dalam kernel.
1. Kompilasi Source Code ke Format Target eBPF
BASH
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 \
-I/usr/include/$(uname -m)-linux-gnu \
-c dns_egress_filter.bpf.c -o dns_egress_filter.bpf.otc_egress_dns yang siap dipasang:
BASH
llvm-objdump -h dns_egress_filter.bpf.oTEXT
dns_egress_filter.bpf.o: file format elf64-bpf
Sections:
Idx Name Size VMA Type
1 .text 00000000 0000000000000000 TEXT
2 tc_egress_dns 00000940 0000000000000000 TEXT
3 .maps 00000028 0000000000000000 DATA
4 .BTF 000005a0 00000000000000002. Inisialisasi Qdisc clsact dan Attachment Filter
clsact terpasang pada interface jaringan (contoh: eth0) sebelum filter dapat disematkan ke jalur egress:
BASH
# Buat qdisc clsact pada interface eth0 jika belum ada
tc qdisc add dev eth0 clsact
# Pasang program eBPF pada hook egress
tc filter add dev eth0 egress bpf direct-action \
obj dns_egress_filter.bpf.o sec tc_egress_dnsdirect-action (disingkat da) menginstruksikan kernel bahwa nilai kembali (return code) dari fungsi eBPF C—seperti TC_ACT_OK (0) atau TC_ACT_SHOT (2)—harus langsung dieksekusi sebagai aksi TC tanpa perlu mencocokkannya dengan action classifier terpisah dari tc.
3. Validasi Status dan Telemetri Filter
BASH
tc -s filter show dev eth0 egressTEXT
filter protocol all pref 49152 bpf chain 0
filter protocol all pref 49152 bpf chain 0 handle 0x1 dns_egress_filter.bpf.o:[tc_egress_dns] direct-action not_in_hw id 142 tag e28f9d8a93cb02a1 jited
action order 1: run 142 id 142 default-action pipe
index 1 ref 1 bind 1 installed 420 sec used 0 sec
Action statistics:
Sent 1248092 bytes 8520 pkt (dropped 14, overlimits 0 requeues 0)
backlog 0b 0p requeues 0dropped 14 mengonfirmasi bahwa 14 transaksi anomali yang membawa signature label panjang atau melanggar token bucket berhasil dieliminasi sebelum diserahkan ke hardware transmitter.
Observabilitas Runtime Menggunakan bpftrace dan bpftool
bpftool.
Inspeksi Map State via bpftool
domain_rate_map yang terpasang pada kernel:
BASH
bpftool map show name domain_rate_mapTEXT
87: lru_hash name domain_rate_map flags 0x0
key 4B value 16B max_entries 16384 memlock 1314816BBASH
bpftool map dump id 87TEXT
key: 2f 8a 44 a1 value: 48 c3 da 1e 38 00 00 00 03 00 00 00 00 00 00 00
key: e8 12 09 3b value: 90 98 de 1e 38 00 00 00 00 00 00 00 00 00 00 00
Found 2 elementstokens pada entri kedua menunjukkan angka 0 (00 00 00 00), yang mengindikasikan domain hash 0x3b0912e8 telah menghabiskan alokasi kuota burst dan paket-paket berikutnya akan dibuang pada egress.
Tracing Aksi Drop Menggunakan bpftrace
bpf_trace_printk() yang lambat, pasang dynamic probe menggunakan bpftrace untuk merekam event kfree_skb yang dipicu oleh tindakan TC_ACT_SHOT:
BASH
bpftrace -e 'kprobe:kfree_skb /arg1 != 0/ { printf("skb freed: %p at func: %s
", arg0, ksym(arg1)); }'perf:
BASH
perf top -C 0-3 --sort comm,dso,symbolsoftirq jaringan, overhead instruksi filter_egress_dns yang telah ter-JIT (Just-In-Time compiled) rata-rata menghabiskan kurang dari 0.4% total runtime cycle, membuktikan efisiensinya dibandingkan inspeksi proxy berbasis user-space.
Analisis Trade-off: False Positive, Fragmentation, dan Memory Bounds
1. DNS Name Compression Pointer
11 atau 0xC0). Dalam transaksi normal, pointer kompresi lazim ditemukan pada Response Section (jawaban resolver), namun sangat jarang digunakan dalam Question Section pada paket Query keluar, kecuali pada implementasi resolver kustom tertentu. Kode eBPF di atas secara defensif membuang paket dengan flag pointer di Question section (len > MAX_LABEL_LEN) untuk memitigasi teknik evasion label length bypass. Pada skenario produksi tertentu dengan legacy system, kondisi ini harus diaudit agar tidak menimbulkan false positive.
2. IP Fragmentation (UDP Jumbograms)
TC_ACT_OK karena ip->frag_off tidak diisolasi secara khusus. Namun, karena fragment pertama (yang memuat QNAME awal) tetap terfilter, payload tunneling yang berada di permulaan QNAME tetap akan terpotong, menggagalkan pembentukan koneksi C2 secara fungsional.
3. Memory Footprint Kernel BPF Map
max_entries pada BPF_MAP_TYPE_LRU_HASH menentukan batas penggunaan alokasi memory kernel. Dengan konfigurasi 16.384 entri:
TEXT
Ukuran Per Entri: Key (4 Byte) + Value (16 Byte) + Overhead Hash Table Kernel (~64 Byte) ≈ 84 Byte
Total Footprint: 16.384 * 84 Byte ≈ 1.37 MB