From 8b88a69d9c0d579a0e882d41c4fcec91f30f9761 Mon Sep 17 00:00:00 2001 From: Robert Muir Date: Thu, 24 Sep 2015 12:17:21 -0400 Subject: [PATCH] Block process execution with seccomp on linux/amd64 Block execve(), fork(), and vfork() system calls, returning EACCES instead, on kernels that support seccomp-bpf: either via seccomp() or falling back to prctl(). Only linux/amd64 is supported. This feature can be disabled (in case of problems) with bootstrap.seccomp=false. Closes #13753 Squashed commit of the following: commit 92cee05c72b49e532d41be7b16709e1c9f919fa9 Author: Robert Muir Date: Thu Sep 24 10:12:51 2015 -0400 Add a note about why we don't parse uname() or anything commit b427971f45cbda4d0b964ddc4a55fae638880335 Author: Robert Muir Date: Thu Sep 24 09:44:31 2015 -0400 style only: we already pull errno into a local, use it for catch-all case commit ddf93305525ed1546baf91f7902148a8f5b1ad06 Author: Robert Muir Date: Thu Sep 24 08:36:01 2015 -0400 add TODO commit f29d1b7b809a9d4c1fcf15f6064e43f7d1b24696 Author: Robert Muir Date: Thu Sep 24 08:33:28 2015 -0400 Add full stacktrace at debug level always commit a3c991ff8b0b16dc5e128af8fb3dfa6346c6d6f1 Author: Robert Muir Date: Thu Sep 24 00:08:19 2015 -0400 Add missing check just in case. commit 628ed9c77603699aa9c67890fe7632b0e662a911 Author: Robert Muir Date: Wed Sep 23 22:47:16 2015 -0400 Add public getter, for stats or whatever if they need to know this commit 3e2265b5f89d42043d9a07d4525ce42e2cb1c727 Author: Robert Muir Date: Wed Sep 23 22:43:06 2015 -0400 Enable use of seccomp(2) on Linux 3.17+ which provides more protection. Add nice errors. Add all kinds of checks and paranoia. Add documentation. Add boolean switch. commit 0e421f7fa2d5236c8fa2cd073bcb616f5bcd2d23 Author: Robert Muir Date: Wed Sep 23 21:36:32 2015 -0400 Add defensive checks and nice error messages commit 6231c3b7c96a81af8460cde30135e077f60a3f39 Author: Robert Muir Date: Wed Sep 23 20:52:40 2015 -0400 clean up JNA and BPF. block fork and vfork too. commit bb31e8a6ef03ceeb1d5137c84d50378c270af85a Author: Robert Muir Date: Wed Sep 23 19:00:32 2015 -0400 order is LE already for the JNA buffer, but be explicit about it commit 10456d2f08f12ddc3d60989acb86b37be6a4b12b Author: Robert Muir Date: Wed Sep 23 17:47:07 2015 -0400 block process execution with seccomp on linux/amd64 --- .../elasticsearch/bootstrap/Bootstrap.java | 10 +- .../bootstrap/BootstrapInfo.java | 7 + .../elasticsearch/bootstrap/JNANatives.java | 17 ++ .../org/elasticsearch/bootstrap/Natives.java | 15 + .../org/elasticsearch/bootstrap/Seccomp.java | 271 ++++++++++++++++++ .../bootstrap/BootstrapForTesting.java | 2 +- .../elasticsearch/bootstrap/SeccompTests.java | 73 +++++ 7 files changed, 392 insertions(+), 3 deletions(-) create mode 100644 core/src/main/java/org/elasticsearch/bootstrap/Seccomp.java create mode 100644 core/src/test/java/org/elasticsearch/bootstrap/SeccompTests.java diff --git a/core/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java b/core/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java index e8fb638bf31..c9c3eeb8974 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java @@ -79,7 +79,7 @@ final class Bootstrap { } /** initialize native resources */ - public static void initializeNatives(boolean mlockAll, boolean ctrlHandler) { + public static void initializeNatives(boolean mlockAll, boolean seccomp, boolean ctrlHandler) { final ESLogger logger = Loggers.getLogger(Bootstrap.class); // check if the user is running as root, and bail @@ -91,6 +91,11 @@ final class Bootstrap { } } + // enable secure computing mode + if (seccomp) { + Natives.trySeccomp(); + } + // mlockall if requested if (mlockAll) { if (Constants.WINDOWS) { @@ -134,7 +139,8 @@ final class Bootstrap { private void setup(boolean addShutdownHook, Settings settings, Environment environment) throws Exception { initializeNatives(settings.getAsBoolean("bootstrap.mlockall", false), - settings.getAsBoolean("bootstrap.ctrlhandler", true)); + settings.getAsBoolean("bootstrap.seccomp", true), + settings.getAsBoolean("bootstrap.ctrlhandler", true)); // initialize probes before the security manager is installed initializeProbes(); diff --git a/core/src/main/java/org/elasticsearch/bootstrap/BootstrapInfo.java b/core/src/main/java/org/elasticsearch/bootstrap/BootstrapInfo.java index 829c45f074e..76485bb86e5 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/BootstrapInfo.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/BootstrapInfo.java @@ -43,4 +43,11 @@ public final class BootstrapInfo { public static boolean isMemoryLocked() { return Natives.isMemoryLocked(); } + + /** + * Returns true if secure computing mode is enabled (linux/amd64 only) + */ + public static boolean isSeccompInstalled() { + return Natives.isSeccompInstalled(); + } } diff --git a/core/src/main/java/org/elasticsearch/bootstrap/JNANatives.java b/core/src/main/java/org/elasticsearch/bootstrap/JNANatives.java index 7a76d37bf12..647f923d5c7 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/JNANatives.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/JNANatives.java @@ -41,6 +41,8 @@ class JNANatives { // Set to true, in case native mlockall call was successful static boolean LOCAL_MLOCKALL = false; + // Set to true, in case native seccomp call was successful + static boolean LOCAL_SECCOMP = false; static void tryMlockall() { int errno = Integer.MIN_VALUE; @@ -170,4 +172,19 @@ class JNANatives { } } + static void trySeccomp() { + if (Constants.LINUX && "amd64".equals(Constants.OS_ARCH)) { + try { + Seccomp.installFilter(); + LOCAL_SECCOMP = true; + } catch (Exception e) { + // this is likely to happen unless the kernel is newish, its a best effort at the moment + // so we log stacktrace at debug for now... + if (logger.isDebugEnabled()) { + logger.debug("unable to install seccomp filter", e); + } + logger.warn("unable to install seccomp filter: " + e.getMessage()); + } + } + } } diff --git a/core/src/main/java/org/elasticsearch/bootstrap/Natives.java b/core/src/main/java/org/elasticsearch/bootstrap/Natives.java index 05b0088ae11..1bf9bb80617 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/Natives.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/Natives.java @@ -88,4 +88,19 @@ final class Natives { } return JNANatives.LOCAL_MLOCKALL; } + + static void trySeccomp() { + if (!JNA_AVAILABLE) { + logger.warn("cannot install seccomp filters because JNA is not available"); + return; + } + JNANatives.trySeccomp(); + } + + static boolean isSeccompInstalled() { + if (!JNA_AVAILABLE) { + return false; + } + return JNANatives.LOCAL_SECCOMP; + } } diff --git a/core/src/main/java/org/elasticsearch/bootstrap/Seccomp.java b/core/src/main/java/org/elasticsearch/bootstrap/Seccomp.java new file mode 100644 index 00000000000..409ebc2181f --- /dev/null +++ b/core/src/main/java/org/elasticsearch/bootstrap/Seccomp.java @@ -0,0 +1,271 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.bootstrap; + +import com.sun.jna.Library; +import com.sun.jna.Memory; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; + +import org.apache.lucene.util.Constants; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.List; + +/** + * Installs a limited form of Linux secure computing mode (filter mode). + * This filters system calls to block process execution. + *

+ * This is only supported on the amd64 architecture, on Linux kernels 3.5 or above, and requires + * {@code CONFIG_SECCOMP} and {@code CONFIG_SECCOMP_FILTER} compiled into the kernel. + *

+ * Filters are installed using either {@code seccomp(2)} (3.17+) or {@code prctl(2)} (3.5+). {@code seccomp(2)} + * is preferred, as it allows filters to be applied to any existing threads in the process, and one motivation + * here is to protect against bugs in the JVM. Otherwise, code will fall back to the {@code prctl(2)} method + * which will at least protect elasticsearch application threads. + *

+ * The filters will return {@code EACCES} (Access Denied) for the following system calls: + *

    + *
  • {@code execve}
  • + *
  • {@code fork}
  • + *
  • {@code vfork}
  • + *
+ *

+ * This is not intended as a sandbox. It is another level of security, mostly intended to annoy + * security researchers and make their lives more difficult in achieving "remote execution" exploits. + * @see + * http://www.kernel.org/doc/Documentation/prctl/seccomp_filter.txt + */ +// only supported on linux/amd64 +// not an example of how to write code!!! +final class Seccomp { + private static final ESLogger logger = Loggers.getLogger(Seccomp.class); + + /** we use an explicit interface for native methods, for varargs support */ + static interface LinuxLibrary extends Library { + /** + * maps to prctl(2) + */ + int prctl(int option, long arg2, long arg3, long arg4, long arg5); + /** + * used to call seccomp(2), its too new... + * this is the only way, DONT use it on some other architecture unless you know wtf you are doing + */ + long syscall(long number, Object... args); + }; + + // null if something goes wrong. + static final LinuxLibrary libc; + + static { + LinuxLibrary lib = null; + try { + lib = (LinuxLibrary) Native.loadLibrary("c", LinuxLibrary.class); + } catch (UnsatisfiedLinkError e) { + logger.warn("unable to link C library. native methods (seccomp) will be disabled.", e); + } + libc = lib; + } + + /** the preferred method is seccomp(2), since we can apply to all threads of the process */ + static final int SECCOMP_SYSCALL_NR = 317; // since Linux 3.17 + static final int SECCOMP_SET_MODE_FILTER = 1; // since Linux 3.17 + static final int SECCOMP_FILTER_FLAG_TSYNC = 1; // since Linux 3.17 + + /** otherwise, we can use prctl(2), which will at least protect ES application threads */ + static final int PR_GET_NO_NEW_PRIVS = 39; // since Linux 3.5 + static final int PR_SET_NO_NEW_PRIVS = 38; // since Linux 3.5 + static final int PR_GET_SECCOMP = 21; // since Linux 2.6.23 + static final int PR_SET_SECCOMP = 22; // since Linux 2.6.23 + static final int SECCOMP_MODE_FILTER = 2; // since Linux Linux 3.5 + + /** corresponds to struct sock_filter */ + static final class SockFilter { + short code; // insn + byte jt; // number of insn to jump (skip) if true + byte jf; // number of insn to jump (skip) if false + int k; // additional data + + SockFilter(short code, byte jt, byte jf, int k) { + this.code = code; + this.jt = jt; + this.jf = jf; + this.k = k; + } + } + + /** corresponds to struct sock_fprog */ + public static final class SockFProg extends Structure implements Structure.ByReference { + public short len; // number of filters + public Pointer filter; // filters + + public SockFProg(SockFilter filters[]) { + len = (short) filters.length; + // serialize struct sock_filter * explicitly, its less confusing than the JNA magic we would need + Memory filter = new Memory(len * 8); + ByteBuffer bbuf = filter.getByteBuffer(0, len * 8); + bbuf.order(ByteOrder.nativeOrder()); // little endian + for (SockFilter f : filters) { + bbuf.putShort(f.code); + bbuf.put(f.jt); + bbuf.put(f.jf); + bbuf.putInt(f.k); + } + this.filter = filter; + } + + @Override + protected List getFieldOrder() { + return Arrays.asList(new String[] { "len", "filter" }); + } + } + + // BPF "macros" and constants + static final int BPF_LD = 0x00; + static final int BPF_W = 0x00; + static final int BPF_ABS = 0x20; + static final int BPF_JMP = 0x05; + static final int BPF_JEQ = 0x10; + static final int BPF_JGE = 0x30; + static final int BPF_JGT = 0x20; + static final int BPF_RET = 0x06; + static final int BPF_K = 0x00; + + static SockFilter BPF_STMT(int code, int k) { + return new SockFilter((short) code, (byte) 0, (byte) 0, k); + } + + static SockFilter BPF_JUMP(int code, int k, int jt, int jf) { + return new SockFilter((short) code, (byte) jt, (byte) jf, k); + } + + static final int AUDIT_ARCH_X86_64 = 0xC000003E; + static final int SECCOMP_RET_ERRNO = 0x00050000; + static final int SECCOMP_RET_DATA = 0x0000FFFF; + static final int SECCOMP_RET_ALLOW = 0x7FFF0000; + + // some errno constants for error checking/handling + static final int EACCES = 0x0D; + static final int EFAULT = 0x0E; + static final int EINVAL = 0x16; + static final int ENOSYS = 0x26; + + // offsets (arch dependent) that our BPF checks + static final int SECCOMP_DATA_NR_OFFSET = 0x00; + static final int SECCOMP_DATA_ARCH_OFFSET = 0x04; + + // currently this range is blocked (inclusive): + // execve is really the only one needed but why let someone fork a 30G heap? (not really what happens) + // ... + // 57: fork + // 58: vfork + // 59: execve + // ... + static final int BLACKLIST_START = 57; + static final int BLACKLIST_END = 59; + + // TODO: execveat()? its less of a risk since the jvm does not use it... + + /** try to install our filters */ + static void installFilter() { + // first be defensive: we can give nice errors this way, at the very least. + // also, some of these security features get backported to old versions, checking kernel version here is a big no-no! + boolean supported = Constants.LINUX && "amd64".equals(Constants.OS_ARCH); + if (supported == false) { + throw new IllegalStateException("bug: should not be trying to initialize seccomp for an unsupported architecture"); + } + + // we couldn't link methods, could be some really ancient kernel (e.g. < 2.1.57) or some bug + if (libc == null) { + throw new UnsupportedOperationException("seccomp unavailable: could not link methods. requires kernel 3.5+ with CONFIG_SECCOMP and CONFIG_SECCOMP_FILTER compiled in"); + } + + // check for kernel version + if (libc.prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) < 0) { + int errno = Native.getLastError(); + switch (errno) { + case ENOSYS: throw new UnsupportedOperationException("seccomp unavailable: requires kernel 3.5+ with CONFIG_SECCOMP and CONFIG_SECCOMP_FILTER compiled in"); + default: throw new UnsupportedOperationException("prctl(PR_GET_NO_NEW_PRIVS): " + JNACLibrary.strerror(errno)); + } + } + // check for SECCOMP + if (libc.prctl(PR_GET_SECCOMP, 0, 0, 0, 0) < 0) { + int errno = Native.getLastError(); + switch (errno) { + case EINVAL: throw new UnsupportedOperationException("seccomp unavailable: CONFIG_SECCOMP not compiled into kernel, CONFIG_SECCOMP and CONFIG_SECCOMP_FILTER are needed"); + default: throw new UnsupportedOperationException("prctl(PR_GET_SECCOMP): " + JNACLibrary.strerror(errno)); + } + } + // check for SECCOMP_MODE_FILTER + if (libc.prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, 0, 0, 0) < 0) { + int errno = Native.getLastError(); + switch (errno) { + case EFAULT: break; // available + case EINVAL: throw new UnsupportedOperationException("seccomp unavailable: CONFIG_SECCOMP_FILTER not compiled into kernel, CONFIG_SECCOMP and CONFIG_SECCOMP_FILTER are needed"); + default: throw new UnsupportedOperationException("prctl(PR_SET_SECCOMP): " + JNACLibrary.strerror(errno)); + } + } + + // ok, now set PR_SET_NO_NEW_PRIVS, needed to be able to set a seccomp filter as ordinary user + if (libc.prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) { + throw new UnsupportedOperationException("prctl(PR_SET_NO_NEW_PRIVS): " + JNACLibrary.strerror(Native.getLastError())); + } + + // BPF installed to check arch, then syscall range. See https://www.kernel.org/doc/Documentation/prctl/seccomp_filter.txt for details. + SockFilter insns[] = { + /* 1 */ BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_ARCH_OFFSET), // if (arch != amd64) goto fail; + /* 2 */ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, AUDIT_ARCH_X86_64, 0, 3), // + /* 3 */ BPF_STMT(BPF_LD + BPF_W + BPF_ABS, SECCOMP_DATA_NR_OFFSET), // if (syscall < BLACKLIST_START) goto pass; + /* 4 */ BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, BLACKLIST_START, 0, 2), // + /* 5 */ BPF_JUMP(BPF_JMP + BPF_JGT + BPF_K, BLACKLIST_END, 1, 0), // if (syscall > BLACKLIST_END) goto pass; + /* 6 */ BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (EACCES & SECCOMP_RET_DATA)), // fail: return EACCES; + /* 7 */ BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) // pass: return OK; + }; + + // seccomp takes a long, so we pass it one explicitly to keep the JNA simple + SockFProg prog = new SockFProg(insns); + prog.write(); + long pointer = Pointer.nativeValue(prog.getPointer()); + + // install filter, if this works, after this there is no going back! + // first try it with seccomp(SECCOMP_SET_MODE_FILTER), falling back to prctl() + if (libc.syscall(SECCOMP_SYSCALL_NR, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, pointer) != 0) { + int errno1 = Native.getLastError(); + if (logger.isDebugEnabled()) { + logger.debug("seccomp(SECCOMP_SET_MODE_FILTER): " + JNACLibrary.strerror(errno1) + ", falling back to prctl(PR_SET_SECCOMP)..."); + } + if (libc.prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, pointer, 0, 0) < 0) { + int errno2 = Native.getLastError(); + throw new UnsupportedOperationException("seccomp(SECCOMP_SET_MODE_FILTER): " + JNACLibrary.strerror(errno1) + + ", prctl(PR_SET_SECCOMP): " + JNACLibrary.strerror(errno2)); + } + } + + // now check that the filter was really installed, we should be in filter mode. + if (libc.prctl(PR_GET_SECCOMP, 0, 0, 0, 0) != 2) { + throw new UnsupportedOperationException("seccomp filter installation did not really succeed. seccomp(PR_GET_SECCOMP): " + JNACLibrary.strerror(Native.getLastError())); + } + } +} diff --git a/core/src/test/java/org/elasticsearch/bootstrap/BootstrapForTesting.java b/core/src/test/java/org/elasticsearch/bootstrap/BootstrapForTesting.java index 0f9664c9678..3dcd8ff0402 100644 --- a/core/src/test/java/org/elasticsearch/bootstrap/BootstrapForTesting.java +++ b/core/src/test/java/org/elasticsearch/bootstrap/BootstrapForTesting.java @@ -52,7 +52,7 @@ public class BootstrapForTesting { static { // just like bootstrap, initialize natives, then SM - Bootstrap.initializeNatives(true, true); + Bootstrap.initializeNatives(true, true, true); // initialize probes Bootstrap.initializeProbes(); diff --git a/core/src/test/java/org/elasticsearch/bootstrap/SeccompTests.java b/core/src/test/java/org/elasticsearch/bootstrap/SeccompTests.java new file mode 100644 index 00000000000..3fbaf5a60e1 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/bootstrap/SeccompTests.java @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.bootstrap; + +import org.elasticsearch.test.ESTestCase; + +/** Simple tests seccomp filter is working. */ +public class SeccompTests extends ESTestCase { + + @Override + public void setUp() throws Exception { + super.setUp(); + assumeTrue("requires seccomp filter installation", Natives.isSeccompInstalled()); + // otherwise security manager will block the execution, no fun + assumeTrue("cannot test with security manager enabled", System.getSecurityManager() == null); + } + + public void testNoExecution() throws Exception { + try { + Runtime.getRuntime().exec("ls"); + fail("should not have been able to execute!"); + } catch (Exception expected) { + // we can't guarantee how its converted, currently its an IOException, like this: + /* + java.io.IOException: Cannot run program "ls": error=13, Permission denied + at __randomizedtesting.SeedInfo.seed([65E6C4BED11899E:FC6E1CA6AA2DB634]:0) + at java.lang.ProcessBuilder.start(ProcessBuilder.java:1048) + at java.lang.Runtime.exec(Runtime.java:620) + ... + Caused by: java.io.IOException: error=13, Permission denied + at java.lang.UNIXProcess.forkAndExec(Native Method) + at java.lang.UNIXProcess.(UNIXProcess.java:248) + at java.lang.ProcessImpl.start(ProcessImpl.java:134) + at java.lang.ProcessBuilder.start(ProcessBuilder.java:1029) + ... + */ + } + } + + // make sure thread inherits this too (its documented that way) + public void testNoExecutionFromThread() throws Exception { + Thread t = new Thread() { + @Override + public void run() { + try { + Runtime.getRuntime().exec("ls"); + fail("should not have been able to execute!"); + } catch (Exception expected) { + // ok + } + } + }; + t.start(); + t.join(); + } +}