ورشة عمل Looney Tunables لتصعيد الامتيازات المحلية (CVE-2023-4911) (للأغراض التعليمية فقط)
في الحوسبة، الرابط الديناميكي هو جزء من نظام التشغيل الذي يقوم بتحميل وربط المكتبات المشتركة التي يحتاجها الملف القابل للتنفيذ عند تنفيذه، عن طريق نسخ محتوى المكتبات من التخزين المستمر إلى ذاكرة الوصول العشوائي (RAM)، وملء جداول الانتقال، ونقل المؤشرات.
على سبيل المثال، لدينا برنامج يستخدم مكتبة opensl لحساب تجزئة 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 أثناء بدء تشغيل البرنامج، فإنه يضع كود هذه المكتبة في ذاكرة الوصول العشوائي للعملية ويربط جميع المراجع بهذه المكتبة.
عند بدء تشغيل برنامج ما، يقوم هذا المُحمل أولاً بفحص البرنامج لتحديد المكتبات المشتركة التي يحتاجها. ثم يقوم بالبحث عن هذه المكتبات، وتحميلها في الذاكرة، وربطها بالملف القابل للتنفيذ في وقت التشغيل. في هذه العملية، يقوم المُحمل الديناميكي بتحليل مراجع الرموز، مثل مراجع الوظائف والمتغيرات، مما يضمن إعداد كل شيء لتنفيذ البرنامج. نظرًا لدوره، فإن المُحمل الديناميكي حساس للغاية للأمان، حيث يعمل الكود الخاص به بامتيازات مرتفعة عندما يقوم مستخدم محلي بتشغيل برنامج set-user-ID أو set-group-ID.
تعد Tunables ميزة في مكتبة GNU C التي تسمح لمؤلفي التطبيقات ومشرفي التوزيع بتغيير سلوك مكتبة وقت التشغيل لتتناسب مع عبء العمل الخاص بهم. يتم تنفيذها كمجموعة من المفاتيح التي يمكن تعديلها بطرق مختلفة. الطريقة الافتراضية الحالية للقيام بذلك هي عبر متغير البيئة GLIBC_TUNABLES عن طريق تعيينه إلى سلسلة من الأسماء المفصولة بنقطتين = أزواج القيمة. على سبيل المثال، المثال التالي يمكّن التحقق من malloc ويعين حد قطع malloc إلى 128 بايت:
GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3
export GLIBC_TUNABLES
تمرير --list-tunables إلى المُحمل الديناميكي لطباعة جميع 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). ):
// (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) من tunstr، ولكنها تحافظ على SXID_IGNORE وNONE tunables (في الأسطر 221- 235):
// (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 ] != ' ' )
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 ] == ' ' )
183 {
184 if ( __libc_enable_secure )
185 tunestr [ off ] = ' ' ;
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 ] != ' ' )
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 != ' ' )
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 ] = ' ' ;
242 tunable_initialize ( cur , value );
243 break ;
244 }
245 }
246
247 if ( p [ len ] != ' ' )
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)، وبالتالي ملء tunstr؛
في الأسطر 247-248، لا تتم زيادة p (p[len] هو '