• ptrace


    linux中是不可以依附别的进程的,更不可能创建远程线程,然而一种不太正规的方式却可以做到这一点,这就是ptrace接口,ptrace可以依附任何用户进程,用特殊的参数甚至可以更改任何进程的寄存器和内存映射,这个功力和创建远程线程不相上下,甚至比其更加灵活,如果理解了elf映像在内存的布局便可以通过ptrace修改被调试进程的任意内存。然而有一个限制就是ptrace接口只能调试属于自己用户的进程,也就是说它不能调试别的用户的进程,一种显而易见的限制就是普通用户的ptrace不能调试root进程,但是2.6.29内核有一个明显的漏洞使得普通用户可以提升本地权限。

    简单来讲就是在调用exec的时候需要经历一系列的uid,euid的计算,特别是exec具有suid的程序的时候,这种计算将变得更加繁复,在ptrace的时候需要进行一系列uid,euid的判断,那么如果处理不好这二者之间的同步,必然会导致ptrace的误判,就是说ptrace调试了一个根本不属于自己的进程,甚至可能是root进程,如果ptrace的调用进程更改了被调用的具有suid的进程的内存,使之exec了一个shell,那么该shell将会是属于root的,因为exec的调用进程也就是suid进程的euid是0.

    早期的内核心漏洞更加明显,看看2.4.9的内核中的ptrace_attach

    int ptrace_attach(struct task_struct *task)

    {

    task_lock(task);

    ...

    if(((current->uid != task->euid) ||

    (current->uid != task->suid) ||

    (current->uid != task->uid) ||

    (current->gid != task->egid) ||

    (current->gid != task->sgid) ||

    (!cap_issubset(task->cap_permitted, current->cap_permitted)) ||

    (current->gid != task->gid)) && !capable(CAP_SYS_PTRACE))

    goto bad;

    ...

    task->ptrace |= PT_PTRACED;

    task_unlock(task);

    write_lock_irq(&tasklist_lock);

    if (task->p_pptr != current) {

    REMOVE_LINKS(task);

    task->p_pptr = current;

    SET_LINKS(task);

    }

    write_unlock_irq(&tasklist_lock);

    send_sig(SIGSTOP, task, 1);

    return 0;

    bad:

    task_unlock(task);

    return -EPERM;

    }

    这个早期的内核显得很整洁,因为都到了很少量的锁,锁的粒度自然很粗,不过还好,看看第一行就锁住了这个进程,然后进行一系列的判断,锁住进程的目的就是阻止别的执行绪在判断期间更改进程的属性,这个想法很好,但是再看一个函数:

    void compute_creds(struct linux_binprm *bprm)

    {

    kernel_cap_t new_permitted, working;

    int do_unlock = 0;

    new_permitted = cap_intersect(bprm->cap_permitted, cap_bset);

    working = cap_intersect(bprm->cap_inheritable,

    current->cap_inheritable);

    new_permitted = cap_combine(new_permitted, working);

    if (bprm->e_uid != current->uid || bprm->e_gid != current->gid ||

    !cap_issubset(new_permitted, current->cap_permitted)) {

    current->mm->dumpable = 0;

    lock_kernel(); //仅仅锁住了kernel而没有锁住task,只要没有执行绪和该执行绪竞争kernel锁,那么谁也不会等待

    if (must_not_trace_exec(current) //注意这个函数,见下面。该位置设为A

    || atomic_read(¤t->fs->count) > 1

    || atomic_read(¤t->files->count) > 1

    || atomic_read(¤t->sig->count) > 1) {

    if(!capable(CAP_SETUID)) { //千万别通过这个if语句,否则弊大于利,攻击成功的可能性就小了

    bprm->e_uid = current->uid;

    bprm->e_gid = current->gid;

    }

    ...

    }

    do_unlock = 1;

    }

    ...//注意,以下就应该设置新进程的各种ID了。下面的位置设置为B。

    current->suid = current->euid = current->fsuid = bprm->e_uid;

    current->sgid = current->egid = current->fsgid = bprm->e_gid;

    if(do_unlock)

    unlock_kernel();

    current->keep_capabilities = 0;

    }

    static inline int must_not_trace_exec(struct task_struct * p)

    {

    return (p->ptrace & PT_PTRACED) && !cap_raised(p->p_pptr->cap_effective, CAP_SYS_PTRACE);

    }

    注意,在A和B之间并没有进行task本身的保护,最起码没有和ptrace互斥,ptrace为了安全起见被设计为不能跟踪自己用户id之外的进程,比如一个普通用户进程不能跟踪suid进程,但是看看2.4.9的代码,如果在A和B之间,ptrace闯了进来要跟踪正在exec的进程,此时被跟踪的正在 exec的进程尚未设置好新的euid和egid之类的id,那么ptrace进程很容易就得逞了,通过了ptrace_attach中的那么长的判断,从而成功实现一个普通用户进程进程跟踪一个suid进程,以后可以通过ptrace接口修改被跟踪的suid进程的内存实现注入,如果注入一个root的 shell,那么一切就成功了!在后来的内核中,在compute_creds也用了task_lock(task)将进程锁住,那么exec和 ptrace就不能同时进入到竞争态了,如果exec先进去,那么ptrace_attach的一系列判断会将有企图的进程赶出去,如果ptrace先进去,那么must_not_trace_exec会将exec新进程的euid设置成current的uid从而降低了它的权限,仔细看 must_not_trace_exec发现它也有漏洞,如果suid程序本身有漏洞,那么!cap_raised(p->p_pptr->cap_effective, CAP_SYS_PTRACE)这个条件就有可能被利用,从而绕过bprm->e_uid = current->uid,因此后来的版本将该函数的后一个条件去掉了,只要一个进程被跟踪了,那么就尽可能不让它获得特权,除非它的调用者本身就是特权程序,皇帝自杀谁也挡不住啊。

    虽然后来的版本在两个地方加上了锁,但是一下子就锁一个进程未免粒度过大,于是2.6的后期内核将锁的粒度减小,引入了ptrace和exec进行互斥的专用锁,这样内核的效率会更高,和这二者无关的操作不会因为锁而被阻拦。

  • 相关阅读:
    #20175120彭宇辰 java第五周学习总结
    结对学习创意照
    #20175120彭宇辰 java第四周总结
    《Java程序设计》第3周学习总结
    # 20175120 2018.3.10 《Java程序设计》第2周学习总结
    20175303柴轩达 信息安全系统设计基础第四周学习总结
    信息安全系统设计基础第三周学习总结
    20175303 《信息安全系统设计基础》第一周学习总结
    20175303柴轩达答辩项目代码链接整合
    团队作业(五):冲刺总结
  • 原文地址:https://www.cnblogs.com/bittorrent/p/3264596.html
Copyright © 2020-2023  润新知