
PTRACE_TRACEME は、2019年7月にJann Hornによって発見されたLinuxカーネルの特権昇格の脆弱性です。
Ptrace はシステムコールであり、プロセス(tracer)が別のプロセス(tracee)の実行を監視・制御し、そのコアイメージやレジスタを検査・変更する方法を提供します。主にデバッグにおけるブレークポイントの設定やシステムコール呼び出しの追跡に使用されます。``` c 1 396 kernel/ptrace.c <<ptrace_attach>> ptrace_link(task, current); 2 469 kernel/ptrace.c <<ptrace_traceme>> ptrace_link(current, current->real_parent);
トレース関係を確立する方法は2つある:
- プロセスが `fork` 関数を呼び出し、その子プロセスが `PTRACE_TRACEME`(カーネル内の `ptrace_traceme` 関数に相当)を呼び出してトレースを初期化する。
- プロセスが `PTRACE_ATTACH` または `PTRACE_SEIZE`(カーネル内の `ptrace_attach` 関数に相当)を呼び出して、別のプロセスをトレースするためのトレーサを初期化する。
どちらの方法を使っても、最終的には `ptrace_link` 関数が呼び出され、トレーサとトレース間のトレース関係が確立される。
- `ptrace_attach` の場合、`ptrace_link` に渡される2つのパラメータは 'task'(トレース)と 'current'(トレーサ)である。
- `ptrace_traceme` の場合、`ptrace_link` に渡される2つのパラメータは 'current'(トレース)と 'current->real_parent'(トレーサ)である。
ここで注意すべき点は、上記の2つの方法で `ptrace_link` 関数を呼び出す際のトレーサとトレースのパラメータが何であるかである。なぜなら、脆弱性は `ptrace_link` 関数に存在するからである。``` c
static void ptrace_link(struct task_struct *child, struct task_struct *new_parent)
{
rcu_read_lock();
__ptrace_link(child, new_parent, __task_cred(new_parent));
rcu_read_unlock();
}
void __ptrace_link(struct task_struct *child, struct task_struct *new_parent,
const struct cred *ptracer_cred)
{
BUG_ON(!list_empty(&child->ptrace_entry));
list_add(&child->ptrace_entry, &new_parent->ptraced); // 1. thêm chính nó vào hàng đợi
// ptraced của process cha
child->parent = new_parent; // 2. Lưu địa chỉ của process cha trong con trỏ parent
child->ptracer_cred = get_cred(ptracer_cred); // 3. Lưu ptracer_cred lại, ta cần tập trung
// vào biến này vì lỗi nằm ở đây
}
トレース関係を設定する鍵は、tracee が tracer の cred を記録し、それを tracee の 'ptracer_cred' 変数に保存することです。
'ptracer_cred' の概念は、2016 年のパッチ ptrace: Capture the ptracer's creds not PT_PTRACE_CAP によって導入されました。 'ptracer_cred' を導入した目的は、tracee が exec を実行して setuid executable をロードする際にセキュリティチェックを行うことです。
なぜこの安全性チェックが必要なのでしょうか?
exec ファミリーはプロセスのイメージを更新できます。 setuid bit が設定された実行ファイルの場合、そのファイルが実行されると、プロセスの euid が実行ファイルの所有者の uid に変更されます。 プロセスの権限は exec を呼び出したユーザーの権限よりも高くなり、このような setuid executable を実行すると、権限昇格 (escalation) が発生します。
想像してみてください。exec を実行するプロセス自体が tracee であり、setuid executable を実行して特権を昇格した後、その tracer はいつでも tracee のレジスタやメモリを変更できます。もし tracer が低特権で、高特権の tracee を制御できる場合、tracer は tracee を通じて不正な操作を行う可能性があります。
しかし、カーネルはそのような権限を超えた動作を許可していないようです。そのため、トレース関係を設定する際、tracee は tracer の cred (すなわち ptracer_cred) を保存する必要があります。tracee が exec プロセスを実行する場合、実行されるファイルの setuid ビットが設定されているかどうかをチェックし、設定されていれば 'ptracer_cred' の権限を確認します。権限が満たされない場合、setuid ビットの実行権限 (ファイル所有者の特権) は exec の実行に使用されず、代わりに元のユーザーの権限で実行されます。
このプロセスのコード分析は以下の通りです (この記事のコード分析は v4.19-rc8 に基づいています)。``` python do_execve -> __do_execve_file -> prepare_binprm -> bprm_fill_uid -> security_bprm_set_creds ->cap_bprm_set_creds -> ptracer_capable ->selinux_bprm_set_creds ->(apparmor_bprm_set_creds) ->(smack_bprm_set_creds) ->(tomoyo_bprm_set_creds)
実行権限に関する操作は主に関数 `prepare_binprm`にあります``` c
1567 int prepare_binprm(struct linux_binprm *bprm)
1568 {
1569 int retval;
1570 loff_t pos = 0;
1571
1572 bprm_fill_uid(bprm); // <-- fill cred của new process (xem hàm bprm_fill_uid bên dưới sẽ rõ hơn)
1573
1574 /* fill in binprm security blob */
1575 retval = security_bprm_set_creds(bprm); // <-- kiểm tra bảo mật, để xem xét sửa đổi cred của new process
1576 if (retval)
1577 return retval;
1578 bprm->called_set_creds = 1;
1579
1580 memset(bprm->buf, 0, BINPRM_BUF_SIZE);
1581 return kernel_read(bprm->file, bprm->buf, BINPRM_BUF_SIZE, &pos);
1582 }
上記のように、まず bprm_fill_uid を呼び出して新しいプロセスの cred を埋め、次に security_bprm_set_creds を呼び出してセキュリティをチェックし、必要に応じて新しい cred を修正します。``` c
1509 static void bprm_fill_uid(struct linux_binprm *bprm)
1510 {
1511 struct inode inode;
1512 unsigned int mode;
1513 kuid_t uid;
1514 kgid_t gid;
1515
1516 /
1517 * Since this can be called multiple times (via prepare_binprm),
1518 * we must clear any previous work done when setting set[ug]id
1519 * bits from any earlier bprm->file uses (for example when run
1520 * first for a setuid script then again for its interpreter).
1521 /
1522 bprm->cred->euid = current_euid(); // <--- trước tiên sẽ sử dụng euid của process hiện tại
1523 bprm->cred->egid = current_egid();
1524
1525 if (!mnt_may_suid(bprm->file->f_path.mnt))
1526 return;
1527
1528 if (task_no_new_privs(current))
1529 return;
1530
1531 inode = bprm->file->f_path.dentry->d_inode;
1532 mode = READ_ONCE(inode->i_mode);
1533 if (!(mode & (S_ISUID|S_ISGID))) // <---------- nếu bit setuid/setgid của file thực thi không được set
1534 return; // , hàm sẽ return tại đây.
1535
1536 / Be careful if suid/sgid is set /
1537 inode_lock(inode);
1538
1539 / reload atomically mode/uid/gid now that lock held /
1540 mode = inode->i_mode;
1541 uid = inode->i_uid; // <---- nếu S_ISUID được set,sử dụng i_uid của file
1542 gid = inode->i_gid;
1543 inode_unlock(inode);
1544
1545 / We ignore suid/sgid if there are no mappings for them in the ns */
1546 if (!kuid_has_mapping(bprm->cred->user_ns, uid) ||
1547 !kgid_has_mapping(bprm->cred->user_ns, gid))
1548 return;
1549
1550 if (mode & S_ISUID) {
1551 bprm->per_clear |= PER_CLEAR_ON_SETID;
1552 bprm->cred->euid = uid; // <------ sử dụng uid của file như là euid của new process
1553 }
1554
1555 if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
1556 bprm->per_clear |= PER_CLEAR_ON_SETID;
1557 bprm->cred->egid = gid;
1558 }
1559 }
Nhìn vào 2 dòng code sau đối với đoạn code ở trên:
- Dòng 1522, gán euid hiện tại của process vào new euid, vì vậy hầu hết các process thực thi đều thực thi dưới quyền ban đầu.
- Dòng 1552, nếu bit suid được set, gán uid của chủ sở hữu file thực thi vào new uid. Có thể hiểu nó giống như việc setuid. New euid trở thành uid của chủ sở hữu file thực thi, nếu chủ sở hữu là một user đặc quyền, leo thang đặc quyền sẽ diễn ra ở đây.
Tuy nhiên, euid ở đây vẫn chưa phải là kết quả cuối cùng, ta cần xem xẻt hàm `security_bprm_set_creds` để biết thêm về việc kiểm tra bảo mật.
Hàm `security_bprm_set_creds` gọi [LSM](https://en.wikipedia.org/wiki/Linux_Security_Modules) framework
Trên phiên bản kernel mà tôi phân tích, có đến 5 điểm hook lsm frameworks thực hiện kiểm tra bảo mật của 'bprm_set_creds'. Các hàm kiểm tra như sau:``` python
cap_bprm_set_creds
selinux_bprm_set_creds
apparmor_bprm_set_creds
smack_bprm_set_creds
tomoyo_bprm_set_creds
ここで実行されるフック関数は、特定のカーネル構成に依存します。理論的には、すべてのLSMフレームワークが有効になっている場合、上記のすべてのフック関数が実装され、'bprm_set_creds' をチェックします。
私の分析環境では、cap_bprm_set_creds と selinux_bprm_set_creds のフック関数のみが実行されました。