25 次代码提交

作者 SHA1 备注 提交日期
bbbdfc44da exile.hpp: do_clone(): free stack memory 2022-05-29 19:25:53 +02:00
2dc61828f1 README: Clarify limitations 2022-04-29 21:25:21 +02:00
cdc265cedf c++: exile_launch(): Correct std::enable_if logic if type is a ptr 2022-04-29 21:23:53 +02:00
91858efa51 vows map: Add memfd_create, rseq 2022-04-22 08:37:34 +02:00
88995d214d README.md: Minor improvements (typos, rephrasing) 2022-04-07 00:04:52 +02:00
6eb47daf84 README: Update Debian section 2022-03-28 19:25:55 +02:00
8bf87717a5 vows: ioctl: Make TIOCSTI illegal even when IOCTL vow is set 2022-03-28 19:14:02 +02:00
bcaefffbe8 Improve various error messages 2022-03-28 19:04:28 +02:00
ed5098f2c6 README: Begin demo section 2022-03-17 17:10:38 +01:00
ea66ef76eb exile_flags_to_landlock(): Cover more with ALL_WRITE, except devices
More consistent with mount(), where MS_NODEV disallows those.

We may need to introduce a flag that simply allows everything
2022-03-17 15:47:22 +01:00
66def7a28f append_syscall_to_bpf(): Check for unlikely case of too many sock_filters 2022-03-17 15:47:22 +01:00
dbf8e87440 exile.hpp: Mark do_clone inline, not static 2022-03-17 15:47:22 +01:00
98421fab90 Makefile: Build exile.o separately, link it in all tests 2022-03-17 15:47:22 +01:00
70c3fef500 exile.h: Retire static child_read/write_pipe vars 2022-03-17 15:47:22 +01:00
69829374c7 exile.h: Move definitions to new file exile.c
Especially with exile_launch(), we will be included
from more than one translation unit. Thus, ODR becomes
a headache now.

So move definitions to exile.c.
2022-03-17 15:47:22 +01:00
005851c645 exile.h: Add extern "C" guards 2022-03-17 15:47:22 +01:00
95fa11e928 c++: Add explicit exile_launch() std::basic_string variant 2022-03-17 15:47:22 +01:00
97e2025758 c++: Retire exile_launch_trivial(), use std::enable_if 2022-03-17 15:47:22 +01:00
8cfb73568a Makefile: Add 'tests' target, depend on headers too to rebuild on changes of those 2022-03-17 15:47:22 +01:00
e7a5ba7f7f test.sh: Also run C++ tests 2022-03-17 15:47:22 +01:00
e52eda186b Add test.cpp to test C++ API 2022-03-17 15:47:22 +01:00
90ed5bbae9 Begin C++ API: Add exile.hpp with exile_launch() wrappers 2022-03-17 15:47:22 +01:00
48b6de9036 struct syscall_vow_map: change 'str' to const char* 2022-03-17 15:47:22 +01:00
93acb13929 test: Introduce LOG(), avoid inconsistent printf/fprintf 2022-03-17 15:47:22 +01:00
9247a6636b Introduce exile_vows_from_str() 2022-03-17 15:47:22 +01:00
共有 3 个文件被更改,包括 144 次插入22 次删除

144
README.md
查看文件

@ -1,22 +1,137 @@
# exile.h # exile.h
`exile.h` is a header-only library, enabling processes to easily isolate themselves on Linux for exploit mitigation. exile.h wants to make existing technologies, such as Seccomp and Linux Namespaces, easier to use. Those generally `exile.h` is a header-only library, enabling processes to easily isolate themselves on Linux for exploit mitigation purposes. exile.h wants to make existing technologies, such as Seccomp and Linux Namespaces, easier to use. Those generally
require knowledge of details and are not trivial for developers to employ, which prevents a more widespread adoption. require knowledge of details and are not trivial for developers to employ, which prevents a more widespread adoption.
The following section gives small quick examples. Then the motivation is explained in more detail. The following section offers small examples. Then the motivation is explained in more detail.
Proper API documentation will be maintained in other files. Proper API documentation will be maintained in other files.
## Quick demo ## Quick demo
TODO This section will demonstrate the simplicity of the API, but only serves as an overview. This section quickly demonstrates the simplicity of the API. It serves as an overview to get
a first impression.
system() is used to keep the example C code short. It also demonstrates that subprocesses are also subject to restrictions imposed by exile.h.
While the example show different features separately, it is generally possible to combine those.
### Filesystem isolation ### Filesystem isolation
```c
#include "exile.h"
#include <assert.h>
int main(void)
{
system("echo test > /home/user/testfile");
struct exile_policy *policy = exile_init_policy();
exile_append_path_policies(policy, EXILE_FS_ALLOW_ALL_READ, "/home/user");
exile_append_path_policies(policy, EXILE_FS_ALLOW_ALL_READ | EXILE_FS_ALLOW_ALL_WRITE, "/tmp");
int ret = exile_enable_policy(policy);
if(ret != 0)
{
exit(EXIT_FAILURE);
}
int fd = open("/home/user/test", O_CREAT | O_WRONLY | O_TRUNC, 0600);
assert(fd == -1);
fd = open("/home/user/testfile", O_RDONLY);
//use fd
assert(fd != -1);
fd = open("/tmp/testfile", O_CREAT | O_WRONLY | O_TRUNC, 0600);
//use fd
assert(fd != -1);
return 0;
}
```
The assert() calls won't be fired, consistent with the policy.
### System call policies / vows ### System call policies / vows
exile.h allows specifying which syscalls are permitted or denied. In the folloing example,
ls is never executed, as the specificed "vows" do not allow the execve() system call. The
process will be killed.
```c
#include "exile.h"
int main(void)
{
struct exile_policy *policy = exile_init_policy();
policy->vow_promises = exile_vows_from_str("stdio rpath wpath cpath");
exile_enable_policy(policy);
printf("Trying to execute...");
execlp("/bin/ls", "ls", "/", NULL);
}
```
### Isolation from network
exile offers a quick way to isolate a process from the default network namespace.
```c
#include "exile.h"
int main(void)
{
struct exile_policy *policy = exile_init_policy();
policy->namespace_options |= EXILE_UNSHARE_NETWORK;
int ret = exile_enable_policy(policy);
if(ret != 0)
{
exit(EXIT_FAILURE);
}
system("curl -I https://evil.tld");
}
```
Produces ```curl: (6) Could not resolve host: evil.tld```. For example, this is useful for subprocesses which do not need
network access, but perform tasks such as parsing user-supplied file formats.
### Isolation of single functions ### Isolation of single functions
exile_launch() demo Currently, work is being done that hopefully will allow isolation of individual function calls in a mostly pain-free manner.
Consider the following C++ code:
```cpp
#include <iostream>
#include <fstream>
#include "exile.hpp"
std::string cat(std::string path)
{
std::fstream f1;
f1.open(path.c_str(), std::ios::in);
std::string content;
std::string line;
while(getline(f1, line)) {
content += line + "\n";
}
return content;
}
int main(void)
{
struct exile_policy *policy = exile_init_policy();
policy->vow_promises = exile_vows_from_str("stdio rpath");
std::string content = exile_launch<std::string>(policy, cat, "/etc/hosts");
std::cout << content;
policy = exile_init_policy();
policy->vow_promises = exile_vows_from_str("stdio");
try
{
content = exile_launch<std::string>(policy, cat, "/etc/hosts");
std::cout << content;
}
catch(std::exception &e)
{
std::cout << "launch failure: " << e.what() << std::endl;
}
}
```
We execute "cat()". The first call succeeds. In the second, we get an exception, because
the subprocess "cat()" was launched in violated the policy (missing "rpath" vow).
Naturally, there is a performance overhead. Certain challenges remain, such as the fact
that being executed in a subprocess, we operate on copies, so handling references
is not something that has been given much thought. There is also the fact
that clone()ing from threads opens a can of worms. Hence, exile_launch()
is best avoided in multi-threaded contexts.
## Status ## Status
No release yet, experimental, API is unstable, builds will break on updates of this library. No release yet, experimental, API is unstable, builds will break on updates of this library.
@ -26,7 +141,7 @@ Currently, it's mainly evolving from the needs of my other projects.
## Motivation and Background ## Motivation and Background
exile.h unlocks existing Linux mechanisms to facilite isolation of processes from resources. Limiting the scope of what programs can do helps defending the rest of the system when a process gets under attacker's control (when classic mitigations such as ASLR etc. failed). To this end, OpenBSD has the pledge() and unveil() functions available. Those functions are helpful mitigation mechanisms, but such accessible ways are unfortunately not readily available on Linux. This is where exile.h steps in. exile.h unlocks existing Linux mechanisms to facilite isolation of processes from resources. Limiting the scope of what programs can do helps defending the rest of the system when a process gets under attacker's control (when classic mitigations such as ASLR etc. failed). To this end, OpenBSD has the pledge() and unveil() functions available. Those functions are helpful mitigation mechanisms, but such accessible ways are unfortunately not readily available on Linux. This is where exile.h steps in.
Seccomp allows to restrict system calls available to a process and thus decrease the systems attack surface, but it generally is not easy to use. Requiring BPF filter instructions, you generally just can't make use of it right away. exile.h provides an API inspired by pledge(), building on top of seccomp. It also provides an interface to manually restrict the system calls that can be issued. Seccomp allows restricting the system calls available to a process and thus decrease the systems attack surface, but it generally is not easy to use. Requiring BPF filter instructions, you generally just can't make use of it right away. exile.h provides an API inspired by pledge(), building on top of seccomp. It also provides an interface to manually restrict the system calls that can be issued.
Traditional methods employed to restrict file system access, like different uids/gids, chroot, bind-mounts, namespaces etc. may require administrator intervention, are perhaps only suitable Traditional methods employed to restrict file system access, like different uids/gids, chroot, bind-mounts, namespaces etc. may require administrator intervention, are perhaps only suitable
for daemons and not desktop applications, or are generally rather involved. As a positive example, Landlock since 5.13 is a vast improvement to limit file system access of processes. It also greatly simplifies exile.h' implementation of fs isolation. for daemons and not desktop applications, or are generally rather involved. As a positive example, Landlock since 5.13 is a vast improvement to limit file system access of processes. It also greatly simplifies exile.h' implementation of fs isolation.
@ -52,9 +167,8 @@ Way more examples can be given, but we can put it in simple words: A general pur
## What it's not ## What it's not
A way for end users/administrators to restrict processes. In the future, a wrapper binary may be available to achieve this, but it generally aims for developers to bring sandboxing/isolation into their software, like web browsers do. This allows a more fine-grained approach, as the developers A way for end users/administrators to restrict processes. In the future, a wrapper binary may be available to achieve this, but it generally aims for developers to bring sandboxing/isolation into their software. This allows a more fine-grained approach, as the developers are more familiar with their software. Applying restrictions with solutions like AppArmor requires
is more familiar with the software. Applying restrictions with solutions like AppArmor requires them to be present and installed on the system and it's easy to break things this way.
them to be present on the system and it's easy to break things this way.
Therefore, software should ideally be written with sandboxing in mind from the beginning. Therefore, software should ideally be written with sandboxing in mind from the beginning.
@ -77,7 +191,7 @@ TODO:
## Requirements ## Requirements
Kernel >=3.17 Kernel >=3.17
While mostly transparent to users of this API, kernel >= 5.13 is required to take advantage of Landlock and furthermore it depends on distro-provided kernels being reasonable and enabling it by default. In practise, this means that Landlock probably won't be used for now, and exile.h will use a combination of namespaces, bind mounts and chroot as fallbacks. While mostly transparent to users of this API, kernel >= 5.13 is required to take advantage of Landlock. Furthermore, it depends on distro-provided kernels being reasonable and enabling it by default. In practise, this means that Landlock probably won't be used for now, and exile.h will use a combination of namespaces, bind mounts and chroot as fallbacks.
## FAQ ## FAQ
@ -87,13 +201,13 @@ While mostly transparent to users of this API, kernel >= 5.13 is required to tak
No. No.
### It doesn't work on Debian! ### It doesn't work on my Debian version!
You can thank a Debian-specific kernel patch for that. Execute
You can thank a Debian-specific kernel patch for that. In the future,
the library may check against that. Execute
`echo 1 > /proc/sys/kernel/unprivileged_userns_clone` to disable that patch for now. `echo 1 > /proc/sys/kernel/unprivileged_userns_clone` to disable that patch for now.
### Examples Note that newer releases should not cause this problem any longer, as [explained](https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html#linux-user-namespaces) in the Debian release notes.
### Real-world usage
- looqs: https://gitea.quitesimple.org/crtxcr/looqs - looqs: https://gitea.quitesimple.org/crtxcr/looqs
- qswiki: https://gitea.quitesimple.org/crtxcr/qswiki - qswiki: https://gitea.quitesimple.org/crtxcr/qswiki

16
exile.c
查看文件

@ -274,10 +274,12 @@ static struct syscall_vow_map exile_vow_map[] =
{EXILE_SYS(sched_getattr), EXILE_SYSCALL_VOW_SCHED}, {EXILE_SYS(sched_getattr), EXILE_SYSCALL_VOW_SCHED},
{EXILE_SYS(renameat2), EXILE_SYSCALL_VOW_CPATH}, {EXILE_SYS(renameat2), EXILE_SYSCALL_VOW_CPATH},
{EXILE_SYS(getrandom), EXILE_SYSCALL_VOW_STDIO}, {EXILE_SYS(getrandom), EXILE_SYSCALL_VOW_STDIO},
{EXILE_SYS(memfd_create), EXILE_SYSCALL_VOW_STDIO},
{EXILE_SYS(execveat), EXILE_SYSCALL_VOW_EXEC}, {EXILE_SYS(execveat), EXILE_SYSCALL_VOW_EXEC},
{EXILE_SYS(mlock2), EXILE_SYSCALL_VOW_STDIO}, {EXILE_SYS(mlock2), EXILE_SYSCALL_VOW_STDIO},
{EXILE_SYS(copy_file_range), EXILE_SYSCALL_VOW_STDIO}, {EXILE_SYS(copy_file_range), EXILE_SYSCALL_VOW_STDIO},
{EXILE_SYS(statx), EXILE_SYSCALL_VOW_RPATH}, {EXILE_SYS(statx), EXILE_SYSCALL_VOW_RPATH},
{EXILE_SYS(rseq), EXILE_SYSCALL_VOW_THREAD},
{EXILE_SYS(clone3), EXILE_SYSCALL_VOW_CLONE}, {EXILE_SYS(clone3), EXILE_SYSCALL_VOW_CLONE},
{EXILE_SYS(close_range), EXILE_SYSCALL_VOW_STDIO}, {EXILE_SYS(close_range), EXILE_SYSCALL_VOW_STDIO},
{EXILE_SYS(openat2), EXILE_SYSCALL_VOW_RPATH|EXILE_SYSCALL_VOW_WPATH}, {EXILE_SYS(openat2), EXILE_SYSCALL_VOW_RPATH|EXILE_SYSCALL_VOW_WPATH},
@ -430,6 +432,7 @@ int get_vow_argfilter(long syscall, uint64_t vow_promises, struct sock_filter *f
struct exile_syscall_filter ioctl_filter[] = { struct exile_syscall_filter ioctl_filter[] = {
EXILE_SYSCALL_FILTER_LOAD_ARG(1), EXILE_SYSCALL_FILTER_LOAD_ARG(1),
{ EXILE_SYSCALL_VOW_IOCTL, EXILE_BPF_NO_MATCH_SET(TIOCSTI), 1 },
{ EXILE_SYSCALL_VOW_IOCTL, EXILE_BPF_RETURN_MATCHING, 1 }, { EXILE_SYSCALL_VOW_IOCTL, EXILE_BPF_RETURN_MATCHING, 1 },
{ EXILE_SYSCALL_VOW_STDIO, EXILE_BPF_MATCH(FIONREAD), 1}, { EXILE_SYSCALL_VOW_STDIO, EXILE_BPF_MATCH(FIONREAD), 1},
{ EXILE_SYSCALL_VOW_STDIO, EXILE_BPF_MATCH(FIONBIO), 1}, { EXILE_SYSCALL_VOW_STDIO, EXILE_BPF_MATCH(FIONBIO), 1},
@ -643,7 +646,7 @@ int (exile_append_path_policies)(struct exile_policy *exile_policy, unsigned int
int fd = open(path, O_PATH); int fd = open(path, O_PATH);
if(fd == -1) if(fd == -1)
{ {
EXILE_LOG_ERROR("Failed to open the specified path: %s\n", strerror(errno)); EXILE_LOG_ERROR("Failed to open %s: %s\n", path, strerror(errno));
exile_policy->exile_flags |= EXILE_FLAG_ADD_PATH_POLICY_FAIL; exile_policy->exile_flags |= EXILE_FLAG_ADD_PATH_POLICY_FAIL;
return -1; return -1;
} }
@ -851,7 +854,7 @@ static int create_chroot_dirs(const char *chroot_target_path, struct exile_path_
ret = mkpath(path_inside_chroot, 0700, baseisfile); ret = mkpath(path_inside_chroot, 0700, baseisfile);
if(ret < 0) if(ret < 0)
{ {
EXILE_LOG_ERROR("Error creating directory structure while mounting paths to chroot. %s\n", strerror(errno)); EXILE_LOG_ERROR("Error creating directory structure %s while mounting paths to chroot: %s\n", path_inside_chroot, strerror(errno));
free(path_inside_chroot); free(path_inside_chroot);
return ret; return ret;
} }
@ -1208,9 +1211,12 @@ static unsigned int exile_flags_to_landlock(unsigned int flags, int statmode)
result |= LANDLOCK_ACCESS_FS_WRITE_FILE; result |= LANDLOCK_ACCESS_FS_WRITE_FILE;
if(S_ISDIR(statmode)) if(S_ISDIR(statmode))
{ {
result |= LANDLOCK_ACCESS_FS_REMOVE_FILE;
result |= LANDLOCK_ACCESS_FS_MAKE_REG;
result |= LANDLOCK_ACCESS_FS_REMOVE_DIR; result |= LANDLOCK_ACCESS_FS_REMOVE_DIR;
result |= LANDLOCK_ACCESS_FS_REMOVE_FILE;
result |= LANDLOCK_ACCESS_FS_MAKE_DIR;
result |= LANDLOCK_ACCESS_FS_MAKE_FIFO;
result |= LANDLOCK_ACCESS_FS_MAKE_REG;
result |= LANDLOCK_ACCESS_FS_MAKE_SOCK;
result |= LANDLOCK_ACCESS_FS_MAKE_SYM; result |= LANDLOCK_ACCESS_FS_MAKE_SYM;
} }
} }
@ -1347,7 +1353,7 @@ static int check_policy_sanity(struct exile_policy *policy)
{ {
if(path_policy_needs_landlock(path_policy)) if(path_policy_needs_landlock(path_policy))
{ {
EXILE_LOG_ERROR("A path policy needs landlock, but landlock is not available. Fallback not possible\n"); EXILE_LOG_ERROR("A path policy (%s) needs landlock, but landlock is not available. Fallback not possible\n", path_policy->path);
return -1; return -1;
} }
path_policy = path_policy->next; path_policy = path_policy->next;

查看文件

@ -100,6 +100,7 @@ inline int do_clone(int (*clonefn)(void *), void *launcharg)
} }
size_t size = rlimit.rlim_cur; size_t size = rlimit.rlim_cur;
char *stack = (char *) calloc(1, size); char *stack = (char *) calloc(1, size);
char *stackbegin = stack;
if(stack == NULL) if(stack == NULL)
{ {
EXILE_LOG_ERROR("Failed to allocate stack memory for child\n"); EXILE_LOG_ERROR("Failed to allocate stack memory for child\n");
@ -110,6 +111,7 @@ inline int do_clone(int (*clonefn)(void *), void *launcharg)
ret = clone(clonefn, stack, 17 /* SIGCHLD */, launcharg); ret = clone(clonefn, stack, 17 /* SIGCHLD */, launcharg);
int status = 0; int status = 0;
waitpid(ret, &status, __WALL); waitpid(ret, &status, __WALL);
free(stackbegin);
if(WIFEXITED(status)) if(WIFEXITED(status))
{ {
return WEXITSTATUS(status); return WEXITSTATUS(status);
@ -119,7 +121,7 @@ inline int do_clone(int (*clonefn)(void *), void *launcharg)
} }
template<typename T, typename U, typename ... Args> template<typename T, typename U, typename ... Args>
typename std::enable_if_t<std::is_trivially_copyable_v<T>, T> exile_launch(struct exile_policy *policy, U fn, Args && ... args) typename std::enable_if_t<std::is_trivially_copyable_v<T> && !std::is_pointer_v<T>, T> exile_launch(struct exile_policy *policy, U fn, Args && ... args)
{ {
size_t mapsize = sizeof(T); size_t mapsize = sizeof(T);
T * sharedbuf = (T *) mmap(NULL, mapsize , PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0); T * sharedbuf = (T *) mmap(NULL, mapsize , PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
@ -145,7 +147,7 @@ typename std::enable_if_t<std::is_trivially_copyable_v<T>, T> exile_launch(struc
template<typename T, typename U, typename ... Args> template<typename T, typename U, typename ... Args>
typename std::enable_if_t<!std::is_trivially_copyable_v<T> && std::is_copy_constructible_v<T>, T> typename std::enable_if_t<std::is_pointer_v<T> || (!std::is_trivially_copyable_v<T> && std::is_copy_constructible_v<T>), T>
exile_launch(struct exile_policy *policy, const std::function<size_t (const T &, char *, size_t)> &serializer, const std::function<T(const char *, size_t)> &deserializer, U fn, Args && ... args) exile_launch(struct exile_policy *policy, const std::function<size_t (const T &, char *, size_t)> &serializer, const std::function<T(const char *, size_t)> &deserializer, U fn, Args && ... args)
{ {
size_t mapsize = EXILE_MMAP_SIZE; size_t mapsize = EXILE_MMAP_SIZE;