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中找到了必要的加密庫。所有引用連結到該庫。
當程式啟動時,該載入程式首先檢查該程式以確定其所需的共用庫。然後它會搜尋這些庫,將它們加載到記憶體中,並在運行時將它們與可執行檔連結。在此過程中,動態載入器解析符號引用,例如函數和變數引用,確保為程式的執行做好一切準備。鑑於其作用,動態載入程式對安全性高度敏感,因為當本機使用者啟動 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() 會從unestr 中刪除所有危險的可調參數(SXID_ERASE 可調參數),但保留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」),則:
在 parse_tunables() 中的「while (true)」的第一次迭代期間,整個「tunable1=tunable2=AAA」就地複製到tunestr(第221-235行),從而填入tunestr;
在第247-248行,p沒有遞增(p[len]是'