เวิร์กช็อปการยกระดับสิทธิ์ท้องถิ่นของ 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)
ดังที่เราเห็น มันพบไลบรารี crypto ที่จำเป็นที่ /lib/x86_64-linux-gnu/libcrypto.so.3 ในระหว่างการเริ่มต้นโปรแกรม มันจะใส่โค้ดของไลบรารีนี้ลงใน RAM กระบวนการ และเชื่อมโยงการอ้างอิงทั้งหมดไปยังไลบรารีนี้
เมื่อโปรแกรมเริ่มทำงาน ตัวโหลดนี้จะตรวจสอบโปรแกรมก่อนเพื่อกำหนดไลบรารีแบบแบ่งใช้ที่ต้องการ จากนั้นจะค้นหาไลบรารีเหล่านี้ โหลดลงในหน่วยความจำ และเชื่อมโยงกับไฟล์ปฏิบัติการในขณะรันไทม์ ในกระบวนการนี้ ตัวโหลดแบบไดนามิกจะแก้ไขการอ้างอิงสัญลักษณ์ เช่น ฟังก์ชันและการอ้างอิงตัวแปร เพื่อให้มั่นใจว่าทุกอย่างได้รับการตั้งค่าสำหรับการทำงานของโปรแกรม เมื่อพิจารณาถึงบทบาทของตัวโหลดแบบไดนามิกแล้ว จะต้องคำนึงถึงความปลอดภัยเป็นอย่างสูง เนื่องจากโค้ดของตัวโหลดจะทำงานด้วยสิทธิ์ระดับสูงเมื่อผู้ใช้เฉพาะที่เรียกใช้โปรแกรม set-user-ID หรือ set-group-ID
การปรับแต่งเป็นคุณสมบัติในไลบรารี GNU C ที่ช่วยให้ผู้เขียนแอปพลิเคชันและผู้ดูแลการแจกจ่ายปรับเปลี่ยนพฤติกรรมไลบรารีรันไทม์เพื่อให้ตรงกับปริมาณงานของพวกเขา สิ่งเหล่านี้ถูกนำมาใช้เป็นชุดสวิตช์ที่อาจแก้ไขได้หลายวิธี วิธีการเริ่มต้นปัจจุบันในการดำเนินการนี้คือผ่านตัวแปรสภาพแวดล้อม GLIBC_TUNABLES โดยการตั้งค่าเป็นสตริงของคู่ชื่อ=ค่าที่คั่นด้วยโคลอน ตัวอย่างเช่น ตัวอย่างต่อไปนี้เปิดใช้งานการตรวจสอบ 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 ):
// (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() จะลบค่าปรับที่เป็นอันตรายทั้งหมด (ค่าปรับ SXID_ERASE) ออกจาก tunestr แต่เก็บค่าปรับ SXID_IGNORE และ NONE ไว้ (ที่บรรทัด 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) ดังนั้นการเติม tunestr;
ที่บรรทัด 247-248 p จะไม่เพิ่มขึ้น (p[len] คือ '