
Zig में CVE-2021-25741 POC
CVE-2021-25741 का शैक्षिक एक्सप्लॉइट, जो कि Kubernetes kubelet में एक भेद्यता है। यह एक रेस कंडीशन TOCTOU (Time Of Check, Time Of Use) पर आधारित है।
यह प्रोजेक्ट स्थानीय स्तर पर (K8s क्लस्टर के बिना) दर्शाता है कि कैसे symlink का समवर्ती स्वैप lstat() और open() के बीच पथ सत्यापन को बायपास कर सकता है, और सिद्ध करता है कि RESOLVE_* फ़्लैग के साथ openat2() समाधान है।
जब kernel /a/b/c/file जैसे पथ को हल करता है, तो वह इसे घटक दर घटक करता है। प्रत्येक चरण पर, यदि घटक एक symlink है, तो kernel स्वचालित रूप से उसका अनुसरण करता है। यह समाधान एटॉमिक नहीं है — दो चरणों के बीच फ़ाइल सिस्टम बदल सकता है।
Kubernetes kubelet ने ठीक यही भेद्य पैटर्न अपनाया था:
1. CHECK (जाँच) : lstat(subPath) → "यह एक निर्देशिका है, सुरक्षित है"
↕ रेस विंडो — कंटेनर में एक प्रक्रिया निर्देशिका को symlink से बदल देती है
2. USE (उपयोग) : mount(subPath) → symlink का अनुसरण करता है, होस्ट फ़ाइल सिस्टम को माउंट करता है
जाँच और माउंट के बीच, कंटेनर में एक दुर्भावनापूर्ण प्रक्रिया subPath को होस्ट के / की ओर इशारा करने वाले symlink से बदल सकती थी, जिससे होस्ट फ़ाइल सिस्टम तक पूर्ण पहुँच प्राप्त हो जाती थी।
openat2 syscall (kernel 5.6+) पथ को हल करता है और फ़ाइल को एटॉमिक रूप से खोलता है, सीमाओं के साथ:
| फ़्लैग | प्रभाव |
|---|---|
RESOLVE_NO_SYMLINKS | किसी भी symlink का अनुसरण करने से इंकार → ELOOP लौटाता है |
RESOLVE_BENEATH | आधार निर्देशिका से बाहर जाने से इंकार → EXDEV लौटाता है |
RESOLVE_IN_ROOT | dirfd को फ़ाइल सिस्टम की जड़ मानता है |
RESOLVE_NO_XDEV | माउंट पॉइंट पार करने से इंकार |
openat2 के साथ, कोई TOCTOU विंडो नहीं है: यदि समाधान के दौरान कोई symlink दिखाई देता है, तो syscall तुरंत विफल हो जाता है।
दो थ्रेड TOCTOU विंडो का शोषण करने के लिए सहयोग करते हैं:
workdir/
├── legit_dir/
│ └── secret.txt → सामग्री "LEGIT"
├── symlink_target/
│ └── secret.txt → सामग्री "PWNED"
└── target/ → वास्तविक निर्देशिका और symlink के बीच स्वैप होता है
अत्यंत तेज़ लूप में घूमता है और renameat2(RENAME_EXCHANGE) के माध्यम से target/ को दो अवस्थाओं के बीच एटॉमिक रूप से स्वैप करता है:
target/ एक वास्तविक निर्देशिका है (इसमें secret.txt = "LEGIT" है)target/ एक symlink है जो symlink_target/ की ओर इशारा करता है (इसमें secret.txt = "PWNED" है)प्रति स्वैप केवल एक syscall = अधिकतम रेस विंडो।
kubelet के भेद्य पैटर्न को दोहराता है:
fstatat("target", AT_SYMLINK_NOFOLLOW) — सत्यापित करता है कि यह एक निर्देशिका हैopenat(dirfd, "target/secret.txt", O_RDONLY) — फ़ाइल खोलता हैread() — सामग्री पढ़ता है"PWNED" → रेस जीत (symlink का अनुसरण हुआ)"LEGIT" → रेस हार (यह वास्तविक निर्देशिका थी)सुरक्षित मोड (--use-openat2) में, चरण 3 RESOLVE_NO_SYMLINKS | RESOLVE_BENEATH के साथ openat2 का उपयोग करता है। यदि कोई symlink मौजूद है, तो kernel उसका अनुसरण करने के बजाय ELOOP लौटाता है।
src/
├── main.zig # प्रवेश बिंदु: CLI पार्स, साझा स्थिति बनाता है, थ्रेड स्पॉन करता है,
│ # समय मापता है, परिणाम प्रदर्शित करता है
│
├── racer.zig # Racer थ्रेड: प्रारंभिक अवस्था तैयार करता है (target → symlink),
│ # फिर renameat2(RENAME_EXCHANGE) पर लूप करके target/ और legit_dir/ को स्वैप करता है
│
├── victim.zig # पीड़ित थ्रेड: fstatat → delay → openat/openat2 → read → "LEGIT" बनाम "PWNED" तुलना के N पुनरावृत्तियाँ
│
├── setup.zig # परीक्षण संरचना बनाता है: workdir/, legit_dir/,
│ # symlink_target/, target/ सेंटिनल फ़ाइलों के साथ
│
├── syscalls.zig # रॉ syscall के लिए स्थिरांक और रैपर:
│ # - open_how संरचना (kernel UAPI, 3×u64)
│ # - RESOLVE_* फ़्लैग
│ # - RENAME_EXCHANGE (= 2)
│ # - openat2() via linux.syscall4(.openat2, ...)
│ # - rename_exchange() via linux.renameat2()
│ # - सहायक: is_err(), to_errno(), to_fd()
│
└── stats.zig # लॉक-फ्री एटॉमिक काउंटर (std.atomic.Value(u64))
# wins, losses, errors, eloop के लिए + स्वरूपित प्रदर्शन
सभी syscalls सीधे std.os.linux.* के माध्यम से कॉल किए जाते हैं (std.fs या std.posix रैपर नहीं)। Zig 0.15 stdlib में बिना रैपर के एकमात्र syscall openat2 है, जिसे हम linux.syscall4(.openat2, ...) के माध्यम से कॉल करते हैं, जिसमें कर्नेल UAPI हेडर से मैन्युअल रूप से परिभाषित open_how संरचना होती है।
| उपकरण | संस्करण | क्यों |
|---|---|---|
| Zig | 0.15.x | कंपाइलर + क्रॉस-कम्पाइलेशन |
| Docker | कोई भी | Mac पर Linux बाइनरी चलाने के लिए |
| colima | कोई भी | macOS पर Docker रनटाइम (या Docker Desktop) |
बाइनरी स्टैटिक (musl libc) कम्पाइल की जाती है और बिना किसी निर्भरता के किसी भी Linux पर चलती है।
न्यूनतम कर्नेल:
renameat2(RENAME_EXCHANGE) के लिए 3.15+RESOLVE_* के साथ openat2 के लिए 5.6+ (केवल --use-openat2 के लिए आवश्यक)Mac पर Docker Desktop और colima 6.x कर्नेल का उपयोग करते हैं — सब कुछ समर्थित है।
# ARM64 (Mac M1/M2/M3 → Docker colima / EC2 ARM)
zig build -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe
# x86_64 (x86 नोड्स के लिए)
zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe
बाइनरी zig-out/bin/race-exploit में है।
$ file zig-out/bin/race-exploit
ELF 64-bit LSB executable, ARM aarch64, statically linked
# Docker रनटाइम शुरू करें (यदि macOS)
colima start
# एक्सप्लॉइट चलाएँ (डिफ़ॉल्ट रूप से भेद्य मोड)
docker run --rm -v $(pwd)/zig-out/bin:/app alpine /app/race-exploit --iterations 10000
# भेद्य मोड (openat) — रेस काम करती है
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
/app/race-exploit --iterations 10000
# सुरक्षित मोड (openat2) — रेस अवरुद्ध
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
/app/race-exploit --iterations 10000 --use-openat2
या स्क्रिप्ट जो दोनों चलाती है:
./scripts/run_in_docker.sh --iterations 10000
पैरामीटर --delay-us lstat (जाँच) और openat (उपयोग) के बीच विलंब जोड़ता है। देरी जितनी लंबी, TOCTOU विंडो उतनी बड़ी, और जीत दर उतनी अधिक:
# बिना देरी — जीत दर ~25%
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
/app/race-exploit --iterations 50000
# 10µs — जीत दर ~35%
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
/app/race-exploit --iterations 50000 --delay-us 10
# 100µs — जीत दर ~50%
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
/app/race-exploit --iterations 50000 --delay-us 100
# 1000µs (1ms) — जीत दर ~50% (सीमित, racer बहुत तेज़ स्वैप करता है)
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
/app/race-exploit --iterations 10000 --delay-us 1000
वास्तविक kubelet में, subPath सत्यापन और bind mount के बीच विलंब कई मिलीसेकंड (API कॉल, mount namespace तैयारी, आदि) का होता है, जो वास्तविक परिस्थितियों में रेस को बहुत विश्वसनीय बनाता है।