xorl %eax, %eax

Linux kernel exit_notify() Invalid Capability Check

leave a comment »

This is a kind of funny bug. It affects Linux kernel prior to 2.6.29-git14 and it was reported by Oleg Nesterov of Red Hat on 25 February 2009. Here I’ll be using 2.6.29 release of the Linux kernel to demonstrate this vulnerability. The bug can be found at kernel/exit.c which guess what is being used for :-P

So… Here we are:

913 /*
914  * Send signals to all our closest relatives so that they know
915  * to properly mourn us..
916  */
917 static void exit_notify(struct task_struct *tsk, int group_dead)
918 {
919        int signal;
920        void *cookie;
      ...
937        /* Let father know we died
938         *
939         * Thread signals are configurable, but you aren't going to use
940         * that to send signals to arbitary processes.
941         * That stops right now.
942         *
943         * If the parent exec id doesn't match the exec id we saved
944         * when we started then we know the parent has changed security
945         * domain.
946         *
947         * If our self_exec id doesn't match our parent_exec_id then
948         * we have changed execution domain as these two values started
949         * the same after a fork.
950         */
951        if (tsk->exit_signal != SIGCHLD && !task_detached(tsk) &&
952            (tsk->parent_exec_id != tsk->real_parent->self_exec_id ||
953             tsk->self_exec_id != tsk->parent_exec_id) &&
954            !capable(CAP_KILL))
955                tsk->exit_signal = SIGCHLD;
956
957        signal = tracehook_notify_death(tsk, &cookie, group_dead);
958        if (signal >= 0)
959                signal = do_notify_parent(tsk, signal);
      ...
973        /* If the process is dead, release it - nobody will wait for it */
974        if (signal == DEATH_REAP)
975                release_task(tsk);
976 }


The comments between line 937 through 950 are pretty descriptive. Now, when we move to the check in the if statement it needs to fulfil these requirements:
1) The signal shouldn’t be SIGCHLD
2) The task should not be detached
3) The self and parent, execution IDs should be the same
4) Check that the running task doesn’t have capability to kill
If all these are met, then the exit signal is set to SIGCHLD. To make this simpler, here is what this capability is used for as defined in include/linux/capability.h:

153 /* Overrides the restriction that the real or effective user ID of a
154    process sending a signal must match the real or effective user ID
155    of the process receiving the signal. */
156
157 #define CAP_KILL             5


However, the check at line 954 is a little bit weird. So, as Oleg Nesterov said:

Whatever logic we have to reset ->exit_signal, the malicious user
can bypass it if it execs the setuid application before exiting.

The patch for this issue was:

 	if (tsk->exit_signal != SIGCHLD && !task_detached(tsk) &&
 	    (tsk->parent_exec_id != tsk->real_parent->self_exec_id ||
-	     tsk->self_exec_id != tsk->parent_exec_id) &&
-	    !capable(CAP_KILL))
+	     tsk->self_exec_id != tsk->parent_exec_id))
 		tsk->exit_signal = SIGCHLD;


Just removing this fairly weird capability check from the if condition. But why is this such a big security issue? Well, it’s simple. As we can read from signal(7) man page:

 SIGCHLD   20,17,18    Ign     Child stopped or terminated

This signal is sent by a chlid process to its parent if it stops or terminates and it’s being stored into exit_signal member of the almighty task_struct structure as we can see from include/linux/sched.h:

1114 struct task_struct {
1115        volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
       ...
1170 /* task state */
1171        struct linux_binfmt *binfmt;
1172        int exit_state;
1173        int exit_code, exit_signal;
1174        int pdeath_signal;  /*  The signal sent when the parent dies  */
       ...
1420};

As Nesterov pointed out. If the child process exits with CAP_KILL capability, then the exit_signal will not be updated to contain SIGCHLD. Moreover, it will keep the signal stored in this variable (exit_signal) and send this to its parent process. That’s the problem with the above code :)

Written by xorl

April 8, 2009 at 23:34

Posted in bugs, linux

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s