[comp.sources.misc] v05i085: mkdir

doug@letni.UUCP (Doug Davis) (12/19/88)

Posting-number: Volume 5, Issue 85
Submitted-by: "Doug Davis" <doug@letni.UUCP>
Archive-name: safe-mkdir

[I looked it over, looks OK from a quick scan -- but is it really secure?
I *think* so, but....  ++bsa]

Attached is a version of /bin/mkdir that should eliminate the 
problem with the race condition that someone can take advantage
of to cause a major security hole.  Machines that have the 
mkdir() function call, most anything based on 4.2 BSD or later,
will not need this program. Everyone else that I know of
should want this.

For security reasons I have elected not to describe the problem with /bin/mkdir
fully, just suffice it to say that I have tested it on 11 differen't
architectures and the "bug" existed on all of them.  If your /bin/mkdir
program is setuid root, you too probably have this bug as well.

This mkdir first makes a directory to play in, which is owned by root
and is mode 000.  It is made in the same directory in which the
user is requesting his directory.  In this "secure" directory, to
which the user allegedly has no access, the mknod(), chown(), and
links for `.' and `..' are performed.  The new directory is then linked
into place.  Finally, the "secure" directory is removed. Yes, there
is a bit more overhead, but a much more secure program is worth it.

As usual, I will accept mail, suggestions, comments, etc will
be appreciated. Flames will be ignored. If anyone can poke security holes
in this code I would really like to hear about it.

BTW: I know the calls to rand() are not really needed. They just make
     it more fun for someone trying to defeat the code.

Doug Davis
--
Lawnet
1030 Pleasent Valley Lane.
Arlington Texas 76015
817-467-3740
{ sys1.tandy.com, motown!sys1, uiucuxc!sys1, killer!texbell } letni!doug

  "Talk about holes in UNIX, geeze thats nothing compaired with the security
      problems in the ship control programs of StarFleet."

#! /bin/sh
# This is a shell archive.  Remove anything before this line, then unpack
# it by saving it into a file and typing "sh file".  To overwrite existing
# files, type "sh file -c".  You can also feed this as standard input via
# unshar, or by typing "sh <file", e.g..  If this archive is complete, you
# will see the following message at the end:
#		"End of shell archive."
# Contents:  Makefile mkdir.c
# Wrapped by doug@letni on Thu Dec 15 01:28:50 1988
# { sys1.tandy.com, motown!sys1, uiucuxc!sys1, killer!texbell } letni!doug
PATH=/bin:/usr/bin:/usr/ucb ; export PATH
if test -f 'Makefile' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'Makefile'\"
else
echo shar: Extracting \"'Makefile'\" \(510 characters\)
sed "s/^X//" >'Makefile' <<'END_OF_FILE'
X# What kind of strrchr do we have?
X# 'strrchr' for most newer unix's,  'rindex' for earlier editions
X# STTRCHR	= -DSTRRCHR=rindex
STRRCHR	= -DSTRRCHR=strrchr
X# do you have a rand() function call? 
X# If you don't have one, comment out the line below
RAND 	= -DRAND
X
X# for debugging, 
X# -DDEBUG
X
DEFINES	= $(STRRCHR) $(DEBUG) $(RAND)
SHELL	=	/bin/sh
CC		=	/bin/cc
CFLAGS	=	-O $(DEFINES)
LDFLAGS	=	-n -s
X
all: mkdir
X
mkdir.o: mkdir.c
X	$(CC) $(CFLAGS) mkdir.c -c
X
mkdir: mkdir.o
X	$(CC) $(LDFLAGS) mkdir.o -o mkdir
END_OF_FILE
if test 510 -ne `wc -c <'Makefile'`; then
    echo shar: \"'Makefile'\" unpacked with wrong size!
fi
# end of 'Makefile'
fi
if test -f 'mkdir.c' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'mkdir.c'\"
else
echo shar: Extracting \"'mkdir.c'\" \(7645 characters\)
sed "s/^X//" >'mkdir.c' <<'END_OF_FILE'
X/*
X * Secure mkdir program, solves that nasty race problem...
X *
X *	13 December 1988	Doug Davis         doug@lenti.lawnet.com
X *                  and John Elliot IV     iv@trsvax.tandy.com
X *     
X *
X *	Theory of operation:
X *		This mkdir first makes a directory to play in, which is
X * 		owned by root and is mode 000.  It is made in the same
X *		directory in which the user is requesting his directory.
X *		In this "secure" directory, to which the user allegedly
X *		has no access, the mknod(), chown(), and links for `.'
X *		and `..' are performed.  The new directory is then linked
X *		into place.  Finally, the "secure" directory is removed.
X *
X * This code copyright 1988 by Doug Davis (doug@letni.lawnet.com) 
X *      You are free to modify, hack, fold, spindle, duplicate, pass-along
X *      give-away, publish, transmit, or mutlate this code in any maner,
X *      provided that you give credit where credit is due and don't pretend
X *      that you wrote it.
X * 
X *  If you do my lawyers (and I have a lot of lawyers) will teach you a lesson
X *  or two in copyright law that you will never ever forget.
X */
X
X#define MAXPATHLEN	128		/* maximum reasonanble path length */
X
X#include <sys/types.h>
X#include <signal.h>
X#include <sys/stat.h>
X#ifdef DEBUG
X#  include <stdio.h>
X#else /*DEBUG*/
X#  define NULL ((char *) 0)
X#endif /*DEBUG*/
X
X#define MKNODE	1
X#define LINK	2
X
char *Malloc_Failed	= "malloc() failed.";
char *Doesnt_Exist	= " does not exist.";
char *Cannot_Access	= "cannot access ";
char *Already_Exist	= " already exists.";
char *Secure_Failed	= "makedir secure parent failed ";
char *Couldnt_Link	= "Couldn't link to ";
char *Mkdir_Failed	= "makedir failed ";
char *Chown_Failed	= "chown() failed ";
X
extern char *STRRCHR();
extern char *malloc();
X
extern int errno;
extern int getpid();
X
extern unsigned short getgid();
extern unsigned short getuid();
X
X#ifdef RAND
extern int rand();
X#else /*RAND*/
extern int getppid();
X#endif /*RAND*/
X
extern long time();
X
char *Progname;
X
main(argc, argv)
int argc;
char *argv[];
X{
X	Progname = argv[0];
X	errno = 0;
X
X	if (argc < 2) {
X		print("Usage:  ");
X		print(Progname);
X		print(" directory_name [ ... directory_name ]\n");
X		exit(0);
X	}
X
X	/* Catch those nasty signals that could cause us
X	 * to mess up the filesystem */
X	swat_sigs();
X
X	while (--argc)
X		md(*++argv); /* make each directory */
X
X	exit(errno);
X}
X
X
md(s)
char *s;
X{
X	char	*basename, *parent, *fullname;
X	char	securename[MAXPATHLEN], securedir[MAXPATHLEN];
X	long	snum;
X	unsigned short myuserid, mygroupid;
X	struct	stat sanity;
X
X	/* find out who I really am */
X	myuserid = getuid();
X	mygroupid = getgid();
X
X	/* set up the pseudo-RANDom number generation system */
X#ifndef RAND
X	srand(getpid());
X#endif /*RAND*/
X
X	/* see if we are explicit or indirect */
X	basename = STRRCHR(s, '/');
X	if (basename == (char *) NULL) {
X		fullname = malloc(strlen(s)+1);
X		if (fullname == (char *) NULL) 
X			error(Malloc_Failed, NULL, errno);
X		parent = malloc(2);
X		if (parent == (char *) NULL)
X			error(Malloc_Failed, NULL, errno);
X		parent[0] = '.';
X		parent[1] = '\0';
X		strcpy(fullname, s);
X		basename = s;
X	} else {
X		fullname = malloc(strlen(s)+1);
X		if (fullname == (char *) NULL) 
X			error(Malloc_Failed, NULL, errno);
X		strcpy(fullname, s);
X		*basename = '\0';
X		basename++;
X		parent = malloc(strlen(s) + 3);
X		if (parent == (char *) NULL)
X			error(Malloc_Failed, NULL, errno);
X		strcpy(parent, s);
X		strcat(parent, "/.");
X	}
X
X	/* Generate the secure names ... */
X	do {
X		/* round and round we go where we stop depends on
X		 * the non-existance of securedir */
X		snum = time((int *) 0);	
X#ifdef RAND
X		sprintf(securedir, "%s/%ld", parent, snum - (long)rand());
X		sprintf(securename, "%s/%ld", securedir, snum + (long)rand());
X#else /*RAND*/
X		sprintf(securedir, "%s/%ld", parent, snum - (long)getppid());
X		sprintf(securename, "%s/%ld", securedir, snum + (long)getppid());
X		snum += (long)getpid();
X#endif /*RAND*/
X	} while (stat(securedir, &sanity) == 0);
X
X#ifdef DEBUG
X	/* spill the beans .. */
X	printf("parent     == %s\n", parent);
X	printf("basename   == %s\n", basename);
X	printf("fullname   == %s\n", fullname);
X	printf("securedir  == %s\n", securedir);
X	printf("securename == %s\n", securename);
X	fflush(stdout);
X#endif /*DEBUG*/
X
X	/* lets see if our parent directory is around... */
X	if ((stat(parent, &sanity)) != 0)
X		error(parent, Doesnt_Exist, 0);
X
X	/* find out if we can write here */
X	if (canIwrite(&sanity, myuserid, mygroupid) != 0) 
X		error(Cannot_Access, parent, 0);
X
X	/* find out if we are going to stomp on something.. */
X	if ((stat(fullname, &sanity)) == 0) 
X		error(fullname, Already_Exist, 0);
X
X	/* make secure parent directory (note the mode of 0) */
X	if (makedir(parent, securedir, 0) > 0) 
X		error(Secure_Failed, securedir, errno);
X	
X	/* now make our directory underneath it */
X	if (makedir(parent, securename, 0777) > 0) 
X		error(Mkdir_Failed, securedir, errno);
X
X	/* do that eerie little chown() thats the "root" of all our problems */
X	if (chown(securename, myuserid, mygroupid) != 0) 
X		error(Chown_Failed, securename, errno);
X	
X	/* do a quick sanity check, just to annoy someone trying, unsccessfully
X	 * I might add, to trick mkdir into chowning something it shouldn't.. */
X	if ((stat(fullname, &sanity)) == 0) {
X		/* what happend? this wasn't here a couple of functions ago.. */
X		unlink(securename);
X		rmdir(securedir);
X		error(fullname, Already_Exist, 0);
X	}
X		
X	/* okay, put it where it belongs */
X	if ((link(securename, fullname)) < 0) 
X		error(Couldnt_Link, fullname, errno);
X	
X	/* remove all our rubbish, and tidy everything up.. */
X	unlink(securename);
X	rmdir(securedir);
X	if (parent != (char *) NULL) 
X		free(parent);
X	if (fullname != (char *) NULL)
X		free(fullname);
X	return(0);
X}
X
makedir(parent, dir, mode)
char *parent, *dir;
int mode;
X{
X	char dotdot[MAXPATHLEN];
X
X#ifdef DEBUG
X	printf("mkdir(%s, %s)\n", parent, dir);
X	fflush(stdout);
X#endif /*DEBUG*/
X
X	/* put the node together */
X	if ((mknod(dir, S_IFDIR | mode, 0)) < 0) 
X		return (MKNODE);
X
X	/* make dot */
X	strcpy(dotdot, dir);
X	strcat(dotdot, "/.");
X	if ((link(dir, dotdot)) < 0) 
X		return (LINK);
X
X	/* make dotdot */
X	strcat(dotdot, ".");
X	if ((link(parent, dotdot)) < 0) 
X		return (LINK);
X
X	return (0);
X}
X
rmdir(dir)
char *dir;
X{
X	char dots[MAXPATHLEN];
X
X#ifdef DEBUG
X	printf("rmdir(%s)\n", dir);
X	fflush(stdout);
X#endif /*DEBUG*/
X
X	strcpy(dots, dir);
X	strcat(dots, "/.");
X
X	/* unlink(".") */
X	if (unlink(dots) < 0)
X		return (LINK);
X
X	/* unlink("..") */
X	strcat(dots, ".");
X	if (unlink(dots) < 0)
X		return (LINK);
X
X	/* unlink the directory itself */
X	if (unlink(dir) < 0)
X		return (LINK);
X
X	return (0);
X}
X
print(s)
char *s;
X{
X	write(2, s, strlen(s));
X}
X
error(s1, s2, err)
char *s1, *s2;
int err;
X{
X	write(2, Progname, strlen(Progname));
X	write(2, ": ", 2);
X	write(2, s1, strlen(s1));
X	errno = err;
X	if (s2 != NULL)
X		write(2, s2, strlen(s2));
X	if (err != 0) 
X		perror(" ");
X	else 
X		write(2, "\n", 1);
X	exit(errno);
X}
swat_sigs()
X{
X	register int i;
X
X	for (i=SIGHUP; i<=NSIG ; i++)
X		signal(i, SIG_IGN); /* bye-bye */
X}
canIwrite(stbuff, uid, gid)
register struct stat *stbuff;
register unsigned short uid, gid;
X{
X	/* we let root get away with anything... */
X	if (uid == 0)
X		return(0);
X
X	/* can I write in it as an OWNER ? */
X	if (uid == stbuff->st_uid && stbuff->st_mode & 0200) 
X		return(0);
X
X	/* okay, so how about as a GROUP ? */
X	if (gid == stbuff->st_gid && stbuff->st_mode & 0020)
X		return(0);
X
X	/* alright, how about an OTHER ? */
X	if (stbuff->st_mode & 0002)
X		return(0);
X
X	/* okay, so I can't write here.. */
X	return(-1);
X}
X#ifdef DEBUG
unlink(s)
char *s;
X{
X	printf("Unlink(%s)\n", s);
X	fflush(stdout);
X}
X#endif /*DEBUG*/
END_OF_FILE
if test 7645 -ne `wc -c <'mkdir.c'`; then
    echo shar: \"'mkdir.c'\" unpacked with wrong size!
fi
chmod +x 'mkdir.c'
# end of 'mkdir.c'
fi
echo shar: End of shell archive.
exit 0