Use LD_PRELOAD to inject pledge() in glibc progs

We're now able to drop both `exec` and `prot_exec` privileges
automatically when launching glibc dynamic executables. We also have
really outstanding standard error logging now, that explains which
promises are needed, even in cases where `exec` is used.
This commit is contained in:
Justine Tunney 2022-08-08 21:23:37 -07:00
parent 0277d7d6e9
commit 6b3d257588
5 changed files with 286 additions and 178 deletions

View file

@ -68,7 +68,6 @@
#define TTY 0x8000 #define TTY 0x8000
#define UNIX 0x4000 #define UNIX 0x4000
#define NOBITS 0x8000 #define NOBITS 0x8000
#define NOSIGSYS 0x8000
#define RESTRICT 0x1000 #define RESTRICT 0x1000
#define PLEDGE(pledge) pledge, ARRAYLEN(pledge) #define PLEDGE(pledge) pledge, ARRAYLEN(pledge)
@ -190,7 +189,7 @@ static const uint16_t kPledgeStdio[] = {
__NR_linux_eventfd2, // __NR_linux_eventfd2, //
__NR_linux_signalfd, // __NR_linux_signalfd, //
__NR_linux_signalfd4, // __NR_linux_signalfd4, //
__NR_linux_sigaction | NOSIGSYS, // __NR_linux_sigaction, //
__NR_linux_sigaltstack, // __NR_linux_sigaltstack, //
__NR_linux_sigprocmask, // __NR_linux_sigprocmask, //
__NR_linux_sigsuspend, // __NR_linux_sigsuspend, //
@ -585,18 +584,21 @@ static privileged void KillThisThread(void) {
: "rcx", "r11", "memory"); : "rcx", "r11", "memory");
} }
static privileged bool HasSyscall(struct Pledges *p, uint16_t n) { static privileged int HasSyscall(struct Pledges *p, uint16_t n) {
int i; int i;
for (i = 0; i < p->len; ++i) { for (i = 0; i < p->len; ++i) {
if ((p->syscalls[i] & 0x0fff) == n) { if (p->syscalls[i] == n) {
return true; return 1;
}
if ((p->syscalls[i] & 0xfff) == n) {
return 2;
} }
} }
return false; return 0;
} }
static privileged void OnSigSys(int sig, siginfo_t *si, ucontext_t *ctx) { static privileged void OnSigSys(int sig, siginfo_t *si, ucontext_t *ctx) {
int i; int i, ok;
bool found; bool found;
char ord[17], rip[17]; char ord[17], rip[17];
enum PledgeMode mode = si->si_errno; enum PledgeMode mode = si->si_errno;
@ -604,15 +606,23 @@ static privileged void OnSigSys(int sig, siginfo_t *si, ucontext_t *ctx) {
FixCpy(ord, si->si_syscall, 12); FixCpy(ord, si->si_syscall, 12);
HexCpy(rip, ctx->uc_mcontext.rip); HexCpy(rip, ctx->uc_mcontext.rip);
for (found = i = 0; i < ARRAYLEN(kPledge); ++i) { for (found = i = 0; i < ARRAYLEN(kPledge); ++i) {
if (HasSyscall(kPledge + i, si->si_syscall)) { switch (HasSyscall(kPledge + i, si->si_syscall)) {
Log("error: has not pledged ", kPledge[i].name, // case 1:
Log("error: should pledge ", kPledge[i].name, //
" (ord=", ord, " rip=", rip, ")\n", 0); " (ord=", ord, " rip=", rip, ")\n", 0);
found = true; found = true;
break; break;
case 2:
Log("error: maybe pledge ", kPledge[i].name, //
" (ord=", ord, " rip=", rip, ")\n", 0);
found = true;
break;
default:
break;
} }
} }
if (!found) { if (!found) {
Log("error: unsupported syscall (ord=", ord, " rip=", rip, ")\n", 0); Log("error: bad syscall (ord=", ord, " rip=", rip, ")\n", 0);
} }
switch (mode) { switch (mode) {
case kPledgeModeKillProcess: case kPledgeModeKillProcess:
@ -876,6 +886,7 @@ static privileged void AllowSetsockoptRestrict(struct Filter *f) {
// The optname argument of getsockopt() must be one of: // The optname argument of getsockopt() must be one of:
// //
// - SO_TYPE (0x03) // - SO_TYPE (0x03)
// - SO_ERROR (0x04)
// - SO_REUSEPORT (0x0f) // - SO_REUSEPORT (0x0f)
// - SO_REUSEADDR (0x02) // - SO_REUSEADDR (0x02)
// - SO_KEEPALIVE (0x09) // - SO_KEEPALIVE (0x09)
@ -885,20 +896,21 @@ static privileged void AllowSetsockoptRestrict(struct Filter *f) {
static privileged void AllowGetsockoptRestrict(struct Filter *f) { static privileged void AllowGetsockoptRestrict(struct Filter *f) {
static const int nr = __NR_linux_getsockopt; static const int nr = __NR_linux_getsockopt;
static const struct sock_filter fragment[] = { static const struct sock_filter fragment[] = {
/* L0*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, nr, 0, 13 - 1), /* L0*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, nr, 0, 14 - 1),
/* L1*/ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(args[1])), /* L1*/ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(args[1])),
/* L2*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 1, 1, 0), /* L2*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 1, 1, 0),
/* L3*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 6, 0, 12 - 4), /* L3*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 6, 0, 13 - 4),
/* L4*/ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(args[2])), /* L4*/ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(args[2])),
/* L5*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x03, 5, 0), /* L5*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x03, 6, 0),
/* L6*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x0f, 4, 0), /* L6*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x04, 5, 0),
/* L7*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x02, 3, 0), /* L7*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x0f, 4, 0),
/* L8*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x09, 2, 0), /* L8*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x02, 3, 0),
/* L9*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x14, 1, 0), /* L9*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x09, 2, 0),
/*L10*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x15, 0, 1), /*L10*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x14, 1, 0),
/*L11*/ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), /*L11*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x15, 0, 1),
/*L12*/ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(nr)), /*L12*/ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
/*L13*/ /* next filter */ /*L13*/ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(nr)),
/*L14*/ /* next filter */
}; };
AppendFilter(f, PLEDGE(fragment)); AppendFilter(f, PLEDGE(fragment));
} }
@ -1215,24 +1227,6 @@ static privileged void AllowSendtoAddrless(struct Filter *f) {
AppendFilter(f, PLEDGE(fragment)); AppendFilter(f, PLEDGE(fragment));
} }
// The sig parameter of sigaction() must NOT be
//
// - SIGSYS (31) [always eperm]
//
static privileged void AllowSigactionNosigsys(struct Filter *f) {
static const int nr = __NR_linux_sigaction;
static const struct sock_filter fragment[] = {
/*L0*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, nr, 0, 6 - 1),
/*L1*/ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(args[0])),
/*L2*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 31, 0, 1),
/*L3*/ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | Eperm),
/*L4*/ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
/*L5*/ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(nr)),
/*L6*/ /* next filter */
};
AppendFilter(f, PLEDGE(fragment));
}
// The family parameter of socket() must be one of: // The family parameter of socket() must be one of:
// //
// - AF_INET (0x02) // - AF_INET (0x02)
@ -1479,9 +1473,6 @@ static privileged void AppendPledge(struct Filter *f, //
case __NR_linux_fchmodat | NOBITS: case __NR_linux_fchmodat | NOBITS:
AllowFchmodatNobits(f); AllowFchmodatNobits(f);
break; break;
case __NR_linux_sigaction | NOSIGSYS:
AllowSigactionNosigsys(f);
break;
case __NR_linux_prctl | STDIO: case __NR_linux_prctl | STDIO:
AllowPrctlStdio(f); AllowPrctlStdio(f);
break; break;
@ -1582,7 +1573,11 @@ privileged int sys_pledge_linux(unsigned long ipromises, //
AppendPledge(&f, PLEDGE(kPledgeDefault)); AppendPledge(&f, PLEDGE(kPledgeDefault));
for (i = 0; i < ARRAYLEN(kPledge); ++i) { for (i = 0; i < ARRAYLEN(kPledge); ++i) {
if (~ipromises & (1ul << i)) { if (~ipromises & (1ul << i)) {
if (kPledge[i].len) {
AppendPledge(&f, kPledge[i].syscalls, kPledge[i].len); AppendPledge(&f, kPledge[i].syscalls, kPledge[i].len);
} else {
AbortPledge("bad ipromises");
}
} }
} }

View file

@ -86,7 +86,7 @@ TEST(pledge, testKillProcessMode) {
TEST(pledge, testLogMessage_inSoftyMode) { TEST(pledge, testLogMessage_inSoftyMode) {
if (IsOpenbsd()) return; if (IsOpenbsd()) return;
int fds[2]; int fds[2];
char msg[64] = {0}; char msg[256] = {0};
ASSERT_SYS(0, 0, pipe(fds)); ASSERT_SYS(0, 0, pipe(fds));
SPAWN(fork); SPAWN(fork);
__pledge_mode = kPledgeModeErrno; __pledge_mode = kPledgeModeErrno;
@ -98,13 +98,13 @@ TEST(pledge, testLogMessage_inSoftyMode) {
read(fds[0], msg, sizeof(msg)); read(fds[0], msg, sizeof(msg));
close(fds[0]); close(fds[0]);
if (IsLinux()) { if (IsLinux()) {
ASSERT_STARTSWITH("error: has not pledged inet", msg); ASSERT_STARTSWITH("error: maybe pledge inet", msg);
} }
} }
TEST(pledge, testLogMessage_onKillProcess) { TEST(pledge, testLogMessage_onKillProcess) {
int fds[2]; int fds[2];
char msg[64] = {0}; char msg[256] = {0};
ASSERT_SYS(0, 0, pipe(fds)); ASSERT_SYS(0, 0, pipe(fds));
SPAWN(fork); SPAWN(fork);
__pledge_mode = kPledgeModeKillThread; __pledge_mode = kPledgeModeKillThread;
@ -116,13 +116,13 @@ TEST(pledge, testLogMessage_onKillProcess) {
read(fds[0], msg, sizeof(msg)); read(fds[0], msg, sizeof(msg));
close(fds[0]); close(fds[0]);
if (IsLinux()) { if (IsLinux()) {
ASSERT_STARTSWITH("error: has not pledged inet", msg); ASSERT_STARTSWITH("error: maybe pledge inet", msg);
} }
} }
TEST(pledge, testNoLogOrAbrtsignoPossibleSadly_becausePledgedExec) { TEST(pledge, testNoLogOrAbrtsignoPossibleSadly_becausePledgedExec) {
int fds[2]; int fds[2];
char msg[64] = {0}; char msg[256] = {0};
ASSERT_SYS(0, 0, pipe(fds)); ASSERT_SYS(0, 0, pipe(fds));
SPAWN(fork); SPAWN(fork);
ASSERT_SYS(0, 2, dup2(fds[1], 2)); ASSERT_SYS(0, 2, dup2(fds[1], 2));

View file

@ -119,23 +119,45 @@ o/$(MODE)/tool/build/dd.zip.o: o/$(MODE)/tool/build/dd
# we need pic because: # we need pic because:
# so it can be an LD_PRELOAD payload # so it can be an LD_PRELOAD payload
o/$(MODE)/tool/build/sandbox.o: \ o/$(MODE)/tool/build/dso/sandbox.o: \
OVERRIDE_CFLAGS += \ OVERRIDE_CFLAGS += \
-fPIC -fPIC
o/$(MODE)/tool/build/sandbox.so: \ o/$(MODE)/tool/build/dso/sandbox.o: \
o/$(MODE)/tool/build/sandbox.o \ libc/calls/calls.h \
tool/build/dso/sandbox.c \
libc/calls/pledge.h \
libc/runtime/runtime.h \
libc/calls/pledge.internal.h \
libc/intrin/promises.internal.h \
tool/build/build.mk
o/$(MODE)/tool/build/dso/sandbox.so: \
o/$(MODE)/tool/build/dso/sandbox.o \
o/$(MODE)/libc/calls/pledge-linux.o \ o/$(MODE)/libc/calls/pledge-linux.o \
o/$(MODE)/libc/sysv/restorert.o o/$(MODE)/libc/sysv/restorert.o
@$(COMPILE) -ALINK.so \ @$(CC) -s \
$(CC) \
-s \
-shared \ -shared \
-nostdlib \ -nostdlib \
-Wl,--gc-sections \ -Wl,--gc-sections \
$(LINKARGS) \ o/$(MODE)/tool/build/dso/sandbox.o \
o/$(MODE)/libc/calls/pledge-linux.o \
o/$(MODE)/libc/sysv/restorert.o \
$(OUTPUT_OPTION) $(OUTPUT_OPTION)
o/$(MODE)/tool/build/dso/sandbox.so.zip.o: \
ZIPOBJ_FLAGS += \
-B
o/$(MODE)/tool/build/pledge.com.dbg: \
$(TOOL_BUILD_DEPS) \
o/$(MODE)/tool/build/build.pkg \
o/$(MODE)/tool/build/dso/sandbox.so.zip.o \
o/$(MODE)/tool/build/pledge.o \
$(CRT) \
$(APE_NO_MODIFY_SELF)
@$(APELINK)
.PHONY: o/$(MODE)/tool/build .PHONY: o/$(MODE)/tool/build
o/$(MODE)/tool/build: \ o/$(MODE)/tool/build: \
o/$(MODE)/tool/build/emucrt \ o/$(MODE)/tool/build/emucrt \

View file

@ -16,13 +16,35 @@
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE. PERFORMANCE OF THIS SOFTWARE.
*/ */
#include "libc/calls/calls.h"
#include "libc/calls/pledge.h" #include "libc/calls/pledge.h"
#include "libc/calls/pledge.internal.h" #include "libc/calls/pledge.internal.h"
#include "libc/intrin/promises.internal.h" #include "libc/intrin/promises.internal.h"
#include "libc/runtime/runtime.h"
hidden char __privileged_start; /*
hidden char __privileged_end; * runs pledge at glibc executable load time, e.g.
* strace -vff bash -c '_PLEDGE=4194303,0,1 LD_PRELOAD=$HOME/sandbox.so ls'
*/
__attribute__((__constructor__)) void InitializeSandbox(void) { hidden uint8_t __privileged_start[1];
sys_pledge_linux(~(1ul << PROMISE_STDIO), kPledgeModeErrno, false); hidden uint8_t __privileged_end[1];
__attribute__((__constructor__)) void init(void) {
int c, i, j;
const char *s;
uint64_t arg[3] = {0};
s = getenv("_PLEDGE");
for (i = j = 0; i < 3; ++i) {
while ((c = s[j] & 255)) {
++j;
if ('0' <= c & c <= '9') {
arg[i] *= 10;
arg[i] += c - '0';
} else {
break;
}
}
}
sys_pledge_linux(~arg[0], arg[1], arg[2]);
} }

View file

@ -16,6 +16,7 @@
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE. PERFORMANCE OF THIS SOFTWARE.
*/ */
#include "libc/assert.h"
#include "libc/bits/bits.h" #include "libc/bits/bits.h"
#include "libc/bits/safemacros.internal.h" #include "libc/bits/safemacros.internal.h"
#include "libc/calls/calls.h" #include "libc/calls/calls.h"
@ -36,6 +37,7 @@
#include "libc/intrin/promises.internal.h" #include "libc/intrin/promises.internal.h"
#include "libc/macros.internal.h" #include "libc/macros.internal.h"
#include "libc/math.h" #include "libc/math.h"
#include "libc/mem/io.h"
#include "libc/mem/mem.h" #include "libc/mem/mem.h"
#include "libc/nexgen32e/kcpuids.h" #include "libc/nexgen32e/kcpuids.h"
#include "libc/runtime/gc.internal.h" #include "libc/runtime/gc.internal.h"
@ -65,6 +67,7 @@
// //
STATIC_YOINK("strerror_wr"); STATIC_YOINK("strerror_wr");
STATIC_YOINK("zip_uri_support");
#define USAGE \ #define USAGE \
"\ "\
@ -123,6 +126,7 @@ int g_uflag;
int g_kflag; int g_kflag;
int g_hflag; int g_hflag;
bool g_nice; bool g_nice;
bool isdynamic;
bool g_noclose; bool g_noclose;
long g_cpuquota; long g_cpuquota;
long g_fszquota; long g_fszquota;
@ -131,6 +135,8 @@ long g_proquota;
long g_dontdrop; long g_dontdrop;
const char *g_chroot; const char *g_chroot;
const char *g_promises; const char *g_promises;
char dsopath[PATH_MAX];
char tmppath[PATH_MAX];
struct { struct {
int n; int n;
@ -393,7 +399,8 @@ void ApplyFilesystemPolicy(unsigned long ipromises) {
Unveil(prog, "rx"); Unveil(prog, "rx");
if (IsDynamicExecutable(prog)) { if (isdynamic) {
Unveil(dsopath, "rx");
UnveilIfExists("/lib", "rx"); UnveilIfExists("/lib", "rx");
UnveilIfExists("/lib64", "rx"); UnveilIfExists("/lib64", "rx");
UnveilIfExists("/usr/lib", "rx"); UnveilIfExists("/usr/lib", "rx");
@ -518,8 +525,36 @@ void DropCapabilities(void) {
} }
} }
bool FileExistsAndIsNewerThan(const char *filepath, const char *thanpath) {
struct stat st1, st2;
if (stat(filepath, &st1) == -1) return false;
if (stat(thanpath, &st2) == -1) return false;
if (st1.st_mtim.tv_sec < st2.st_mtim.tv_sec) return false;
if (st1.st_mtim.tv_sec > st2.st_mtim.tv_sec) return true;
return st1.st_mtim.tv_nsec >= st2.st_mtim.tv_nsec;
}
int Extract(const char *from, const char *to, int mode) {
int fdin, fdout;
if ((fdin = open(from, O_RDONLY)) == -1) return -1;
if ((fdout = creat(to, mode)) == -1) {
close(fdin);
return -1;
}
if (_copyfd(fdin, fdout, -1) == -1) {
close(fdout);
close(fdin);
return -1;
}
return close(fdout) | close(fdin);
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
const char *s;
bool hasfunbits; bool hasfunbits;
int fdin, fdout;
char buf[PATH_MAX];
int e, zipfd, memfd;
int useruid, usergid; int useruid, usergid;
int owneruid, ownergid; int owneruid, ownergid;
int oldfsuid, oldfsgid; int oldfsuid, oldfsgid;
@ -607,6 +642,29 @@ int main(int argc, char *argv[]) {
setfsgid(oldfsgid); setfsgid(oldfsgid);
} }
// figure out where we want the dso
if (IsDynamicExecutable(prog)) {
isdynamic = true;
if ((s = getenv("TMPDIR")) || //
(s = getenv("HOME")) || //
(s = ".")) {
ksnprintf(dsopath, sizeof(dsopath), "%s/sandbox.so", s);
if (!FileExistsAndIsNewerThan(dsopath, GetProgramExecutableName())) {
ksnprintf(tmppath, sizeof(tmppath), "%s/sandbox.so.%d", s, getpid());
if (Extract("/zip/sandbox.so", tmppath, 0755) == -1) {
kprintf("error: extract dso failed: %m\n");
exit(1);
}
if (rename(tmppath, dsopath) == -1) {
kprintf("error: rename dso failed: %m\n");
exit(1);
}
}
ksnprintf(buf, sizeof(buf), "LD_PRELOAD=%s", dsopath);
putenv(buf);
}
}
if (g_dontdrop) { if (g_dontdrop) {
if (hasfunbits) { if (hasfunbits) {
kprintf("error: -D flag forbidden on setuid binaries\n"); kprintf("error: -D flag forbidden on setuid binaries\n");
@ -669,11 +727,6 @@ int main(int argc, char *argv[]) {
ApplyFilesystemPolicy(ipromises); ApplyFilesystemPolicy(ipromises);
// we always need exec which is a weakness of this model
if (!(~ipromises & (1ul << PROMISE_EXEC))) {
g_promises = xstrcat(g_promises, ' ', "exec");
}
// pledge.com uses the return eperm instead of killing the process // pledge.com uses the return eperm instead of killing the process
// model. we do this becasue it's only possible to have sigsys print // model. we do this becasue it's only possible to have sigsys print
// crash messages if we're not pledging exec, which is what this tool // crash messages if we're not pledging exec, which is what this tool
@ -684,6 +737,22 @@ int main(int argc, char *argv[]) {
__pledge_mode = kPledgeModeErrno; __pledge_mode = kPledgeModeErrno;
} }
// we need to be able to call execv and mmap the dso
// it'll be pledged away once/if the dso gets loaded
if (!(~ipromises & (1ul << PROMISE_EXEC))) {
g_promises = xstrcat(g_promises, ' ', "exec");
}
if (isdynamic) {
g_promises = xstrcat(g_promises, ' ', "prot_exec");
}
// pass arguments to pledge() inside the dso
if (isdynamic) {
ksnprintf(buf, sizeof(buf), "_PLEDGE=%ld,%ld,%ld", ~ipromises,
__pledge_mode, false);
putenv(buf);
}
// apply sandbox // apply sandbox
if (pledge(g_promises, g_promises) == -1) { if (pledge(g_promises, g_promises) == -1) {
kprintf("error: pledge(%#s) failed: %m\n", g_promises); kprintf("error: pledge(%#s) failed: %m\n", g_promises);