fishbowl.c 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. // This program runs another program in a "fishbowl" set to the
  2. // current working directory. The idea is that the subprocess
  3. // should only be able to edit files in that path or anything
  4. // descended from it. It can read outside the fishbowl but if it
  5. // attempts to create or edit files outside of it that syscall
  6. // is blocked (by switching it to getpid which does nothing).
  7. //
  8. // A malicious program can bypass the fishbowl using threads to
  9. // make a syscall and then swap the path after verification.
  10. // This is not a security tool, it is just to protect against
  11. // user error (e.g. if you run a buggy script that accidentally
  12. // deletes or overwrites a file you wanted).
  13. //
  14. // So it is even less secure than a chroot. This could be fixed
  15. // by implementing it in the kernel itself as a new syscall,
  16. // like OpenBSD pledge.
  17. //
  18. // ~/bowl$ fishbowl `which sh`
  19. // Fishbowl: blocking attempt to write to </dev/tty>.
  20. // ~/bowl$ touch test
  21. // ~/bowl$ touch ~/test
  22. // Fishbowl: blocking attempt to write to </home/goldie/test>.
  23. // touch: cannot touch ‘/home/goldie/test’: Bad file descriptor
  24. // ~/bowl$ exit
  25. // Fishbowl: blocking attempt to write to </home/goldie/.bash_history>.
  26. //
  27. // Related/Different tools:
  28. // * users/groups
  29. // * chroot
  30. // * seccomp
  31. // * linux container
  32. // * ld preload
  33. // * pledge
  34. //
  35. // * <http://dev.exherbo.org/~alip/sydbox/sydbox.html>
  36. // * <https://gitweb.gentoo.org/proj/sandbox.git/tree/README>
  37. // * <https://www.kernel.org/doc/Documentation/prctl/seccomp_filter.txt>
  38. // * <http://chdir.org/~nico/seccomp-nurse/>
  39. // * <https://lwn.net/Articles/347547/>
  40. // * <http://eigenstate.org/notes/seccomp>
  41. // * <http://subuser.org/>
  42. #include <stdlib.h>
  43. #include <stdio.h>
  44. #include <signal.h>
  45. #include <string.h>
  46. #include <unistd.h>
  47. #include <assert.h>
  48. #include <errno.h>
  49. #include <fcntl.h>
  50. #include <limits.h>
  51. #include <sys/ptrace.h>
  52. #include <sys/types.h>
  53. #include <sys/reg.h>
  54. #include <sys/wait.h>
  55. #include <sys/syscall.h>
  56. // SOURCES:
  57. // * man ptrace, execve, waitpid
  58. // * https://blog.nelhage.com/2010/08/write-yourself-an-strace-in-70-lines-of-code/
  59. // * http://alip.github.io/code/ptrace-linux-deny.c
  60. // * http://theantway.com/2013/01/notes-for-playing-with-ptrace-on-64-bits-ubuntu-12-10/
  61. // * https://stackoverflow.com/questions/4414605/how-can-linux-ptrace-be-unsafe-or-contain-a-race-condition (how you can get past it wyth race conditions)
  62. // * http://stackoverflow.com/a/11092828 (reason for kill(getpid(), SIGSTOP); between ptrace and execve)
  63. // * http://blog.rchapman.org/post/36801038863/linux-system-call-table-for-x86-64
  64. // * https://github.com/MerlijnWajer/tracy/
  65. // * https://github.com/Pardus-Linux/catbox/blob/4c5af965cb93a0eacb9d2991df2b5838e9fd0b54/src/core.c#L210
  66. // TODO:
  67. // * is argv, envp necessarily NULL terminated or do we need to do that?
  68. // * other syscalls, writev, access, truncate, rename, mkdir, rmdir, creat, link, unlink?
  69. // * is wait == child good enough or do we have to maintain a list of pids? zombies?
  70. // * can it be made to work recursively?
  71. // * handle signals. we currently eat SIGINT.
  72. // * go over the path handling logic carefully.
  73. // * investigate symlink situation. (what if you point outside the fishbowl?)
  74. // NOTES:
  75. // This is for 64-bit architecture.
  76. // Threads can get past it
  77. int cwd_len;
  78. char cwd[PATH_MAX];
  79. char proc[512];
  80. char path[PATH_MAX];
  81. char real[PATH_MAX];
  82. void child(int argc, char **argv, char **envp);
  83. void parent(pid_t);
  84. void parent_handle_open_syscall(pid_t child);
  85. int main(int argc, char **argv, char **envp) {
  86. pid_t pid;
  87. if(argc <= 1) {
  88. puts("USAGE: ./fishbowl `pwd <program>` <arg> ...");
  89. return EXIT_SUCCESS;
  90. }
  91. getcwd(cwd, PATH_MAX);
  92. cwd_len = strlen(cwd);
  93. pid = fork();
  94. if(pid < 0) {
  95. puts("Error: Could not fork.");
  96. return EXIT_FAILURE;
  97. }
  98. if(pid == 0) {
  99. child(argc-1, argv+1, envp);
  100. }
  101. else {
  102. parent(pid);
  103. }
  104. return EXIT_SUCCESS;
  105. }
  106. void child(int argc, char** argv, char **envp) {
  107. ptrace(PTRACE_TRACEME, 0, NULL, NULL);
  108. kill(getpid(), SIGSTOP);
  109. execve(argv[0], argv, envp);
  110. puts("Error: execve failed in child.");
  111. }
  112. void parent(pid_t child) {
  113. pid_t wait, grandchild;
  114. int status, event;
  115. long syscall;
  116. wait = waitpid(child, &status, 0);
  117. assert(WIFSTOPPED(status));
  118. assert(WSTOPSIG(status) == SIGSTOP);
  119. ptrace(PTRACE_SETOPTIONS, child, NULL, (void*)(PTRACE_O_TRACESYSGOOD|PTRACE_O_TRACEFORK|PTRACE_O_TRACEEXEC));
  120. ptrace(PTRACE_SYSCALL, child, NULL, NULL);
  121. do {
  122. wait = waitpid(-1, &status, 0);
  123. if(wait == -1) {
  124. puts("Error: Waitpid failed killing subprocesses.");
  125. kill(child, SIGKILL);
  126. return;
  127. }
  128. if(WIFEXITED(status)) {
  129. if(wait == child) {
  130. return;
  131. }
  132. else {
  133. continue;
  134. }
  135. }
  136. assert(WIFSTOPPED(status));
  137. if(WSTOPSIG(status) == SIGTRAP|0x80) {
  138. syscall = ptrace(PTRACE_PEEKUSER, wait, 8*ORIG_RAX, NULL);
  139. if(syscall == 2) { // TODO: put in the syscall name
  140. parent_handle_open_syscall(wait);
  141. }
  142. ptrace(PTRACE_SYSCALL, wait, NULL, NULL);
  143. }
  144. else { // TODO: change to an else if
  145. event = (status >> 16) & 0xffff;
  146. assert (event == PTRACE_EVENT_FORK
  147. || event == PTRACE_EVENT_VFORK
  148. || event == PTRACE_EVENT_CLONE);
  149. ptrace(PTRACE_GETEVENTMSG, wait, 0, &grandchild);
  150. ptrace(PTRACE_SETOPTIONS, grandchild, NULL, (void*)(PTRACE_O_TRACESYSGOOD|PTRACE_O_TRACEFORK|PTRACE_O_TRACEEXEC));
  151. ptrace(PTRACE_SYSCALL, grandchild, NULL, NULL);
  152. ptrace(PTRACE_SYSCALL, wait, NULL, NULL);
  153. }
  154. } while(1);
  155. }
  156. // copied from nelhage
  157. // TODO: used a global buffer instead of malloc
  158. char *read_string(pid_t child, unsigned long addr) {
  159. char *val = malloc(PATH_MAX);
  160. int allocated = PATH_MAX;
  161. int read = 0;
  162. unsigned long tmp;
  163. while (1) {
  164. if (read + sizeof tmp > allocated) {
  165. allocated *= 2;
  166. val = realloc(val, allocated);
  167. }
  168. tmp = ptrace(PTRACE_PEEKDATA, child, addr + read);
  169. if(errno != 0) {
  170. val[read] = 0;
  171. break;
  172. }
  173. memcpy(val + read, &tmp, sizeof tmp);
  174. if (memchr(&tmp, 0, sizeof tmp) != NULL)
  175. break;
  176. read += sizeof tmp;
  177. }
  178. return val;
  179. }
  180. void parent_handle_open_syscall(pid_t child) {
  181. long filename_ptr, flags/*, mode*/;
  182. int len;
  183. char *filename;
  184. filename_ptr = ptrace(PTRACE_PEEKUSER, child, 8*RDI, NULL);
  185. flags = ptrace(PTRACE_PEEKUSER, child, 8*RSI, NULL);
  186. //mode = ptrace(PTRACE_PEEKUSER, child, 8*RDX, NULL);
  187. if(flags & O_WRONLY || flags & O_RDWR) {
  188. filename = read_string(child, filename_ptr);
  189. if(filename[0] == '/') {
  190. realpath(filename, real);
  191. }
  192. else {
  193. sprintf(proc, "/proc/%d/cwd", child);
  194. len = readlink(proc, path, PATH_MAX);
  195. path[len] = '/';
  196. len++;
  197. path[len] = 0;
  198. strncpy(path+len, filename, PATH_MAX-len);
  199. realpath(path, real);
  200. }
  201. if(strlen(real) < cwd_len || memcmp(cwd, real, cwd_len)) {
  202. fprintf(stderr, "Fishbowl: blocking attempt to write to <%s>.\n", filename);
  203. ptrace(PTRACE_POKEUSER, child, 8*ORIG_RAX, (void*)39); // TODO: getpid
  204. }
  205. free(filename);
  206. }
  207. }