
اكتشاف تحميلات الذاكرة التي يستحدثها المترجم وتحوّل الكود الآمن المكتوب بلغة C إلى ثغرات TOCTOU. يشمل تدقيقات آلية لمصدر الكود، وتحليلًا ثنائيًا قائمًا على Unicorn، ومسوحات للمترجمات/المعماريات/الأعلام عبر أكثر من 100 مشروع.
"...تعريف «المُصرّف السليم» يزداد تراخيًا."
الملف التنفيذي الذي تُشغّله ليس البرنامج الذي كتبته. يُعيد مُحسِّن المُصرّف كتابة شفرتك المصدرية بطرق لا تراها أبدًا — وبعض هذه التغييرات يمكنها، بصمت وبشكل مشروع، أن تحوّل شيفرة تبدو آمنة إلى ملفات تنفيذية قابلة للاستغلال. قد يكون السطر نفسه آمنًا مع مُصرّف وقابلًا للاستغلال مع آخر، دون ما في الشيفرة المصدرية يُخبرك بأيهما: ثغرة معلّقة في تراكب كمّي، لا تنهار إلا عند البناء. يستكشف 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، التي تفترض أن الذاكرة لا يمكن أن تتغير بين قراءتين. لكن عندما تكون تلك الذاكرة قابلة للكتابة من قبل المهاجم، يتحول الافتراض إلى استغلال: إذ يمكن أن يقع التحميل المُبتكَر بعد فحص أمني، معيدًا بصمت فتح نافذة من نوع التحقق من الوقت إلى الاستخدام (TOCTOU) التي ظن المبرمج أنه أغلقها. فالقيمة التي تحققت منها والقيمة التي تستخدمها لم يعودا مضمونين أن يكونا متماثلين — رغم أنك لم تكتب أبدًا كودًا يعيد قراءتها.
يُثبت التحدي وجود التحميل المُبتكَر؛ فلنرَ كيف يتحول ذلك إلى تلفٍ في الذاكرة.
في ثغرة من نوع TOCTOU، يتحقق البرنامج من أن القيمة آمنة ثم يستخدمها. ومع ذلك، توجد نافذة للاستغلال إذا استطاع المهاجم تغيير القيمة في جزء صغير من الوقت بين هاتين القراءتين — فالقيمة غير الضارة تجتاز الفحص بينما الخطيرة هي التي تُستخدم:```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
الإصلاح الكلاسيكي هو **اللقطة أولاً**: انسخ أي بيانات قد يعبث بها المهاجم
في متغير محلي لا يمكن للمهاجم الوصول إليه، ثم لا تثق إلا
بذلك المتغير المحلي. بمجرد أن يصبح `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]
يعمل الفحص على READ #1؛ القيمة التي تستقر في `slot.len` هي READ #2. المهاجم الذي يقلب `len` بينهما يمرّر قيمة آمنة إلى فحص `<= 20` بينما تُنشر قيمة ضخمة في `slot` — وبعدها تنسخ `forward` ذلك العدد من البايتات إلى `out[20]`، أي الفيض نفسه الذي كانت اللقطة تهدف إلى منعه، ويعيده المُحسِّن إلى الحياة.
هذا يتحوّل إلى إثبات مفهوم كامل في [`poc/example.c`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/poc/example.c)، حيث يستخدم الكود النهج المتعارف عليه لتحصين TOCTOU: بنية `message` غير الموثوقة تُلتقط في `local` بحيث لا يمكن تعديلها، ويتم التحقق من `local.len` الخاص باللقطة مقابل سعة المخزن المؤقت، ولا يُنشر في `slot` إلا النسخة الموثقة؛ ويقوم مستهلك لاحقًا بنسخ عدد من البايتات بقدر `slot.len` من الحمولة إلى مخزن مؤقت ثابت. في الوقت نفسه، ينافس المهاجم على `shared->len`. حمل مبتكر غير متوقع من المترجم يعيد قراءة `shared->len` من أجل النشر الجماعي، لذلك يحمل `slot.len` قيمة المهاجم الضخمة على الرغم من نجاح الفحص — معيدًا إدخال TOCTOU الذي كان المبرمج يحاول الدفاع ضده، ومنشئًا فيض مخزن مؤقت يبدو مستحيلًا — من العدم.
## السبب
> *بحلول الوقت الذي يصل فيه C إلى كود الآلة، يكون قد أعيد تشكيله عبر تخفيض الواجهة الأمامية، وتحسينات IR، وتخصيص السجلات، وتوليد الكود الخلفي — خط أنابيب عميق متعدد المراحل يتخذ قرارات لا يمكنك رؤيتها. لا توجد مرحلة واحدة تُلام. الحمل المبتكر خاصية ناشئة لخط الأنابيب بأكمله، وليس خطأً في أي جزء منه.*
عند هذه النقطة: يمكن للمترجمين *أن يصدروا* حمولات مبتكرة، والنمط نفسه الذي كان يهدف إلى منع الخطأ — الالتقاط، التحقق، الاستخدام — هو ما يعيد إدخاله. الخطوة التالية (لمعرفة ما إذا كنا عرضة فعليًا للخطر) هي توصيف *متى* يحدث ذلك. يتبيّن أن ذلك صعب.