
ورشة عمل Looney Tunables لتصعيد الامتيازات المحلية (CVE-2023-4911)
ورشة عمل Looney Tunables لتصعيد الصلاحيات المحلية (CVE-2023-4911) (لأغراض تعليمية فقط)
في الحوسبة، الموصِّل الديناميكي هو الجزء من نظام التشغيل الذي يقوم بتحميل وربط المكتبات المشتركة التي يحتاجها الملف القابل للتنفيذ عند تشغيله، عن طريق نسخ محتوى المكتبات من وحدة التخزين الدائمة إلى الذاكرة العشوائية (RAM)، وملء جداول القفز وإعادة توجيه المؤشرات.
على سبيل المثال، لدينا برنامج يستخدم مكتبة openssl لحساب تجزئة md5:``` $ head md5_hash.c #include <stdio.h> #include <string.h> #include <openssl/md5.h>
ld.so يحلل الملف الثنائي ويحاول العثور على مكتبة مرتبطة بـ <openssl/md5.h>```
$ ldd md5_hash
linux-vdso.so.1 (0x00007fffa530b000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f19cda00000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f19cd81e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f19ce032000)
كما نرى، يعثر على مكتبة التشفير اللازمة في /lib/x86_64-linux-gnu/libcrypto.so.3 أثناء بدء تشغيل البرنامج، يضع رمز هذه المكتبة في ذاكرة العملية ويربط جميع المراجع إلى هذه المكتبة.
عند بدء تشغيل البرنامج، يفحص هذا المُحمّل أولاً البرنامج لتحديد المكتبات المشتركة التي يتطلبها. ثم يبحث عن هذه المكتبات، ويحمّلها في الذاكرة، ويربطها بالملف التنفيذي في وقت التشغيل. وفي هذه العملية، يحل المُحمّل الديناميكي مراجع الرموز، مثل مراجع الدوال والمتغيرات، مما يضمن تجهيز كل شيء لتنفيذ البرنامج. ونظراً لدوره، يُعد المُحمّل الديناميكي شديد الحساسية من ناحية الأمان، إذ يعمل كوده بصلاحيات مرتفعة عندما يشغّل مستخدم محلي برنامجاً بمعرّف مستخدم أو معرّف مجموعة مضبوطاً.
Tunables هي ميزة في مكتبة GNU C تتيح لمؤلفي التطبيقات والقائمين على صيانة التوزيعات تغيير سلوك مكتبة التشغيل ليتناسب مع عبء العمل لديهم. ويتم تنفيذها كمجموعة من المفاتيح التي يمكن تعديلها بطرق مختلفة. الطريقة الافتراضية الحالية للقيام بذلك هي عبر متغير البيئة GLIBC_TUNABLES عن طريق تعيينه إلى سلسلة من أزواج name=value مفصولة بنقطتين رأسيتين. على سبيل المثال، المثال التالي يفعّل فحص malloc ويضبط عتبة اقتطاع malloc على 128 بايت:``` GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3 export GLIBC_TUNABLES
تمرير --list-tunables إلى المُحمِّل الديناميكي لطباعة جميع الخيارات القابلة للضبط مع القيم الدنيا والقصوى:```
$ /lib64/ld-linux-x86-64.so.2 --list-tunables
glibc.rtld.nns: 0x4 (min: 0x1, max: 0x10)
glibc.elision.skip_lock_after_retries: 3 (min: 0, max: 2147483647)
glibc.malloc.trim_threshold: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.perturb: 0 (min: 0, max: 255)
glibc.cpu.x86_shared_cache_size: 0x100000 (min: 0x0, max: 0xffffffffffffffff)
glibc.pthread.rseq: 1 (min: 0, max: 1)
glibc.cpu.prefer_map_32bit_exec: 0 (min: 0, max: 1)
glibc.mem.tagging: 0 (min: 0, max: 255)
في بداية تنفيذه مباشرةً، يستدعي ld.so الدالة __tunables_init() لاجتياز البيئة (في السطر 279)، بحثًا عن متغيرات GLIBC_TUNABLES (في السطر 282)؛ ولكل GLIBC_TUNABLES يعثر عليه، يقوم بنسخ هذا المتغير (في السطر 284)، ثم يستدعي parse_tunables() لمعالجة هذه النسخة وتنقيتها (في السطر 286)، وأخيرًا يستبدل GLIBC_TUNABLES الأصلي بهذه النسخة المنقّاة (في السطر 288):```C // (GLIBC ld.so sources in ./glibc-2.37/elf/dl-tunables.c) 269 void 270 __tunables_init (char **envp) 271 { 272 char *envname = NULL; 273 char *envval = NULL; 274 size_t len = 0; 275 char **prev_envp = envp; ... 279 while ((envp = get_next_env (envp, &envname, &len, &envval, 280 &prev_envp)) != NULL) 281 { 282 if (tunable_is_name ("GLIBC_TUNABLES", envname)) // searching for GLIBC_TUNABLES variables 283 { 284 char new_env = tunables_strdup (envname); 285 if (new_env != NULL) 286 parse_tunables (new_env + len + 1, envval); // 287 / Put in the updated envval. */ 288 *prev_envp = new_env; 289 continue; 290 }
الوسيطة الأولى لدالة parse_tunables() (tunestr) تشير إلى النسخة التي سيتم تعقيمها قريبًا من GLIBC_TUNABLES، بينما تشير الوسيطة الثانية (valstring) إلى متغير البيئة الأصلي GLIBC_TUNABLES (في المكدس). لتعقيم نسخة GLIBC_TUNABLES (والتي يجب أن تكون بالشكل "tunable1=`aaa:tunable2=bbb"`), تزيل parse_tunables() جميع الـ tunables الخطرة (أي tunables SXID_ERASE) من tunestr، مع الإبقاء على tunables SXID_IGNORE وNONE (في الأسطر 221-235):```C
// (GLIBC ld.so sources in ./glibc-2.37/elf/dl-tunables.c)
162 static void
163 parse_tunables (char *tunestr, char *valstring)
164 {
...
168 char *p = tunestr;
169 size_t off = 0;
170
171 while (true)
172 {
173 char *name = p;
174 size_t len = 0;
175
176 /* First, find where the name ends. */
177 while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
178 len++;
179
180 /* If we reach the end of the string before getting a valid name-value
181 pair, bail out. */
182 if (p[len] == '\0')
183 {
184 if (__libc_enable_secure)
185 tunestr[off] = '\0';
186 return;
187 }
188
189 /* We did not find a valid name-value pair before encountering the
190 colon. */
191 if (p[len]== ':')
192 {
193 p += len + 1;
194 continue;
195 }
196
197 p += len + 1;
198
199 /* Take the value from the valstring since we need to NULL terminate it. */
200 char *value = &valstring[p - tunestr];
201 len = 0;
202
203 while (p[len] != ':' && p[len] != '\0')
204 len++;
205
206 /* Add the tunable if it exists. */
207 for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
208 {
209 tunable_t *cur = &tunable_list[i];
210
211 if (tunable_is_name (cur->name, name))
212 {
...
219 if (__libc_enable_secure)
220 {
221 if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
222 {
223 if (off > 0)
224 tunestr[off++] = ':';
225
226 const char *n = cur->name;
227
228 while (*n != '\0')
229 tunestr[off++] = *n++;
230
231 tunestr[off++] = '=';
232
233 for (size_t j = 0; j < len; j++)
234 tunestr[off++] = value[j];
235 }
236
237 if (cur->security_level != TUNABLE_SECLEVEL_NONE)
238 break;
239 }
240
241 value[len] = '\0';
242 tunable_initialize (cur, value);
243 break;
244 }
245 }
246
247 if (p[len] != '\0')
248 p += len + 1;
249 }
250 }
لسوء الحظ، إذا كان متغير البيئة GLIBC_TUNABLES بالشكل "tunable1=tunable2=AAA" (حيث "tunable1" و"tunable2" هما وحدتا ضبط من نوع SXID_IGNORE، على سبيل المثال "glibc.malloc.mxfast")، فإن:
أثناء التكرار الأول لـ "while (true)" في parse_tunables()،
يتم نسخ السلسلة بأكملها "tunable1=tunable2=AAA" في مكانها إلى tunestr
(في الأسطر 221-235)، مما يملأ tunestr؛
في الأسطر 247-248، لا يتم زيادة p (p[len] هو '\0' لأنه لم يتم العثور
على : في الأسطر 203-204) وبالتالي لا تزال p تشير إلى قيمة
"tunable1"، أي "tunable2=AAA"؛