
कंपाइलर-निर्मित मेमोरी लोड का पता लगाएं जो सुरक्षित C को TOCTOU कमजोरियों में बदल देते हैं। इसमें स्वचालित सोर्स ऑडिट, Unicorn-आधारित बाइनरी विश्लेषण, और 100+ प्रोजेक्ट्स में कंपाइलर/आर्क/फ्लैग स्वीप शामिल हैं।
..."'sane compiler' की परिभाषा और ढीली होती जाती है।"
आप जो बाइनरी चलाते हैं वह वह प्रोग्राम नहीं है जो आपने लिखा था। कंपाइलर ऑप्टिमाइज़र पुनर्लेखित
आपके सोर्स को ऐसे तरीकों से जिन्हें आप कभी नहीं देख पाते — और उनमें से कुछ परिवर्तन
चुपचाप और वैध रूप से प्रतीत होने वाले सुरक्षित कोड को
भेद्य बाइनरियों में बदल सकते हैं। एक ही पंक्ति
एक कंपाइलर के अंतर्गत सुरक्षित और दूसरे के अंतर्गत शोषण योग्य,
और सोर्स में कुछ भी नहीं जो आपको बताए कि कौन-सा है: एक भेद्यता जो
सुपरपोज़िशन में रखी है, केवल बिल्ड करने पर संघटित होती है। Schrödinger's TOCTOU अन्वेषण करता है
कंपाइलर-निर्मित लोड तथा उनके व्यापक
समय-जाँच से समय-उपयोग (TOCTOU) भेद्यताओं के लिए निहितार्थ — पाए गए
विभिन्न ओपन-सोर्स
कर्नेल,
हाइपरवाइज़र,
एन्क्लेव,
फर्मवेयर,
और लाइब्रेरियों में।
हम जहाँ भी देखते हैं, प्रतीत होने वाला सुरक्षित कोड कंपाइलर की मनमर्जी के
प्रति खुला छोड़ दिया गया है। लेकिन वे एक नमूना हैं, सीमा नहीं;
वही बग बहुत संभवतः आपके कोड में भी हैं।
"कुछ आसान से शुरू करें।"
यह फ़ंक्शन *p को कितनी बार लोड करता है?```c
unsigned int g(unsigned short *p)
{
short t = p; / copy *p into a local for safekeeping */
return (unsigned short)t - t;
}
संकेत: उत्तर 1 है — स्रोत `*p` को केवल एक बार `t` में लोड करता है।
इसे [Compiler Explorer](https://godbolt.org/z/c5K9P4dPd) में पेस्ट करें (`arm gcc 14.2.0`, `-O2`) और `r0` से होने वाले लोड्स को गिनें, जो `p` रखता है:```asm
g:
ldrh r2, [r0] # load *p, once
ldrsh r0, [r0] # load *p, twice
subs r0, r2, r0
bx lr
स्रोत में एक लोड, बाइनरी में दो। दूसरा एक आविष्कृत लोड है — एक ऐसा रीड जिसे कंपाइलर ने बनाया और आपने कभी नहीं लिखा। यह C के abstract machine के अंतर्गत कानूनी है, जो मानता है कि दो रीड्स के बीच मेमोरी नहीं बदल सकती। लेकिन जब वह मेमोरी attacker-writable है, तो यह धारणा एक exploit बन जाती है: यह आविष्कृत लोड एक सुरक्षा जाँच के बाद आ सकता है, चुपचाप एक time-of-check to time-of-use (TOCTOU) विंडो को फिर से खोलता है जिसे प्रोग्रामर ने बंद कर दिया था। जिस मान को आपने सत्यापित किया और जिस मान का आप उपयोग करते हैं, अब समान होने की गारंटी नहीं है — भले ही आपने वह कोड कभी नहीं लिखा जो उसे दोबारा पढ़ता।
यह चुनौती साबित करती है कि आविष्कृत लोड मौजूद है; देखते हैं कि वह कैसे मेमोरी भ्रष्टाचार में बदल जाता है।
TOCTOU भेद्यता में, एक प्रोग्राम जाँचता है कि एक मान सुरक्षित है, फिर उस मान का उपयोग करता है। हालाँकि, exploitation की एक खिड़की मौजूद होती है यदि कोई हमलावर उन दो रीड्स के बीच के क्षण में मान को बदल सकता है – हानिरहित मान जाँच पास कर लेता है जबकि खतरनाक मान वही होता है जिसका उपयोग किया जाता है:```c if (shared->len <= 20) // CHECK reads shared->len // ** attacker modifies shared->len ** memcpy(out, shared->data, shared->len); // USE reads it again: buffer overflow
पाठ्यपुस्तकीय समाधान है **पहले स्नैपशॉट लेना**: हमलावर जिस भी डेटा के साथ छेड़छाड़ कर सकता है, उसे ऐसे स्थानीय (local) में कॉपी करें जहाँ हमलावर नहीं पहुँच सकता, और फिर उस स्थानीय के अलावा किसी और चीज़ पर भरोसा न करें। एक बार `len` स्थानीय में आ जाए, यह स्थिर हो जाता है — साझा मेमोरी के साथ प्रतिस्पर्धा करने वाला हमलावर अब उसे छू नहीं सकता — इसलिए जाँच और प्रतिलिपि को एक ही मान देखने की गारंटी मिलती है। इसी प्रकार नीचे `receive` में दिया गया कोड TOCTOU को ठीक करता है: यह संदेश का स्नैपशॉट लेता है, स्नैपशॉट को मान्य करता है, और मान्य की गई प्रति को `slot` में प्रकाशित करता है ताकि एक उपभोक्ता उसे आगे भेज सके:```c
#include <string.h>
struct message {
int len; /* payload length */
char data[20]; /* payload */
};
struct message slot; /* the most recently validated message */
char out[20]; /* fixed 20-byte destination */
void receive(struct message *shared) {
struct message local = *shared; /* 1. snapshot untrusted input */
if (local.len <= 20) /* 2. validate the snapshot */
slot = local; /* 3. publish the validated copy */
}
void forward(void) { /* the time of use, later */
memcpy(out, slot.data, slot.len); /* slot.len was checked <= 20 ... right? */
}
स्रोत के अनुसार, यह सही है। len को ठीक एक बार पढ़ा जाता है — स्नैपशॉट में —
इसलिए जो मान <= 20 जाँच को पास कराता है वही मान slot में प्रकाशित होता है।
TOCTOU विंडो बंद हो जाती है और कोड सुरक्षित है।
सिवाय इसके कि यह सुरक्षित नहीं है। x86-64 gcc -O2 के अंतर्गत, receive इसे
मूल साझा मेमोरी से दो बार पढ़ता है: एक बार
चेक को गेट करने के लिए एक स्केलर के रूप में, और फिर बल्क
कॉपी के हिस्से के रूप में जो slot में प्रकाशित होती है:```nasm
receive:
cmp DWORD PTR [rdi], 20 ; READ #1: the CHECK reads shared->len directly
movdqu xmm0, XMMWORD PTR [rdi] ; READ #2: the bulk copy re-reads it (len is byte 0)
mov rax, QWORD PTR [rdi+16] ; (the bulk copy's tail: struct bytes 16-23)
jg .L1 ; len > 20? skip the publish
mov QWORD PTR slot[rip+16], rax ; (publish that tail)
movaps XMMWORD PTR slot[rip], xmm0 ; and publish the TOCTOU-vulnerable snapshot
.L1:
ret
forward:
movsx rdx, DWORD PTR slot[rip] ; copy size = slot.len, the unchecked READ #2 value
mov esi, OFFSET FLAT:slot+4 ; src = slot.data
mov edi, OFFSET FLAT:out ; dst = out[20]
jmp memcpy ; copies slot.len bytes into out[20]
The check runs on READ #1; the value that lands in `slot.len` is READ #2. An
attacker who flips `len` between them passes a safe value to the `<= 20` check
while an oversized one is published into `slot` — and `forward` then copies that
many bytes into `out[20]`, the exact overflow the snapshot was meant to prevent,
reintroduced by the optimizer.