システムコールの制限とファイル権限
操作が拒否される箇所を、フィルターとアクセス権の両方から調べます。
説明
seccompフィルターはシステムコールの種類や数値引数に応じて、実行の許可・拒否等を決めます。通過した呼出しでも、ファイル権限など別の条件を満たさなければ操作は成功しません。
Linux x86-64の固定した呼出し列を使い、番号と引数からフィルターを判定します。その後、readが拒否される条件とopenatを通してもファイルを開けない条件を比べ、フィルターで直接扱えないパス文字列も確認します。
1呼出し番号と引数の記録
x86-64のネイティブABIに限定し、read=0、write=1、openat=257を使います。ABIが違えば番号も変わります。
calls = [(0, (3, 0x4000, 8)), (1, (1, 0x4000, 8)), (257, (-100, 0x5000, 0, 0))]固定したシステムコール列; names = {0: "read", 1: "write", 257: "openat"}このABIの番号表; print([(nr, names[nr], args) for nr,args in calls])名前と引数を表示readはfd3から8バイトを読み、writeはfd1へ8バイトを書き、openatはパスを指定して開きます。0x4000や0x5000はプロセスのアドレスであり、フィルターがその先の文字列を直接読めるという意味ではありません。
2呼出しの種類によるフィルター
readとwriteを通し、それ以外をEPERMで拒否する規則を計算します。これは注目する呼出しだけを抜き出した規則モデルです。
allowed = {0, 1}許可する番号; decisions = [(names[nr], "ALLOW" if nr in allowed else "ERRNO:EPERM") for nr,args in calls]番号で判定; print(decisions)呼出しごとの扱いopenatは実行されずEPERM相当の結果になります。これはフィルター自身が選んだエラーで、ファイルのアクセス権まで検査して拒否された結果ではありません。実プログラム用には終了、メモリ管理等の必要な呼出しも含めて設計します。
3引数も条件に含める
readはfd3、writeはfd1と2だけを通す規則へ絞ります。関数は数値引数を検査する部分を表します。
def filter_call(nr, args):番号と引数を受け取る
if nr == 0 and args[0] == 3:fd3のread
return "ALLOW"通過
if nr == 1 and args[0] in (1, 2):fd1・2のwrite
return "ALLOW"通過
return "ERRNO:EPERM"それ以外は拒否同じreadでもfdが違えば判定が変わります。実際のフィルターではアーキテクチャ識別子やABIを先に確認し、番号の解釈を取り違えないようにします。
4同じバッファでもfdが違えば拒否
読取り先バッファと長さを固定し、fd3とfd4を比較します。
print(filter_call(0, (3, 0x4000, 8)), filter_call(0, (4, 0x4000, 8)))fdだけ変えた読取り; print(filter_call(1, (2, 0x4000, 8)))標準エラーへの書込みfd3のreadは通過、fd4は拒否、fd2へのwriteは通過します。fd番号は開いたファイルの参照表の位置です。fdを閉じて別の資源に再利用する経路を許すと、同じ番号でも対象が変わるため、番号だけで対象ファイルを固定したつもりにならないようにします。
5フィルター通過後のファイル権限
openatも通す規則にした場合を考えます。現在のUID1000が、所有者2000・mode0600のファイルを読めるか、通常権限だけで計算します。
seccomp_allows = Trueフィルター通過; uid, owner, mode = 1000, 2000, 0o600呼出し元とファイル; permission = (mode >> 6) & 7 if uid == owner else mode & 7この条件で適用する通常権限; dac_allows = bool(permission & 4)読取りの許可; print(seccomp_allows, dac_allows, seccomp_allows and dac_allows)両条件を確認フィルターはTrueでも、通常権限でFalseなので読めません。この例はグループ権限0で、ACLや権限を迂回するcapabilityはなく、経路探索は可能な条件です。seccompのALLOWは、システムコールを無条件で成功させる意味ではありません。
6所有者に一致してもフィルター拒否は残る
呼出し元を所有者に一致させ、通常権限で読める条件にします。一方、openatを拒否するフィルターへ戻します。
uid = owner; dac_allows = bool(((mode >> 6) & 7) & 4)所有者の読取り権限; seccomp_allows = False呼出し自体を拒否; print(dac_allows, seccomp_allows, dac_allows and seccomp_allows)権限だけでは通らない例通常権限はTrueでもフィルターがFalseなので実行できません。同じ操作が失敗しても、ファイル権限だけを変更して解決できるとは限りません。拒否した層とエラーを対応させて確認します。
7ポインター値とパスの内容
同じアドレス0x5000に、時点によって別のパスが入った状態を作ります。
memory = {0x5000: b"/data/public.txt\0"}最初のパス; pointer = calls[2][1][1]; print(hex(pointer), memory[pointer])引数の値と参照先; memory[0x5000] = b"/data/private.txt\0"同じ位置の内容を変更; print(hex(pointer), memory[pointer])数値だけでは見分けられない例数値引数は同じですが、パスの内容は変わっています。通常のseccomp BPFはポインターの参照先を読み取れないため、この値だけでpublic.txtへのアクセスと判断できません。パス単位のアクセス制御は適切な別の仕組みと組み合わせます。
8規則を重ねても拒否を緩められない
二つのALLOW/ERRNO規則を重ね、一方が拒否する呼出しを確認します。ここではより強い拒否側を採用する関係を計算します。
layer_a = {0, 1, 257}; layer_b = {0, 1}二つの許可集合; combined = layer_a & layer_b両方で許可される呼出し; print(sorted(combined), 257 in combined)openatが残るか確認openatは後の規則が拒否するので通りません。実際のseccompではアクションの優先順位が決められており、この例はALLOWとERRNOの二種類に限定しています。既存フィルターを新たなALLOWで解除することはできません。
まとめ
seccompは呼出しの実行を制限し、ALLOW後も通常のアクセス制御が働きます。
数値引数の検査と、ポインターの参照先やファイルパスの検査は別です。
ABI・必要な呼出し・fdの再利用・重ねた規則も含めて境界を確認します。
操作履歴と解説を振り返れます。
Python 3
calls = [(0, (3, 0x4000, 8)), (1, (1, 0x4000, 8)), (257, (-100, 0x5000, 0, 0))]; names = {0: "read", 1: "write", 257: "openat"}; print([(nr, names[nr], args) for nr,args in calls])[(0, 'read', (3, 16384, 8)), (1, 'write', (1, 16384, 8)), (257, 'openat', (-100, 20480, 0, 0))]
allowed = {0, 1}; decisions = [(names[nr], "ALLOW" if nr in allowed else "ERRNO:EPERM") for nr,args in calls]; print(decisions)[('read', 'ALLOW'), ('write', 'ALLOW'), ('openat', 'ERRNO:EPERM')]def filter_call(nr, args):
if nr == 0 and args[0] == 3:
return "ALLOW"
if nr == 1 and args[0] in (1, 2):
return "ALLOW"
return "ERRNO:EPERM"print(filter_call(0, (3, 0x4000, 8)), filter_call(0, (4, 0x4000, 8))); print(filter_call(1, (2, 0x4000, 8)))ALLOW ERRNO:EPERM ALLOW
seccomp_allows = True; uid, owner, mode = 1000, 2000, 0o600; permission = (mode >> 6) & 7 if uid == owner else mode & 7; dac_allows = bool(permission & 4); print(seccomp_allows, dac_allows, seccomp_allows and dac_allows)True False False
uid = owner; dac_allows = bool(((mode >> 6) & 7) & 4); seccomp_allows = False; print(dac_allows, seccomp_allows, dac_allows and seccomp_allows)True False False
memory = {0x5000: b"/data/public.txt\0"}; pointer = calls[2][1][1]; print(hex(pointer), memory[pointer]); memory[0x5000] = b"/data/private.txt\0"; print(hex(pointer), memory[pointer])0x5000 b'/data/public.txt\x00' 0x5000 b'/data/private.txt\x00'
layer_a = {0, 1, 257}; layer_b = {0, 1}; combined = layer_a & layer_b; print(sorted(combined), 257 in combined)[0, 1] False
演習完了