eBPF improper program verification OOB (CVE-2020-8835)¶
The verifier's 32-bit jump bounds refinement marked register bits "known" that were not, letting a register the verifier believes is 0 hold 1 at runtime — an OOB read/write primitive (Manfred Paul, Pwn2Own 2020).
Mechanism¶
Note
Her BPF register durumu umin_value/umax_value (unsigned 64-bit aralık),
smin/smax (signed aralık) ve var_off (known/unknown bit'lerden oluşan bir tnum)
taşır. Bir 32-bit conditional jump variant'ı (BPF_JMP32) yalnızca alt 32 bit'i
karşılaştırır, dolayısıyla verifier register'ın 32-bit görünümünü, tam 64-bit değeri
over-constrain etmeden güncellemelidir.
Note
Bir 32-bit conditional jump'tan (is_jmp32) sonra, kernel/bpf/verifier.c içindeki
__reg_bound_offset32(), register'ın tnum'unu alt 32 bit'ini
tnum_range(reg->umin_value & mask, reg->umax_value & mask) ile kesiştirerek
daraltıyordu. Bu, low-32 bounds arasındaki her değerin bounds'un bit pattern'ını
paylaştığını yanlışça varsayıyordu. ZDI'nın deyişiyle: umin ve umax'ın ikisinin de
00...01 ile bitmesi, aralarındaki her değerin de öyle bittiği anlamına gelmez.
Aslında "known" olmayan bit'ler "known" işaretlenir, dolayısıyla attacker
verifier'ın 0 olarak modellediği ama runtime'da 1 olan bir register hazırlar.
Bug'lı daraltma:
static void __reg_bound_offset32(struct bpf_reg_state *reg)
{
u64 mask = 0xffffFFFF;
struct tnum range = tnum_range(reg->umin_value & mask, reg->umax_value & mask);
struct tnum lo32 = tnum_cast(reg->var_off, 4);
struct tnum hi32 = tnum_lshift(tnum_rshift(reg->var_off, 32), 32);
reg->var_off = tnum_or(hi32, tnum_intersect(lo32, range));
}
Bu fake-zero'yu bir ölçekle (örneğin 6000) çarpıp bir map-value pointer'ından çıkarmak verification'ı geçer (offset 0 sanılır) ama runtime'da out of bounds'tur.
Hatalı çıkarımın özü — [umin, umax] aralığını tnum_range ile bit-pattern'a
çevirmek, uçların paylaştığı bit'leri tüm aralık için "known" sayar:
register value (alt 4 bit), aralık [umin=0b0000 .. umax=0b0011]:
umin = 0 0 0 0
umax = 0 0 1 1
| | | |
| | | +-- bit gerçekten "known 0/1" değil
| | +---- bit gerçekten "known" değil
| +------ üst 2 bit her iki uçta da 0 -> doğru: known 0
+-------- (tnum_range, üst bit'leri 0 olarak işaretler: OK)
aralıktaki gerçek değerler: 0000, 0001, 0010, 0011
^^^^^^^^^^
alt 2 bit hem 0 hem 1 olabilir = UNKNOWN
verifier'ın "0" modeli : x x 0 0 (alt bit'leri yanlışça 0 sabitledi)
runtime'da mümkün olan : x x ? ? <- buradan "0 sanılan ama 1 olan" register
not: gerçek tnum/bit genişlikleri kernel sürümüne göre değişir; bu yalnızca
mantık hatasının kavramsal şemasıdır.
Walkthrough¶
Exploit, OOB'yi bir BPF map üzerinden arbitrary kernel R/W'ye çevirir:
- Register'ı öyle hazırla ki verifier
0görsün ve runtime1görsün; onunla bir OOB map-element pointer'ı inşa et. - Map'in
btfpointer'ını OOB-overwrite et, sonra onuBPF_OBJ_GET_INFO_BY_FDüzerinden (btf_idalanı) geri oku — bir arbitrary read. array_map_ops'u map data'sına kopyala ve kontrollü bir write içinmap_push_elem'imap_get_next_key'e yeniden yönlendir (gadget*next = index + 1;).init_pid_ns'tencredstruct'ına kadar yürü ve root içinuid/gid'i sıfırla (ya damodprobe_path'i overwrite et).
Warning
Bug, 581738a681b6 ("provide better register bounds after jmp32", mainline 5.5)
tarafından eklendi ve f2d67fec0b43'te ("Undo incorrect __reg_bound_offset32 handling")
düz bir revert ile düzeltildi. 5.6.1 / 5.5.14 / 5.4.29'da fix'lendi. Unprivileged
istismar, unprivileged_bpf_disabled'ın kapalı olmasını gerektirir. Aynı 32-bit ALU
bounds mistracking ailesi sonraki CVE'lerde tekrar ortaya çıkar (örneğin CVE-2021-3490).
Mitigation¶
sysctl kernel.unprivileged_bpf_disabled=1, non-root için attack surface'ı kaldırır;
upstream fix bug'lı daraltmayı tamamen kaldırdı. CONFIG_BPF_JIT_ALWAYS_ON ve slab
hardening, OOB-sonrası istismar edilebilirliği azaltır ama verifier kusurunun kendisini
azaltmaz.