Skip to content

eBPF ALU32 bounds-tracking container escape (CVE-2021-3490)

The eBPF verifier's scalar32_min_max_and/or/xor helpers failed to update 32-bit bounds for bitwise ops, leaving a register the verifier believes is constrained but which is unbounded at runtime — usable for out-of-bounds read/write on a BPF map and full local privilege escalation / container escape.

Mechanism

Kaçırılan bir 32-bit bounds güncellemesi verifier'ı neden alt eder

eBPF verifier'ı her register için hem bir tnum'u (known/unknown bit'ler) hem de iki genişlikte sayısal bounds'u takip ederek memory safety'yi kanıtlar: 64-bit (umin_value/umax_value, smin/smax) ve 32-bit (u32_min_value/u32_max_value, s32_min/s32_max). Her ALU op'undan sonra verifier bunların hepsini yeniden hesaplamalıdır ki static bounds, register'ın tutabileceği her runtime değerini sağlam biçimde over-approximate etsin. Herhangi bir bound gerçekten geniş bırakılırsa ya da daha kötüsü kendi içinde tutarsız olursa, safety proof'u sağlam değildir.

Bitwise op'lar için 32-bit yarısı scalar32_min_max_and, scalar32_min_max_or ve scalar32_min_max_xor tarafından işlenir. Bug:

/* scalar32_min_max_and (buggy) */
bool src_known = tnum_subreg_is_const(src_reg->var_off);
bool dst_known = tnum_subreg_is_const(dst_reg->var_off);
...
if (src_known && dst_known)
    return;   /* assumes the 64-bit handler already set bounds */

tnum_subreg_is_const() yalnızca alt 32 bit'in sabit olup olmadığını kontrol eder. Erken return, 64-bit kardeş scalar_min_max_and()'in bounds'u sonlandırmış olacağını varsayar — ama o fonksiyon tnum_is_const() üzerinden gate'lenir, ki bu tüm 64 bit'in sabit olmasını gerektirir. Dolayısıyla bir register'ın alt 32 bit'i known ama yüksek bit'leri unknown olduğunda, 32-bit handler vazgeçer ve 64-bit handler da reddeder, ve 32-bit bounds asla güncellenmez.

Geriye kalan değerler op-öncesi default'lardır ve ters çevrilebilir:

u32_min_value = 1
u32_max_value = 0     (umin > umax: impossible, yet accepted)

umin > umax ile verifier'ın sonraki akıl yürütmesi (conditional jump'lar, scalar arithmetic) öyle yönlendirilebilir ki verifier bir register'ın küçük, in-bounds bir değer (örneğin 0) tuttuğu sonucuna varırken runtime'da o başka bir şey (örneğin 1, sonra ölçeklenmiş) tutar. O register sonra bir BPF map value'suna index/offset olarak kullanılır; bu da map'in arkadaki allocation'ının ötesine out-of-bounds read ve write verir — programın dokunmaya asla yetkili olmadığı kernel belleği.

AND/OR variant'ları 3f50f132d840 commit'inde ("bpf: Verifier, do explicit ALU32 bounds tracking", v5.7-rc1) eklendi, XOR variant'ı ise 2921c90d4718'de ("bpf: Fix a verifier failure with xor", v5.10-rc1). Fix, 049c4e13714e commit'idir (v5.13-rc4, 5.12.4 / 5.11.21 / 5.10.37'ye backport edildi); erken return; yerine subreg'i known olarak işaretler:

/* fix: 049c4e13714e */
if (src_known && dst_known) {
    __mark_reg32_known(dst_reg, var32_off.value);   /* was: return; */
    return;
}

Stock Ubuntu'da unprivileged eBPF default olarak etkin olduğu için bug, sıradan bir kullanıcı / container'dan LPE'ye ve container escape'e ulaşır.

Walkthrough

Referans exploit Manfred Paul'un bug'ıdır, chompie1337 tarafından silahlandırılmıştır (Ubuntu 20.04.02 / 20.10, kernel 5.8.0-25.26'dan 5.8.0-52.58'e ve 21.04 5.11.0-16.17 üzerinde test edildi).

1. Unknown scalar'ı oluştur. Bir BPF array map'ten bir değer yükle; verifier onu tamamen unknown olarak işaretler:

// after bpf_map_lookup_elem + deref:
// reg.var_off = {mask: 0xffffffffffffffff, value: 0x0}  (entirely unknown)

2. Alt 32 bit'i known bir sabite kısıtla. Mask'le/ekle ki alt yarı known (1) olurken üst yarı unknown kalsın — tam olarak tnum_subreg_is_const'un "const" dediği ama tnum_is_const'un demediği durum:

val &= 0x...;   // force low 32 bits known
val += 1;       // low 32 bits == 1, high 32 bits still unknown

3. Kaçırılan güncellemeyi tetikle. Üst yarısı non-trivial olan bir 64-bit sabit ile AND'le ki 64-bit handler da reddetsin:

val &= 0x100000002ULL;   // hits scalar32_min_max_and early-return path
// verifier now leaves u32_min_value=1, u32_max_value=0 (inverted)

4. Bir "in-bounds" offset forge et. Bozulmuş register üzerinde conditional jump'lar ve arithmetic kullanarak, verifier'ın 0 olduğunu (ve dolayısıyla güvenli bir offset olduğunu) kanıtladığı ama runtime'da sıfırdan farklı olan bir değer inşa et. Onu ikinci bir map value'suna index olarak kullan:

verifier's view:  offset == 0   -> access is in bounds, program accepted
runtime reality:  offset != 0   -> read/write lands past the map allocation

5. OOB read/write -> arbitrary kernel R/W. OOB komşu slab/heap object'lerine düşer. Exploit, adres leak'lemek için komşu bpf_map metadata'sını okur ve komşu bir map'in data pointer'ını bozar; böylece arbitrary kernel read ve write'ı bootstrap eder.

6. Privilege escalation. Arbitrary R/W ile mevcut task'ın cred'ini uid 0'a overwrite et (bkz. ../kernel/cred-struct-overwrite.md ve ../kernel/commit-creds.md). Kernel-seviyesi R/W process- ve namespace-bağımsızdır, dolayısıyla aynı primitive, host kernel'inin task credential'larını / namespace pointer'larını doğrudan düzenleyerek bir container'dan kaçar.

Beklenen başarılı çalıştırma (chompie1337 PoC)
$ ./exploit
[*] creating bpf maps...
[*] loading ebpf program (triggers verifier bug)...
[+] program accepted by verifier
[*] leaking kernel addresses via OOB read...
[+] kernel base: 0xffffffff8....
[*] obtaining arbitrary read/write...
[*] overwriting cred struct...
[+] got root!
# id
uid=0(root) gid=0(root) groups=0(root)

Detection

  • Bug bir verifier-acceptance kusurudur, dolayısıyla iyi biçimlenmiş kötü niyetli bir program sessizce load olur — loglanacak bir hata yoktur. Policy katmanında tespit edin: unprivileged / container'lı process'lerden gelen bpf() syscall'larını (BPF_PROG_LOAD) auditd ya da bir Falco kuralı ile denetleyin.
  • Yamalı kernel'lerde aynı program bir verifier hatasıyla reddedilir ("math between ... pointer and register with unbounded ...").

Mitigation

  • 049c4e13714e commit'ini içeren bir kernel'e yamalayın (5.13-rc4 ya da backport'lar 5.12.4 / 5.11.21 / 5.10.37).
  • kernel.unprivileged_bpf_disabled=1 ayarlayın ki non-root / non-CAP_BPF process'ler eBPF program'ları load edemesin — bu, bu sınıf verifier bug'ının attack surface'ını tamamen kaldırır.
  • Container'lar için CAP_BPF/CAP_SYS_ADMIN'i drop edin ve bpf syscall'ını bloklayan bir seccomp profili uygulayın.

References