diff options
Diffstat (limited to 'contrib')
32 files changed, 2920 insertions, 21 deletions
diff --git a/contrib/Makefile b/contrib/Makefile index 4749630..a4b5008 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -1,3 +1,3 @@ .PHONY: all clean all clean: - for x in bug* ; do make -C $$x $@ || exit 1 ; done + for x in bug* ; do $(MAKE) -C $$x $@ || exit 1 ; done diff --git a/contrib/bug216610/.gitignore b/contrib/bug216610/.gitignore new file mode 100644 index 0000000..1478d58 --- /dev/null +++ b/contrib/bug216610/.gitignore @@ -0,0 +1,3 @@ +*~ +arms +Dockerfile diff --git a/contrib/bug216610/Dockerfile b/contrib/bug216610/Dockerfile new file mode 100644 index 0000000..5502b71 --- /dev/null +++ b/contrib/bug216610/Dockerfile @@ -0,0 +1,13 @@ +FROM debian:latest + +# A directory to share files via. +RUN mkdir /shared + +RUN apt-get update +RUN apt-get install -y gcc-arm-linux-gnueabi binutils-arm-linux-gnueabi +RUN apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu + +# create a builder user +RUN echo "builder:x:1000:1000:,,,:/home/builder:/bin/bash" >> /etc/passwd +RUN echo "builder:*:19289:0:99999:7:::" >> /etc/shadow +RUN mkdir -p /home/builder && chown builder.bin /home/builder diff --git a/contrib/bug216610/Makefile b/contrib/bug216610/Makefile new file mode 100644 index 0000000..ce96fb3 --- /dev/null +++ b/contrib/bug216610/Makefile @@ -0,0 +1,30 @@ +topdir=$(shell pwd)/../.. +include ../../Make.Rules + +GOTARGET=$(shell eval $$(go env) ; echo $${GOHOSTOS}_$${GOARCH}) + +all: go/fib + +go/fib: go/main.go go/fibber/fib.go go/fibber/linkage.go go/fibber/fibs_$(GOTARGET).s go/fibber/fib_$(GOTARGET).syso + cd go && CGO_ENABLED=0 go build + +# Build the host native version. +go/fibber/fib_$(GOTARGET).syso go/fibber/linkage.go: c/fib.c ./c/gcc.sh ./package_fns.sh + GCC=gcc ./c/gcc.sh -O3 c/fib.c -c -o go/fibber/fib_$(GOTARGET).syso + ./package_fns.sh fibber go/fibber/fib_$(GOTARGET).syso > go/fibber/linkage.go + +Dockerfile: Makefile ./mkdocker.sh + ./mkdocker.sh > $@ + +# Use this build target (make arms) to extend support to include arm +# and arm64 GOARCH values. +arms: Dockerfile Makefile ./c/gcc.sh ./c/build.sh ./c/fib.c + docker run --rm -v $$PWD/c:/shared:z -h debian -u $$(id -u) -it expt shared/build.sh + mv c/*.syso go/fibber/ + touch arms + +clean: + rm -f *~ arms + rm -f c/*.o c/*~ + rm -f go/fib go/*~ + rm -f go/fibber/*.syso go/fibber/*~ go/fibber/linkage.go diff --git a/contrib/bug216610/README.md b/contrib/bug216610/README.md new file mode 100644 index 0000000..4425715 --- /dev/null +++ b/contrib/bug216610/README.md @@ -0,0 +1,139 @@ +# Linking psx and C code without cgo + +## Overview + +In some embedded situations, there is a desire to compile Go binaries +to include some C code, but not `libc` etc. For a long time, I had +assumed this was not possible, since using `cgo` *requires* `libc` and +`libpthread` linkage. + +This _embedded compilation_ need was referenced in a [bug +filed](https://bugzilla.kernel.org/show_bug.cgi?id=216610) against the +[`"psx"`](https://pkg.go.dev/kernel.org/pub/linux/libs/security/libcap/psx) +package. The bug-filer was seeking an alternative to `CGO_ENABLED=1` +compilation _requiring_ the `cgo` variant of `psx` build. However, the +go `"runtime"` package will always +[`panic()`](https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/runtime/os_linux.go;l=717-720) +if you try this because it needs `libpthread` and `[g]libc` to work. + +In researching that bug report, however, I have learned there is a +trick to combining a non-CGO built binary with compiled C code. I +learned about it from a brief reference in the [Go Programming +Language +Wiki](https://zchee.github.io/golang-wiki/GcToolchainTricks/). + +This present directory evolved from my attempt to understand and +hopefully resolve what was going on as reported in that bug into an +example of this _trick_. I was unable to resolve the problem as +reported because of the aformentioned `panic()` in the Go +runtime. However, I was able to demonstrate embedding C code in a Go +binary _without_ use of cgo. In such a binary, the Go-native version +of `"psx"` is thus achievable. This is what the example in this +present directory demonstrates. + +*Caveat Emptor*: this example is very fragile. The Go team only +supports `cgo` linking against C. That being said, I'd certainly like +to receive bug fixes, etc for this directory if you find you need to +evolve it to make it work for your use case. + +## Content + +In this example we have: + +- Some C code for the functions `fib_init()` and `fib_next()` that +combine to implement a _compute engine_ to determine [Fibonacci +Numbers](https://en.wikipedia.org/wiki/Fibonacci_number). The source +for this is in the sub directory `c/fib.c`. + +- Some Go code, in the directory `go/fibber` that uses this C compiled +compute kernel. + +- `c/gcc.sh` which is a wrapper for `gcc` that adjusts the compilation +to be digestible by Go's (internal) linker (the one that gets invoked +when compiling `CGO_ENABLED=0`. Using `gcc` directly instead of this +wrapper generates an incomplete binary - which miscomputes the +expected answers. See the discussion below for what seems to be going +on. + +- A top level `Makefile` to build it all. + +## Building and running the built binary + +Set things up with: +``` +$ git clone git://git.kernel.org/pub/scm/libs/libcap/libcap.git +$ cd libcap +$ make all +$ cd contrib/bug216610 +$ make clean all +``` +When you run `./go/fib` it should generate the following output: +``` +$ ./go/fib +psx syscall result: PID=<nnnnn> +fib: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ... +$ +``` +Where `<nnnnn>` is the PID of the program at runtime and will be +different each time the program is invoked. + +## Discussion + +The Fibonacci detail of what is going on is mostly uninteresting. The +reason for developing this example was to explore the build issues in +the reported [Bug +216610](https://bugzilla.kernel.org/show_bug.cgi?id=216610). Ultimately, +this example offers an alternative path to building a `nocgo` program +that links to compute kernel of C code. + +The reason we have added the `c/gcc.sh` wrapper for `gcc` is that +we've found the Go linker has a hard time digesting the +cross-sectional `%rip` based data addressing that various optimization +modes of gcc like to use. Specifically, in the x86_64/amd64 +architecture, if a `R_X86_64_PC32` relocation entry made in a `.text` +section refers to an `.rodata.cst8` section in a generated `.syso` +file, the Go linker seems to [replace this reference with a `0` offset +to +`(%rip)`](https://github.com/golang/go/issues/24321#issuecomment-1296084103). What +our wrapper script does is rewrite the generated assembly to store +these data references to the `.text` section. The Go linker has no +problem with this _same section_ relative addressing and is able to +link the resulting objects without problems. + +If you want to cross compile, we have support for 32-bit arm +compilation: what is needed for the Raspberry PI. To get this support, +try: +``` +$ make clean all arms +$ cd go +$ GOARCH=arm CGO_ENABLED=0 go build +``` +The generated `fib` binary runs on a 32-bit Raspberry Pi. + +## Future thoughts + +At present, this example only works on Linux with `x86_64` and `arm` +build architectures. (In go-speak that is `linux_amd64` and +`linux_arm`). This is because I have only provided some bridging +assembly for Go to C calling conventions for those architecture +targets: `./go/fibber/fibs_linux_amd64.s` and +`./go/fibber/fibs_linux_arm.s`. The non-native, `make arms`, cross +compilation requires the `docker` command to be available. + +I intend to implement an `arm64` build, when I have a system on which +to test it. + +**Note** The Fedora system on which I've been developing this has some + SELINUX impediment to naively using the `docker -v ...` bind mount + option. I need the `:z` suffix for bind mounting. I don't know how + common an issue this is. On Fedora, building the arm variants of the + .syso file can be performed as follows: +``` +$ docker run --rm -v $PWD/c:/shared:z -h debian -u $(id -u) -it expt shared/build.sh +``` + +## Reporting bugs + +Please report issues or offer improvements to this example via the +[Fully Capable `libcap`](https://sites.google.com/site/fullycapable/) +website. diff --git a/contrib/bug216610/c/build.sh b/contrib/bug216610/c/build.sh new file mode 100755 index 0000000..7458fb1 --- /dev/null +++ b/contrib/bug216610/c/build.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# +# Builds the following .syso files to the directory containing this script: +# +# fib_linux_arm.syso +# fib_linux_arm64.syso + +cd ${0%/*} +GCC=arm-linux-gnueabi-gcc ./gcc.sh -O3 fib.c -c -o fib_linux_arm.syso +GCC=aarch64-linux-gnu-gcc ./gcc.sh -O3 fib.c -c -o fib_linux_arm64.syso diff --git a/contrib/bug216610/c/fib.c b/contrib/bug216610/c/fib.c new file mode 100644 index 0000000..bd665c7 --- /dev/null +++ b/contrib/bug216610/c/fib.c @@ -0,0 +1,20 @@ +#include <inttypes.h> + +struct state { + uint32_t b, a; +}; + +void fib_init(struct state *s); +void fib_init(struct state *s) +{ + s->a = 0; + s->b = 1; +} + +void fib_next(struct state *s); +void fib_next(struct state *s) +{ + uint32_t next = s->a + s->b; + s->a = s->b; + s->b = next; +} diff --git a/contrib/bug216610/c/gcc.sh b/contrib/bug216610/c/gcc.sh new file mode 100755 index 0000000..33655d6 --- /dev/null +++ b/contrib/bug216610/c/gcc.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# +# The Go linker does not seem to know what to do with relative +# addressing of rodata.* offset from %rip. GCC likes to use this +# addressing mode on this architecture, so we quickly run into +# mis-computation when the relative addressing used in a .syso file of +# symbol located data is resolved to completely the wrong place by the +# Go (internal) linker. +# +# As a workaround for this, we can modify the assembly source code +# generated by GCC to not point at problematic '.rodata.*' sections, +# and place this data in the good old '.text' section where Go's +# linker can make sense of it. +# +# This script exists to generate a '.syso' file from some '*.c' files. +# It works by recognizing the '*.c' command line arguments and +# converting them into fixed-up '*.s' files. It then performs the +# compilation for the collection of the '*.s' files. Upon success, it +# purges the intermediate '*.s' files. +# +# The fragile aspect of this present script is which compiler +# arguments should be used for the compilation from '.c' -> '.s' +# files. What we do is accumulate arguments until we encounter our +# first '*.c' file and use those to perform the '.c' -> '.s' +# compilation. We build up a complete command line for gcc +# substituting '.s' files for '.c' files in the original command +# line. Then with the new command line assembled we invoke gcc with +# those. If that works, we remove all of the intermediate '.s' files. + +GCC="${GCC:=gcc}" +setup=0 +args=() +final=() +ses=() + +for arg in "$@"; do + if [[ "${arg##*.}" = "c" ]]; then + setup=1 + s="${arg%.*}.s" + "${GCC}" "${args[@]}" -S -o "${s}" "${arg}" + sed -i -e 's/.*\.rodata\..*/\t.text/' "${s}" + final+=("${s}") + ses+=("${s}") + else + if [[ $setup -eq 0 ]]; then + args+=("${arg}") + fi + final+=("${arg}") + fi +done + +#echo final: "${final[@]}" +#echo args: "${args[@]}" +#echo ses: "${ses[@]}" + +"${GCC}" "${final[@]}" +if [[ $? -ne 0 ]]; then + echo "failed to compile" + exit 1 +fi +rm -f "${ses[@]}" diff --git a/contrib/bug216610/go/.gitignore b/contrib/bug216610/go/.gitignore new file mode 100644 index 0000000..ae14305 --- /dev/null +++ b/contrib/bug216610/go/.gitignore @@ -0,0 +1,5 @@ +fib +*.syso +main +go.sum +linkage.go diff --git a/contrib/bug216610/go/fibber/fib.go b/contrib/bug216610/go/fibber/fib.go new file mode 100644 index 0000000..49757cd --- /dev/null +++ b/contrib/bug216610/go/fibber/fib.go @@ -0,0 +1,32 @@ +// Package fibber implements a Fibonacci sequence generator using a C +// coded compute kernel (a .syso file). +package fibber + +import ( + "unsafe" +) + +// State is the native Go form of the C.state structure. +type State struct { + B, A uint32 +} + +// cPtr converts State into a C pointer suitable as an argument for +// sysoCaller. +func (s *State) cPtr() unsafe.Pointer { + return unsafe.Pointer(&s.B) +} + +// NewState initializes a Fibonacci Number sequence generator. Upon +// return s.A=0 and s.B=1 are the first two numbers in the sequence. +func NewState() *State { + s := &State{} + syso__fib_init.call(s.cPtr()) + return s +} + +// Next advances the state to the next number in the sequence. Upon +// return, s.B is the most recently calculated value. +func (s *State) Next() { + syso__fib_next.call(s.cPtr()) +} diff --git a/contrib/bug216610/go/fibber/fibs_linux_amd64.s b/contrib/bug216610/go/fibber/fibs_linux_amd64.s new file mode 100644 index 0000000..5992d09 --- /dev/null +++ b/contrib/bug216610/go/fibber/fibs_linux_amd64.s @@ -0,0 +1,21 @@ +// To transition from a Go call to a C function call, we are skating +// on really thin ice... Ceveat Emptor! +// +// Ref: +// https://gitlab.com/x86-psABIs/x86-64-ABI/-/wikis/home +// +// This is not strictly needed, but it makes gdb debugging less +// confusing because spacer ends up being an alias for the TEXT +// section start. +TEXT ·spacer(SB),$0 + RET + +#define RINDEX(n) (8*n) + +// Header to this function wrapper is the last time we can voluntarily +// yield to some other goroutine. +TEXT ·syso(SB),$0-16 + MOVQ cFn+RINDEX(0)(FP), SI + MOVQ state+RINDEX(1)(FP), DI + CALL *SI + RET diff --git a/contrib/bug216610/go/fibber/fibs_linux_arm.s b/contrib/bug216610/go/fibber/fibs_linux_arm.s new file mode 100644 index 0000000..39640a5 --- /dev/null +++ b/contrib/bug216610/go/fibber/fibs_linux_arm.s @@ -0,0 +1,23 @@ +// To transition from a Go call to a C function call, we are skating +// on really thin ice... Ceveat Emptor! +// +// Ref: +// https://stackoverflow.com/questions/261419/what-registers-to-save-in-the-arm-c-calling-convention +// +// This is not strictly needed, but it makes gdb debugging less +// confusing because spacer ends up being an alias for the TEXT +// section start. +TEXT ·spacer(SB),$0 + RET + +#define FINDEX(n) (8*n) + +// Header to this function wrapper is the last time we can voluntarily +// yield to some other goroutine. +// +// Conventions: PC == R15, SP == R13, LR == R14, IP (scratch) = R12 +TEXT ·syso(SB),$0-8 + MOVW cFn+0(FP), R14 + MOVW state+4(FP), R0 + BL (R14) + RET diff --git a/contrib/bug216610/go/go.mod b/contrib/bug216610/go/go.mod new file mode 100644 index 0000000..8531994 --- /dev/null +++ b/contrib/bug216610/go/go.mod @@ -0,0 +1,5 @@ +module fib + +go 1.18 + +require kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 diff --git a/contrib/bug216610/go/main.go b/contrib/bug216610/go/main.go new file mode 100644 index 0000000..65121f6 --- /dev/null +++ b/contrib/bug216610/go/main.go @@ -0,0 +1,29 @@ +// Program fib uses the psx package once, and then prints the first +// ten Fibonacci numbers. +package main + +import ( + "fmt" + "log" + "syscall" + + "fib/fibber" + + "kernel.org/pub/linux/libs/security/libcap/psx" +) + +func main() { + pid, _, err := psx.Syscall3(syscall.SYS_GETPID, 0, 0, 0) + if err != 0 { + log.Fatalf("failed to get PID via psx: %v", err) + } + fmt.Print("psx syscall result: PID=") + fmt.Println(pid) + s := fibber.NewState() + fmt.Print("fib: ", s.A, ", ", s.B) + for i := 0; i < 8; i++ { + s.Next() + fmt.Print(", ", s.B) + } + fmt.Println(", ...") +} diff --git a/contrib/bug216610/mkdocker.sh b/contrib/bug216610/mkdocker.sh new file mode 100755 index 0000000..860c198 --- /dev/null +++ b/contrib/bug216610/mkdocker.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# +# This script generates a Dockerfile to be used for cross-compilation +cat <<EOF +FROM debian:latest + +# A directory to share files via. +RUN mkdir /shared + +RUN apt-get update +RUN apt-get install -y gcc-arm-linux-gnueabi binutils-arm-linux-gnueabi +RUN apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu + +# create a builder user +RUN echo "builder:x:$(id -u):$(id -g):,,,:/home/builder:/bin/bash" >> /etc/passwd +RUN echo "builder:*:19289:0:99999:7:::" >> /etc/shadow +RUN mkdir -p /home/builder && chown builder.bin /home/builder +EOF diff --git a/contrib/bug216610/package_fns.sh b/contrib/bug216610/package_fns.sh new file mode 100755 index 0000000..0f4b91c --- /dev/null +++ b/contrib/bug216610/package_fns.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# +# Generate some Go code to make calling into the C code of the .syso +# file easier. + +package="${1}" +syso="${2}" + +if [[ -z "${syso}" ]]; then + echo "usage: $0 <package> <.....syso>" >&2 + exit 1 +fi + +if [[ "${syso%.syso}" == "${syso}" ]]; then + echo "2nd argument should be a .syso file" >&2 + exit 1 +fi + +cat<<EOF +package ${package} + +import ( + "unsafe" +) + +// syso is how we call, indirectly, into the C-code. +func syso(cFn, state unsafe.Pointer) + +type sysoCaller struct { + ptr unsafe.Pointer +} + +// call calls the syso linked C-function, $sym(). +func (s *sysoCaller) call(data unsafe.Pointer) { + syso(s.ptr, data) +} +EOF + +for sym in $(objdump -x "${syso}" | grep -F 'g F' | awk '{print $6}'); do + cat<<EOF + +//go:linkname _${sym} ${sym} +var _${sym} byte +var syso__${sym} = &sysoCaller{ptr: unsafe.Pointer(&_${sym})} + +EOF +done diff --git a/contrib/bug400591/Makefile b/contrib/bug400591/Makefile index 320610c..bb2e59d 100644 --- a/contrib/bug400591/Makefile +++ b/contrib/bug400591/Makefile @@ -1,8 +1,8 @@ all: bug bug: bug.c ../../libcap Makefile - make -C ../../libcap - cc -g -I../../libcap/include --static -o $@ $< -L../../libcap -lcap + $(MAKE) -C ../../libcap + $(CC) $(CFLAGS) $(CPPFLAGS) -g -I../../libcap/include --static -o $@ $< -L../../libcap -lcap ./bug clean: diff --git a/contrib/capso/.gitignore b/contrib/capso/.gitignore new file mode 100644 index 0000000..222d35d --- /dev/null +++ b/contrib/capso/.gitignore @@ -0,0 +1,2 @@ +capso.so +bind diff --git a/contrib/capso/Makefile b/contrib/capso/Makefile new file mode 100644 index 0000000..70af7f9 --- /dev/null +++ b/contrib/capso/Makefile @@ -0,0 +1,23 @@ +topdir=$(shell pwd)/../.. +include ../../Make.Rules + +# Always build sources this way: +CFLAGS += -fPIC $(CAPSO_DEBUG) + +all: bind + +bind: bind.c capso.so + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ bind.c capso.so -L../../libcap -lcap + +../../libcap/loader.txt: + $(MAKE) -C ../../libcap loader.txt + +capso.o: capso.c capso.h ../../libcap/execable.h ../../libcap/loader.txt + $(CC) $(CFLAGS) $(CPPFLAGS) -DLIBCAP_VERSION=\"libcap-$(VERSION).$(MINOR)\" -DSHARED_LOADER=\"$(shell cat ../../libcap/loader.txt)\" -c capso.c -o $@ + +capso.so: capso.o + $(LD) $(LDFLAGS) -o $@ $< $(LIBCAPLIB) -ldl -Wl,-e,__so_start + sudo setcap cap_net_bind_service=p $@ + +clean: + rm -f bind capso.o capso.so *~ diff --git a/contrib/capso/README.md b/contrib/capso/README.md new file mode 100644 index 0000000..df2e878 --- /dev/null +++ b/contrib/capso/README.md @@ -0,0 +1,21 @@ +# Leveraging file capabilities on shared libraries + +This directory contains an example of a shared library (`capso.so`) +that can be installed with file capabilities. When the library is +linked against an unprivileged program, it includes internal support +for re-invoking itself as a child subprocess to execute a privileged +operation on bahalf of the parent. + +The idea for doing this was evolved from the way `pam_unix.so` is able +to leverage a separate program, and `libcap`'s recently added support +for supporting binary execution of all the `.so` files built by the +package. + +The actual program example `./bind` leverages the +`"cap_net_bind_service=p"` enabled `./capso.so` file to bind to the +privileged port 80. + +A writeup of how to build and explore the behavior of this example is +provided on the `libcap` distribution website: + +https://sites.google.com/site/fullycapable/capable-shared-objects diff --git a/contrib/capso/bind.c b/contrib/capso/bind.c new file mode 100644 index 0000000..609e4e4 --- /dev/null +++ b/contrib/capso/bind.c @@ -0,0 +1,29 @@ +/* + * Unprivileged program that binds to port 80. It does this by + * leveraging a file capable shared library. + */ +#include <stdio.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <unistd.h> + +#include "capso.h" + +int main(int argc, char **argv) { + int f = bind80("127.0.0.1"); + if (f < 0) { + perror("unable to bind to port 80"); + exit(1); + } + if (listen(f, 10) == -1) { + perror("unable to listen to port 80"); + exit(1); + } + printf("Webserver code to use filedes = %d goes here.\n" + "(Sleeping for 60s... Try 'netstat -tlnp|grep :80')\n", f); + fflush(stdout); + sleep(60); + close(f); + printf("Done.\n"); +} diff --git a/contrib/capso/capso.c b/contrib/capso/capso.c new file mode 100644 index 0000000..7ca3427 --- /dev/null +++ b/contrib/capso/capso.c @@ -0,0 +1,368 @@ +/* + * Worked example for a shared object with a file capability on it + * leveraging itself for preprogrammed functionality. + * + * This example implements a shared library that can bind to + * the privileged port. ":80". + * + * The shared library needs to be installed with + * cap_net_bind_service=p. As a shared library, it provides the + * function bind80(). + */ + +#define _GNU_SOURCE + +#include <dlfcn.h> +#include <netdb.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/capability.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/un.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "capso.h" + +extern char **environ; + +/* + * fake_exploit is some dedicated code to simulate a shell escape type + * exploit. This is obviously not something serious to include in code + * that has actually been audited for security, but we use it to + * demonstrate an aspect of file capabilities vs. setuid root for + * granting privilege. + */ +static void fake_exploit(void) { +#ifdef ALLOW_EXPLOIT + const char *exploit = getenv("TRIGGER_EXPLOIT"); + if (exploit == NULL) { + return; + } + + switch (*exploit) { + case '^': + case '%': + exploit++; + cap_value_t caps = CAP_NET_BIND_SERVICE; + cap_t c = cap_get_proc(); + cap_set_flag(c, CAP_INHERITABLE, 1, &caps, CAP_SET); + if (cap_set_proc(c)) { + perror("Failed to raise inheritable capability"); + exit(1); + } + if (*(exploit-1) == '%') { + break; + } + cap_free(c); + if (cap_set_ambient(caps, CAP_SET) != 0) { + perror("Unable to raise ambient capability"); + exit(1); + } + break; + } + + char *ts = strdup(exploit); + if (ts == NULL) { + perror("Failed to duplicate exploit string"); + exit(1); + } + + int i, j, n = 1; + for (i = 0; ts[i]; i++) { + switch (ts[i]) { + case ' ': + case '\t': + n++; + ts[i] = '\0'; + } + } + char **argv = calloc(n, sizeof(char *)); + for (i = 0, j = 0; j < n; j++) { + char *s = ts+i; + argv[j] = s; + i += 1 + strlen(s); + printf("execv argv[%d] = \"%s\"\n", j, s); + } + + execv(argv[0], argv); + perror("Execv failed"); + exit(1); +#endif /* def ALLOW_EXPLOIT */ +} + +/* + * where_am_i determines the full path for the shared libary that + * contains this function. It allocates the path in strdup()d memory + * that should be free()d by the caller. If it can't find itself, it + * returns NULL. + */ +static char *where_am_i(void) +{ + Dl_info info; + if (dladdr(where_am_i, &info) == 0) { + return NULL; + } + return strdup(info.dli_fname); +} + +/* + * try_bind80 attempts to reuseably bind to port 80 with the given + * hostname. It returns a bound filedescriptor or -1 on error. + */ +static int try_bind80(const char *hostname) +{ + struct addrinfo *conf, *detail = NULL; + int err, ret = -1, one = 1; + + conf = calloc(1, sizeof(*conf)); + if (conf == NULL) { + return -1; + } + + conf->ai_family = PF_UNSPEC; + conf->ai_socktype = SOCK_STREAM; + conf->ai_protocol = 0; + conf->ai_flags = AI_PASSIVE | AI_ADDRCONFIG; + + err = getaddrinfo(hostname, "80", conf, &detail); + if (err != 0) { + goto done; + } + + ret = socket(detail->ai_family, detail->ai_socktype, detail->ai_protocol); + if (ret == -1) { + goto done; + } + + if (setsockopt(ret, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one))) { + close(ret); + ret = -1; + goto done; + } + + if (bind(ret, detail->ai_addr, detail->ai_addrlen)) { + close(ret); + ret = -1; + goto done; + } + + done: + if (detail != NULL) { + freeaddrinfo(detail); + } + free(conf); + + return ret; +} + +/* + * set_fd3 forces file descriptor 3 to be associated with a unix + * socket that can be used to send a file descriptor back to the + * parent program. + */ +static int set_fd3(void *detail) +{ + int *sp = detail; + + close(sp[0]); + if (dup2(sp[1], 3) != 3) { + return -1; + } + close(sp[1]); + + return 0; +} + +/* + * bind80 returns a socket filedescriptor that is bound to port 80 of + * the provided service address. + * + * Example: + * + * int fd = bind80("localhost"); + * + * fd < 0 in the case of error. + */ +int bind80(const char *hostname) +{ + cap_launch_t helper; + pid_t child; + char const *args[3]; + char *path; + int fd, ignored; + int sp[2]; + char junk[1]; + const int rec_buf_len = CMSG_SPACE(sizeof(int)); + char *rec_buf[CMSG_SPACE(sizeof(int))]; + struct iovec *iov; + struct msghdr *msg; + + fd = try_bind80(hostname); + if (fd >= 0) { + return fd; + } + +#ifdef CAPSO_DEBUG + printf("application bind80(%s) attempt failed\n", hostname); + sleep(30); +#endif + + iov = calloc(1, sizeof(struct iovec)); + if (iov == NULL) { + return -1; + } + msg = calloc(1, sizeof(struct msghdr)); + if (msg == NULL) { + free(iov); + return -1; + } + + /* + * Initial attempt didn't work, so try launching the shared + * library as an executable and getting it to yield a bound + * filedescriptor for us via a unix socket pair. + */ + path = where_am_i(); + if (path == NULL) { + perror("Unable to find self"); + goto drop_alloc; + } + + args[0] = "bind80-helper"; + args[1] = hostname; + args[2] = NULL; + + helper = cap_new_launcher(path, args, (void *) environ); + if (helper == NULL) { + goto drop_path; + } + + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sp)) { + goto drop_helper; + } + + cap_launcher_callback(helper, set_fd3); + child = cap_launch(helper, sp); + close(sp[1]); + + if (child <= 0) { + goto drop_sp; + } + + iov[0].iov_base = junk; + iov[0].iov_len = 1; + + msg->msg_name = NULL; + msg->msg_namelen = 0; + msg->msg_iov = iov; + msg->msg_iovlen = 1; + msg->msg_control = rec_buf; + msg->msg_controllen = rec_buf_len; + + if (recvmsg(sp[0], msg, 0) != -1) { + fd = * (int *) CMSG_DATA(CMSG_FIRSTHDR(msg)); + } + waitpid(child, &ignored, 0); + + drop_sp: + close(sp[0]); + + drop_helper: + cap_free(helper); + + drop_path: + free(path); + + drop_alloc: + free(msg); + free(iov); + + return fd; +} + +#include "../../libcap/execable.h" +//#define SO_MAIN int main + +SO_MAIN(int argc, char **argv) +{ + const char *cmd = "<capso.so>"; + const cap_value_t cap_net_bind_service = CAP_NET_BIND_SERVICE; + cap_t working; + int fd; + struct msghdr msg; + struct cmsghdr *ctrl; + struct iovec payload; + char data[CMSG_SPACE(sizeof(fd))]; + char junk[1]; + +#ifdef CAPSO_DEBUG + printf("invoking %s standalone\n", argv[0]); + sleep(30); +#endif + + if (argv != NULL) { + cmd = argv[0]; + } + + if (argc != 2 || argv[1] == NULL || !strcmp(argv[1], "--help")) { + fprintf(stderr, "usage: %s <hostname>\n", cmd); + exit(1); + } + + working = cap_get_proc(); + if (working == NULL) { + perror("Unable to read capabilities"); + exit(1); + } + + if (cap_set_flag(working, CAP_EFFECTIVE, 1, + &cap_net_bind_service, CAP_SET) != 0) { + perror("Unable to raise CAP_NET_BIND_SERVICE"); + exit(1); + } + + if (cap_set_proc(working) != 0) { + perror("Problem with cap_set_proc"); + fprintf(stderr, "Try: sudo setcap cap_net_bind_service=p %s\n", + argv[0]); + exit(1); + } + + fd = try_bind80(argv[1]); + + memset(data, 0, sizeof(data)); + memset(&payload, 0, sizeof(payload)); + + payload.iov_base = junk; + payload.iov_len = 1; + + msg.msg_name = NULL; + msg.msg_namelen = 0; + msg.msg_iov = &payload; + msg.msg_iovlen = 1; + msg.msg_control = data; + msg.msg_controllen = sizeof(data); + + ctrl = CMSG_FIRSTHDR(&msg); + ctrl->cmsg_level = SOL_SOCKET; + ctrl->cmsg_type = SCM_RIGHTS; + ctrl->cmsg_len = CMSG_LEN(sizeof(fd)); + + *((int *) CMSG_DATA(ctrl)) = fd; + + if (sendmsg(3, &msg, 0) < 0) { + perror("Failed to write fd"); + } + + fake_exploit(); + +#ifdef CAPSO_DEBUG + printf("exiting standalone %s\n", argv[0]); + sleep(30); +#endif + + exit(0); +} diff --git a/contrib/capso/capso.h b/contrib/capso/capso.h new file mode 100644 index 0000000..ae18f3a --- /dev/null +++ b/contrib/capso/capso.h @@ -0,0 +1,16 @@ +#ifndef CAPSO_H +#define CAPSO_H + +/* + * bind80 returns a socket filedescriptor that is bound to port 80 of + * the provided service address. + * + * Example: + * + * int fd = bind80("localhost"); + * + * fd < 0 in the case of error. + */ +extern int bind80(const char *hostname); + +#endif /* ndef CAPSO_H */ diff --git a/contrib/pcaps4convenience b/contrib/pcaps4convenience index c46735d..b78a25b 100644 --- a/contrib/pcaps4convenience +++ b/contrib/pcaps4convenience @@ -63,22 +63,22 @@ p4c_test(){ # are we sane? WICH=`which which 2>/dev/null` if [ $WICH == "" ]; then - # thats bad + # that's bad echo "Sorry, I haven't found which" exit fi - # we needt his apps + # we need this app SETCAP=`which setcap 2>/dev/null` if [ "$SETCAP" == "" ]; then - echo "Sorry, I'm missing setcap !" + echo "Sorry, I'm missing setcap!" exit fi - # checking setcap for SET_SETFCAP PCap ? + # checking setcap for SET_SETFCAP PCap? # for now we stick to root if [ "$( id -u )" != "0" ]; then - echo "Sorry, you must be root !" + echo "Sorry, you must be root!" exit 1 fi } @@ -113,7 +113,7 @@ p4c_app_convert(){ p4c_app_revert(){ - # revert a singel app + # revert a single app # $1 is app name APP=`which -a $1 2>/dev/null` if [ "$APP" != "" ]; then @@ -136,7 +136,7 @@ p4c_app_revert(){ p4c_convert(){ - # we go throug the APPSARRAY and call s2p_app_convert to do the job + # we go through the APPSARRAY and call s2p_app_convert to do the job COUNTER=0 let UPPER=${#APPSARRAY[*]}-1 until [ $COUNTER == $UPPER ]; do @@ -170,9 +170,9 @@ p4c_usage(){ echo "through the PAM module pam_cap.so." echo "A user who has not the needed PCaps in his Inheritance Set CAN NOT execute" echo "these binaries successful." - echo "(well, still per sudo or su -c - but thats not the point here)" + echo "(well, still per sudo or su -c - but that's not the point here)" echo - echo "You need and I will check fot the utilities which and setcap." + echo "You need and I will check for the utilities which and setcap." echo echo "Your Filesystem has to support extended attributes and your kernel must have" echo "support for POSIX File Capabilities (CONFIG_SECURITY_FILE_CAPABILITIES)." diff --git a/contrib/pcaps4server b/contrib/pcaps4server index af6f9ca..f72a4d3 100644 --- a/contrib/pcaps4server +++ b/contrib/pcaps4server @@ -8,7 +8,7 @@ # changelog: # 1 - initial release pcaps4convenience # 1 - 2007.02.15 - initial release -# 2 - 2007.11.02 - changed to new setfcaps api; each app is now callable; supressed error of id +# 2 - 2007.11.02 - changed to new setfcaps api; each app is now callable; suppressed error of id # 3 - 2007.12.28 - changed to libcap2 package setcap/getcap # 4 - renamed to pcaps4server # removed suid0 and convenience files, diff --git a/contrib/pcaps4suid0 b/contrib/pcaps4suid0 index 799df28..2cbdcee 100644 --- a/contrib/pcaps4suid0 +++ b/contrib/pcaps4suid0 @@ -77,23 +77,23 @@ p4s_test(){ # are we sane? WICH=`which which 2>/dev/null` if [ $WICH == "" ]; then - # thats bad + # that's bad echo "Sorry, I haven't found which" exit fi - # we needt his apps + # we need these apps CHMOD=`which chmod 2>/dev/null` SETCAP=`which setcap 2>/dev/null` if [ "$CHMOD" == "" -o "$SETCAP" == "" ]; then - echo "Sorry, I'm missing chmod or setcap !" + echo "Sorry, I'm missing chmod or setcap!" exit fi - # checking setcap for SET_SETFCAP PCap ? + # checking setcap for SET_SETFCAP PCap? # for now we stick to root if [ "$( id -u )" != "0" ]; then - echo "Sorry, you must be root !" + echo "Sorry, you must be root!" exit 1 fi } @@ -129,7 +129,7 @@ p4s_app_convert(){ p4s_app_revert(){ - # revert a singel app + # revert a single app # $1 is app name APP=`which -a $1 2>/dev/null` if [ "$APP" != "" ]; then @@ -153,7 +153,7 @@ p4s_app_revert(){ p4s_convert(){ - # we go throug the APPSARRAY and call s2p_app_convert to do the job + # we go through the APPSARRAY and call s2p_app_convert to do the job COUNTER=0 let UPPER=${#APPSARRAY[*]}-1 until [ $COUNTER == $UPPER ]; do @@ -190,7 +190,7 @@ p4s_usage(){ echo "If you are using pam_cap.so, you might want to change the set into the" echo "Inherited and Effective set (check for the SET var)." echo - echo "You need and I will check fot the utilities which, chmod and setcap." + echo "You need and I will check for the utilities which, chmod and setcap." echo echo "Your Filesystem has to support extended attributes and your kernel must have" echo "support for POSIX File Capabilities (CONFIG_SECURITY_FILE_CAPABILITIES)." diff --git a/contrib/seccomp/explore.go b/contrib/seccomp/explore.go new file mode 100644 index 0000000..8203d4f --- /dev/null +++ b/contrib/seccomp/explore.go @@ -0,0 +1,277 @@ +// Program explore is evolved from the code discussed in more depth +// here: +// +// https://github.com/golang/go/issues/3405 +// +// The code here demonstrates that while PR_SET_NO_NEW_PRIVS only +// applies to the calling thread, since +// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=103502a35cfce0710909da874f092cb44823ca03 +// the seccomp filter application forces the setting to be mirrored on +// all the threads of a process. +// +// Based on the command line options, we can manipulate the program to +// behave in various ways. Example command lines: +// +// sudo ./explore +// sudo ./explore --kill=false +// sudo ./explore --kill=false --errno=0 +// +// Supported Go toolchains are after go1.10. Those prior to go1.15 +// require this environment variable to be set to build successfully: +// +// export CGO_LDFLAGS_ALLOW="-Wl,-?-wrap[=,][^-.@][^,]*" +// +// Go toolchains go1.16+ can be compiled CGO_ENABLED=0 too, +// demonstrating native nocgo support for seccomp features. +package main + +import ( + "flag" + "fmt" + "log" + "runtime" + "syscall" + "time" + "unsafe" + + "kernel.org/pub/linux/libs/security/libcap/psx" +) + +var ( + withPSX = flag.Bool("psx", false, "use the psx mechanism to invoke prctl syscall") + delays = flag.Bool("delays", false, "use this to pause the program at various places") + kill = flag.Bool("kill", true, "kill the process if setuid attempted") + errno = flag.Int("errno", int(syscall.ENOTSUP), "if kill is false, block syscall and return this errno") +) + +const ( + prSetNoNewPrivs = 38 + + sysSeccomp = 317 // x86_64 syscall number + seccompSetModeFilter = 1 // uses user-supplied filter. + seccompFilterFlagTsync = (1 << 0) // mirror filtering on all threads. + seccompRetErrno = 0x00050000 // returns an errno + seccompRetData = 0x0000ffff // mask for RET data payload (ex. errno) + seccompRetKillProcess = 0x80000000 // kill the whole process immediately + seccompRetTrap = 0x00030000 // disallow and force a SIGSYS + seccompRetAllow = 0x7fff0000 + + bpfLd = 0x00 + bpfJmp = 0x05 + bpfRet = 0x06 + + bpfW = 0x00 + + bpfAbs = 0x20 + bpfJeq = 0x10 + + bpfK = 0x00 + + auditArchX86_64 = 3221225534 // HACK: I don't understand this value + archNr = auditArchX86_64 + + syscallNr = 0 +) + +// SockFilter is a single filter block. +type SockFilter struct { + // Code is the filter code instruction. + Code uint16 + // Jt is the target for a true result from the code execution. + Jt uint8 + // Jf is the target for a false result from the code execution. + Jf uint8 + // K is a generic multiuse field + K uint32 +} + +// SockFProg is a +type SockFProg struct { + // Len is the number of contiguous SockFilter blocks that can + // be found at *Filter. + Len uint16 + // Filter is the address of the first SockFilter block of a + // program sequence. + Filter *SockFilter +} + +// SockFilterSlice is a subprogram filter. +type SockFilterSlice []SockFilter + +func bpfStmt(code uint16, k uint32) SockFilter { + return SockFilter{code, 0, 0, k} +} + +func bpfJump(code uint16, k uint32, jt uint8, jf uint8) SockFilter { + return SockFilter{code, jt, jf, k} +} + +func validateArchitecture() []SockFilter { + return []SockFilter{ + bpfStmt(bpfLd+bpfW+bpfAbs, 4), // HACK: I don't understand this 4. + bpfJump(bpfJmp+bpfJeq+bpfK, archNr, 1, 0), + bpfStmt(bpfRet+bpfK, seccompRetKillProcess), + } +} + +func examineSyscall() []SockFilter { + return []SockFilter{ + bpfStmt(bpfLd+bpfW+bpfAbs, syscallNr), + } +} + +func allowSyscall(syscallNum uint32) []SockFilter { + return []SockFilter{ + bpfJump(bpfJmp+bpfJeq+bpfK, syscallNum, 0, 1), + bpfStmt(bpfRet+bpfK, seccompRetAllow), + } +} + +func disallowSyscall(syscallNum, errno uint32) []SockFilter { + return []SockFilter{ + bpfJump(bpfJmp+bpfJeq+bpfK, syscallNum, 0, 1), + bpfStmt(bpfRet+bpfK, seccompRetErrno|(errno&seccompRetData)), + } +} + +func killProcess() []SockFilter { + return []SockFilter{ + bpfStmt(bpfRet+bpfK, seccompRetKillProcess), + } +} + +func notifyProcessAndDie() []SockFilter { + return []SockFilter{ + bpfStmt(bpfRet+bpfK, seccompRetTrap), + } +} + +func trapOnSyscall(syscallNum uint32) []SockFilter { + return []SockFilter{ + bpfJump(bpfJmp+bpfJeq+bpfK, syscallNum, 0, 1), + bpfStmt(bpfRet+bpfK, seccompRetTrap), + } +} + +func allGood() []SockFilter { + return []SockFilter{ + bpfStmt(bpfRet+bpfK, seccompRetAllow), + } +} + +// prctl executes the prctl - unless the --psx commandline argument is +// used, this is on a single thread. +//go:uintptrescapes +func prctl(option, arg1, arg2, arg3, arg4, arg5 uintptr) error { + var e syscall.Errno + if *withPSX { + _, _, e = psx.Syscall6(syscall.SYS_PRCTL, option, arg1, arg2, arg3, arg4, arg5) + } else { + _, _, e = syscall.RawSyscall6(syscall.SYS_PRCTL, option, arg1, arg2, arg3, arg4, arg5) + } + if e != 0 { + return e + } + if *delays { + fmt.Println("prctl'd - check now") + time.Sleep(1 * time.Minute) + } + return nil +} + +// SeccompSetModeFilter is our wrapper for performing our seccomp system call. +//go:uintptrescapes +func SeccompSetModeFilter(prog *SockFProg) error { + if _, _, e := syscall.RawSyscall(sysSeccomp, seccompSetModeFilter, seccompFilterFlagTsync, uintptr(unsafe.Pointer(prog))); e != 0 { + return e + } + return nil +} + +var empty func() + +func lockProcessThread(pick bool) { + // Make sure we are + pid := uintptr(syscall.Getpid()) + runtime.LockOSThread() + for { + tid, _, _ := syscall.RawSyscall(syscall.SYS_GETTID, 0, 0, 0) + if (tid == pid) == pick { + fmt.Println("validated TID:", tid, "== PID:", pid, "is", pick) + break + } + runtime.UnlockOSThread() + go func() { + time.Sleep(1 * time.Microsecond) + }() + runtime.Gosched() + runtime.LockOSThread() + } +} + +// applyPolicy uploads the program sequence. +func applyPolicy(prog *SockFProg) { + // Without PSX we can't guarantee the thread we execute the + // seccomp call on will be the same one that we disabled new + // privs on. With PSX, the disabling of new privs is mirrored + // on all threads. + if !*withPSX { + lockProcessThread(false) + defer runtime.UnlockOSThread() + } + + // This is required to load a filter without privilege. + if err := prctl(prSetNoNewPrivs, 1, 0, 0, 0, 0); err != nil { + log.Fatalf("Prctl(PR_SET_NO_NEW_PRIVS): %v", err) + } + + fmt.Println("Applying syscall policy...") + if err := SeccompSetModeFilter(prog); err != nil { + log.Fatalf("seccomp_set_mode_filter: %v", err) + } + fmt.Println("...Policy applied") +} + +func main() { + flag.Parse() + + if *delays { + fmt.Println("check first", syscall.Getpid()) + time.Sleep(60 * time.Second) + } + + var filter []SockFilter + filter = append(filter, validateArchitecture()...) + + // Grab the system call number. + filter = append(filter, examineSyscall()...) + + // List disallowed syscalls. + for _, x := range []uint32{ + syscall.SYS_SETUID, + } { + if *kill { + filter = append(filter, trapOnSyscall(x)...) + } else { + filter = append(filter, disallowSyscall(x, uint32(*errno))...) + } + } + + filter = append(filter, allGood()...) + + prog := &SockFProg{ + Len: uint16(len(filter)), + Filter: &filter[0], + } + + applyPolicy(prog) + + // Ensure we are running on the TID=PID. + lockProcessThread(true) + + log.Print("Now it is time to try to run something privileged...") + if _, _, e := syscall.RawSyscall(syscall.SYS_SETUID, 1, 0, 0); e != 0 { + log.Fatalf("setuid failed with an error: %v", e) + } + log.Print("Looked like that worked, but it really didn't: uid == ", syscall.Getuid(), " != 1") +} diff --git a/contrib/seccomp/go.mod b/contrib/seccomp/go.mod new file mode 100644 index 0000000..ecf18d9 --- /dev/null +++ b/contrib/seccomp/go.mod @@ -0,0 +1,5 @@ +module explore + +go 1.14 + +require kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 diff --git a/contrib/sucap/Makefile b/contrib/sucap/Makefile new file mode 100644 index 0000000..df61ed5 --- /dev/null +++ b/contrib/sucap/Makefile @@ -0,0 +1,18 @@ +topdir=$(shell pwd)/../.. +include ../../Make.Rules + +# This line is here to link against the in-tree copy of libcap.so +LINKEXTRA=-Wl,-rpath,$(topdir)/libcap +DEPS=../../libcap/libcap.so + +all: su + +su: su.c $(DEPS) + $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -DPAM_APP_NAME=\"sucap\" $< -o $@ $(LINKEXTRA) -lpam -lpam_misc $(LIBCAPLIB) + # to permit all ambient capabilities, this needs all permitted. + # sudo setcap =p ./su + # to permit all inheritable, as CAP_PURE1E needs, we don't need as much + sudo setcap cap_chown,cap_setgid,cap_setuid,cap_dac_read_search,cap_setpcap=p ./su + +clean: + rm -f su su.o *~ diff --git a/contrib/sucap/README.md b/contrib/sucap/README.md new file mode 100644 index 0000000..5cc0dcc --- /dev/null +++ b/contrib/sucap/README.md @@ -0,0 +1,40 @@ +# A fully capable version of `su` + +This directory contains a port of the `SimplePAMApp` `su` one that can +work in a `PURE1E` `libcap`-_mode_ environment. + +The point of developing this is to better test the full `libcap` +implementation, and to also provide a non-setuid-root worked example +for testing PAM interaction with `libcap` and `pam_cap.so`. The +required expectations for `pam_unix.so` are that it include this +commit: + +https://github.com/linux-pam/linux-pam/pull/373/commits/bf9b1d8ad909634000a7356af2d865a79d3f86f3 + +The original sources for this version of `su` were found here: + +https://kernel.org/pub/linux/libs/pam/pre/applications/SimplePAMApps-0.60.tar.gz + +The `SimplePAMApps` contain the same License as `libcap` (they were +originally started by the same authors!). The credited Authors in the +above tarball were: + +- Andrew [G.] Morgan +- Andrey V. Savochkin +- Alexei V. Galatenko + +The code in this present directory is freely adapted from the above +tar ball and is thus a derived work from that. + +**NOTE** As of the time of writing, this adaptation is likely rife + with bugs. + +Finally, Andrew would like to apologize to Andrey for removing all of +the config support he worked to add all those decades ago..! I just +wanted to make a quick tester for a potential workaround for this +`pam_cap.so` issue: + +- https://bugzilla.kernel.org/show_bug.cgi?id=212945 + +Andrew G. Morgan <morgan@kernel.org> +2021-06-30 diff --git a/contrib/sucap/su.c b/contrib/sucap/su.c new file mode 100644 index 0000000..e3dfe70 --- /dev/null +++ b/contrib/sucap/su.c @@ -0,0 +1,1638 @@ +/* + * Originally based on an implementation of `su' by + * + * Peter Orbaek <poe@daimi.aau.dk> + * + * obtained circa 1997 from ftp://ftp.daimi.aau.dk/pub/linux/poe/ + * + * Rewritten for Linux-PAM by Andrew G. Morgan <morgan@linux.kernel.org> + * Modified by Andrey V. Savochkin <saw@msu.ru> + * Modified for use with libcap by Andrew G. Morgan <morgan@kernel.org> + */ + +/* #define PAM_DEBUG */ + +#include <sys/prctl.h> + +/* non-root user of convenience to block signals */ +#define TEMP_UID 1 + +#ifndef PAM_APP_NAME +#define PAM_APP_NAME "su" +#endif /* ndef PAM_APP_NAME */ + +#define DEFAULT_HOME "/" +#define DEFAULT_SHELL "/bin/bash" +#define SLEEP_TO_KILL_CHILDREN 3 /* seconds to wait after SIGTERM before + SIGKILL */ +#define SU_FAIL_DELAY 2000000 /* usec on authentication failure */ + +#define RHOST_UNKNOWN_NAME "" /* perhaps "[from.where?]" */ +#define DEVICE_FILE_PREFIX "/dev/" +#define WTMP_LOCK_TIMEOUT 3 /* in seconds */ + +#ifndef UT_IDSIZE +#define UT_IDSIZE 4 /* XXX - this is sizeof(struct utmp.ut_id) */ +#endif + +#include <stdlib.h> +#include <signal.h> +#include <stdio.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> +#include <pwd.h> +#include <grp.h> +#include <string.h> +#include <syslog.h> +#include <errno.h> +#include <fcntl.h> +#include <termios.h> +#include <sys/wait.h> +#include <utmp.h> +#include <ctype.h> +#include <stdarg.h> +#include <netdb.h> +#include <unistd.h> + +#include <security/pam_appl.h> +#include <security/pam_misc.h> +#include <sys/capability.h> + +#include <security/_pam_macros.h> + +/* -------------------------------------------- */ +/* ------ declarations ------------------------ */ +/* -------------------------------------------- */ + +extern char **environ; +static pam_handle_t *pamh = NULL; + +static int wait_for_child_caught=0; +static int need_job_control=0; +static int is_terminal = 0; +static struct termios stored_mode; /* initial terminal mode settings */ +static uid_t terminal_uid = (uid_t) -1; +static uid_t invoked_uid = (uid_t) -1; + +/* -------------------------------------------- */ +/* ------ some local (static) functions ------- */ +/* -------------------------------------------- */ + +/* + * We will attempt to transcribe the following env variables + * independent of whether we keep the whole environment. Others will + * be set elsewhere: either in modules; or after the identity of the + * user is known. + */ + +static const char *posix_env[] = { + "LANG", + "LC_COLLATE", + "LC_CTYPE", + "LC_MONETARY", + "LC_NUMERIC", + "TZ", + NULL +}; + +/* + * make_environment transcribes a selection of environment variables + * from the invoking user. + */ +static int make_environment(int keep_env) +{ + const char *tmpe; + int i; + int retval; + + if (keep_env) { + /* preserve the original environment */ + return pam_misc_paste_env(pamh, (const char * const *)environ); + } + + /* we always transcribe some variables anyway */ + tmpe = getenv("TERM"); + if (tmpe == NULL) { + tmpe = "dumb"; + } + retval = pam_misc_setenv(pamh, "TERM", tmpe, 0); + if (retval == PAM_SUCCESS) { + retval = pam_misc_setenv(pamh, "PATH", "/bin:/usr/bin", 0); + } + if (retval != PAM_SUCCESS) { + tmpe = NULL; + D(("error setting environment variables")); + return retval; + } + + /* also propagate the POSIX specific ones */ + for (i=0; retval == PAM_SUCCESS && posix_env[i]; ++i) { + tmpe = getenv(posix_env[i]); + if (tmpe != NULL) { + retval = pam_misc_setenv(pamh, posix_env[i], tmpe, 0); + } + } + tmpe = NULL; + + return retval; +} + +/* + * checkfds ensures that stdout and stderr filedescriptors are + * defined. If all else fails, it directs them to /dev/null. + */ +static void checkfds(void) +{ + struct stat st; + int fd; + + if (fstat(1, &st) == -1) { + fd = open("/dev/null", O_WRONLY); + if (fd == -1) goto badfds; + if (fd != 1) { + if (dup2(fd, 1) == -1) goto badfds; + if (close(fd) == -1) goto badfds; + } + } + if (fstat(2, &st) == -1) { + fd = open("/dev/null", O_WRONLY); + if (fd == -1) goto badfds; + if (fd != 2) { + if (dup2(fd, 2) == -1) goto badfds; + if (close(fd) == -1) goto badfds; + } + } + + return; + +badfds: + perror("bad filedes"); + exit(1); +} + +/* + * store_terminal_modes captures the current state of the input + * terminal. Calling this at the start of the program, we ensure we + * can restore these default settings when su exits. + */ +static void store_terminal_modes(void) +{ + if (isatty(STDIN_FILENO)) { + is_terminal = 1; + if (tcgetattr(STDIN_FILENO, &stored_mode) != 0) { + fprintf(stderr, PAM_APP_NAME ": couldn't copy terminal mode"); + exit(1); + } + return; + } + fprintf(stderr, PAM_APP_NAME ": must be run from a terminal\n"); + exit(1); +} + +/* + * restore_terminal_modes resets the terminal to the state it was in + * when the program started. + * + * Returns: + * 0 ok + * 1 error + */ +static int restore_terminal_modes(void) +{ + if (is_terminal && tcsetattr(STDIN_FILENO, TCSAFLUSH, &stored_mode) != 0) { + fprintf(stderr, PAM_APP_NAME ": cannot restore terminal mode: %s\n", + strerror(errno)); + return 1; + } else { + return 0; + } +} + +/* ------ unexpected signals ------------------ */ + +struct sigaction old_int_act, old_quit_act, old_tstp_act, old_pipe_act; + +/* + * disable_terminal_signals attempts to make the process resistant to + * being stopped - it helps ensure that the PAM stack can complete + * session and auth failure logging etc. + */ +static void disable_terminal_signals(void) +{ + /* + * Protect the process from dangerous terminal signals. + * The protection is implemented via sigaction() because + * the signals are sent regardless of the process' uid. + */ + struct sigaction act; + + act.sa_handler = SIG_IGN; /* ignore the signal */ + sigemptyset(&act.sa_mask); /* no signal blocking on handler + call needed */ + act.sa_flags = SA_RESTART; /* do not reset after first signal + arriving, restart interrupted + system calls if possible */ + sigaction(SIGINT, &act, &old_int_act); + sigaction(SIGQUIT, &act, &old_quit_act); + /* + * Ignore SIGTSTP signals. Why? attacker could otherwise stop + * a process and a. kill it, or b. wait for the system to + * shutdown - either way, nothing appears in syslogs. + */ + sigaction(SIGTSTP, &act, &old_tstp_act); + /* + * Ignore SIGPIPE. The parent `su' process may print something + * on stderr. Killing of the process would be undesired. + */ + sigaction(SIGPIPE, &act, &old_pipe_act); +} + +static void enable_terminal_signals(void) +{ + sigaction(SIGINT, &old_int_act, NULL); + sigaction(SIGQUIT, &old_quit_act, NULL); + sigaction(SIGTSTP, &old_tstp_act, NULL); + sigaction(SIGPIPE, &old_pipe_act, NULL); +} + +/* ------ terminal ownership ------------------ */ + +/* + * change_terminal_owner changes the ownership of STDIN if needed. + * Returns: + * 0 ok, + * -1 fatal error (continuing is impossible), + * 1 non-fatal error. + * In the case of an error "err_descr" is set to the error message + * and "callname" to the name of the failed call. + */ +static int change_terminal_owner(uid_t uid, int is_login, + const char **callname, const char **err_descr) +{ + /* determine who owns the terminal line */ + if (is_terminal && is_login) { + struct stat stat_buf; + cap_t current, working; + int status; + cap_value_t cchown = CAP_CHOWN; + + if (fstat(STDIN_FILENO, &stat_buf) != 0) { + *callname = "fstat to STDIN"; + *err_descr = strerror(errno); + return -1; + } + + current = cap_get_proc(); + working = cap_dup(current); + cap_set_flag(working, CAP_EFFECTIVE, 1, &cchown, CAP_SET); + status = cap_set_proc(working); + cap_free(working); + + if (status != 0) { + *callname = "capset CHOWN"; + } else if ((status = fchown(STDIN_FILENO, uid, -1)) != 0) { + *callname = "fchown of STDIN"; + } else { + cap_set_proc(current); + } + cap_free(current); + + if (status != 0) { + *err_descr = strerror(errno); + return 1; + } + + terminal_uid = stat_buf.st_uid; + } + return 0; +} + +/* + * restore_terminal_owner changes the terminal owner back to the value + * it had when su was started. + */ +static void restore_terminal_owner(void) +{ + if (terminal_uid != (uid_t) -1) { + cap_t current, working; + int status; + cap_value_t cchown = CAP_CHOWN; + + current = cap_get_proc(); + working = cap_dup(current); + cap_set_flag(working, CAP_EFFECTIVE, 1, &cchown, CAP_SET); + status = cap_set_proc(working); + cap_free(working); + + if (status == 0) { + status = fchown(STDIN_FILENO, terminal_uid, -1); + cap_set_proc(current); + } + cap_free(current); + + if (status != 0) { + openlog(PAM_APP_NAME, LOG_CONS|LOG_PERROR|LOG_PID, LOG_AUTHPRIV); + syslog(LOG_ALERT, "Terminal owner hasn\'t been restored: %s", + strerror(errno)); + closelog(); + } + terminal_uid = (uid_t) -1; + } +} + +/* + * make_process_unkillable changes the uid of the process. TEMP_UID is + * used for this temporary state. + * + * Returns: + * 0 ok, + * -1 fatal error (continue of the work is impossible), + * 1 non-fatal error. + * In the case of an error "err_descr" is set to the error message + * and "callname" to the name of the failed call. + */ +static int make_process_unkillable(const char **callname, + const char **err_descr) +{ + invoked_uid = getuid(); + if (invoked_uid == TEMP_UID) { + /* no change needed */ + return 0; + } + + if (cap_setuid(TEMP_UID) != 0) { + *callname = "setuid"; + *err_descr = strerror(errno); + return -1; + } + return 0; +} + +/* + * make_process_killable restores the invoking uid to the current + * process. + */ +static void make_process_killable(void) +{ + (void) cap_setuid(invoked_uid); +} + +/* ------ command line parser ----------------- */ + +static void usage(int exit_val) +{ + fprintf(stderr,"usage: su [-] [-h] [-c \"command\"] [username]\n"); + exit(exit_val); +} + +/* + * parse_command_line extracts the options from the command line + * arguments. + */ +static void parse_command_line(int argc, char *argv[], int *is_login, + const char **user, const char **command) +{ + int username_present, command_present; + + *is_login = 0; + *user = NULL; + *command = NULL; + username_present = command_present = 0; + + while ( --argc > 0 ) { + const char *token; + + token = *++argv; + if (*token == '-') { + switch (*++token) { + case '\0': /* su as a login shell for the user */ + if (*is_login) + usage(1); + *is_login = 1; + break; + case 'c': + if (command_present) { + usage(1); + } else { /* indicate we are running commands */ + if (*++token != '\0') { + command_present = 1; + *command = token; + } else if (--argc > 0) { + command_present = 1; + *command = *++argv; + } else + usage(1); + } + break; + case 'h': + usage(0); + default: + usage(1); + } + } else { /* must be username */ + if (username_present) { + usage(1); + } + username_present = 1; + *user = *argv; + } + } + + if (!username_present) { + fprintf(stderr, PAM_APP_NAME ": requires a username\n"); + usage(1); + } +} + +/* + * This following contains code that waits for a child process to die. + * It also chooses to intercept a couple of signals that it will + * kindly pass on a SIGTERM to the child ;^). Waiting again for the + * child to exit. If the child resists dying, it will SIGKILL it! + */ + +static void wait_for_child_catch_sig(int ignore) +{ + wait_for_child_caught = 1; +} + +static void prepare_for_job_control(int need_it) +{ + sigset_t ourset; + + (void) sigfillset(&ourset); + if (sigprocmask(SIG_BLOCK, &ourset, NULL) != 0) { + fprintf(stderr,"[trouble blocking signals]\n"); + wait_for_child_caught = 1; + return; + } + need_job_control = need_it; +} + +static int wait_for_child(pid_t child) +{ + int retval, status, exit_code; + sigset_t ourset; + + exit_code = -1; /* no exit code yet, exit codes could be from 0 to 255 */ + if (child == -1) { + return exit_code; + } + + /* + * set up signal handling + */ + + if (!wait_for_child_caught) { + struct sigaction action, defaction; + + action.sa_handler = wait_for_child_catch_sig; + sigemptyset(&action.sa_mask); + action.sa_flags = 0; + + defaction.sa_handler = SIG_DFL; + sigemptyset(&defaction.sa_mask); + defaction.sa_flags = 0; + + sigemptyset(&ourset); + + if ( sigaddset(&ourset, SIGTERM) + || sigaction(SIGTERM, &action, NULL) + || sigaddset(&ourset, SIGHUP) + || sigaction(SIGHUP, &action, NULL) + || sigaddset(&ourset, SIGALRM) /* required by sleep(3) */ + || (need_job_control && sigaddset(&ourset, SIGTSTP)) + || (need_job_control && sigaction(SIGTSTP, &defaction, NULL)) + || (need_job_control && sigaddset(&ourset, SIGTTIN)) + || (need_job_control && sigaction(SIGTTIN, &defaction, NULL)) + || (need_job_control && sigaddset(&ourset, SIGTTOU)) + || (need_job_control && sigaction(SIGTTOU, &defaction, NULL)) + || (need_job_control && sigaddset(&ourset, SIGCONT)) + || (need_job_control && sigaction(SIGCONT, &defaction, NULL)) + || sigprocmask(SIG_UNBLOCK, &ourset, NULL) + ) { + fprintf(stderr,"[trouble setting signal intercept]\n"); + wait_for_child_caught = 1; + } + + /* application should be ready for receiving a SIGTERM/HUP now */ + } + + /* + * This code waits for the process to actually die. If it stops, + * then the parent attempts to mimic the behavior of the + * child.. There is a slight bug in the code when the 'su'd user + * attempts to restart the child independently of the parent -- + * the child dies. + */ + while (!wait_for_child_caught) { + /* parent waits for child */ + if ((retval = waitpid(child, &status, 0)) <= 0) { + if (errno == EINTR) { + continue; /* recovering from a 'fg' */ + } + fprintf(stderr, "[error waiting child: %s]\n", strerror(errno)); + /* + * Break the loop keeping exit_code undefined. + * Do we have a chance for a successful wait() call + * after kill()? (SAW) + */ + wait_for_child_caught = 1; + break; + } else { + /* the child is terminated via exit() or a fatal signal */ + if (WIFEXITED(status)) { + exit_code = WEXITSTATUS(status); + } else { + exit_code = 1; + } + break; + } + } + + if (wait_for_child_caught) { + fprintf(stderr,"\nKilling shell..."); + kill(child, SIGTERM); + } + + /* + * do we need to wait for the child to catch up? + */ + if (wait_for_child_caught) { + sleep(SLEEP_TO_KILL_CHILDREN); + kill(child, SIGKILL); + fprintf(stderr, "killed\n"); + } + + /* + * collect the zombie the shell was killed by ourself + */ + if (exit_code == -1) { + do { + retval = waitpid(child, &status, 0); + } while (retval == -1 && errno == EINTR); + if (retval == -1) { + fprintf(stderr, PAM_APP_NAME ": the final wait failed: %s\n", + strerror(errno)); + } + if (WIFEXITED(status)) { + exit_code = WEXITSTATUS(status); + } else { + exit_code = 1; + } + } + + return exit_code; +} + + +/* + * Next some code that parses the spawned shell command line. + */ + +static const char * const *build_shell_args(const char *pw_shell, int login, + const char *command) +{ + int use_default = 1; /* flag to signal we should use the default shell */ + const char **args=NULL; /* array of PATH+ARGS+NULL pointers */ + + D(("called.")); + if (login) { + command = NULL; /* command always ignored for login */ + } + + if (pw_shell && *pw_shell != '\0') { + char *line; + const char *tmp, *tmpb=NULL; + int arg_no=0,i; + + /* first find the number of arguments */ + D(("non-null shell")); + for (tmp=pw_shell; *tmp; ++arg_no) { + + /* skip leading spaces */ + while (isspace(*tmp)) + ++tmp; + + if (tmpb == NULL) /* mark beginning token */ + tmpb = tmp; + if (*tmp == '\0') /* end of line with no token */ + break; + + /* skip token */ + while (*tmp && !isspace(*tmp)) + ++tmp; + } + + /* + * We disallow shells: + * - without a full specified path; + * - when we are not logging in and the #args != 1 + * (unlikely a simple shell) + */ + + D(("shell so far = %s, arg_no = %d", tmpb, arg_no)); + if (tmpb != NULL && tmpb[0] == '/' /* something (full path) */ + && ( login || arg_no == 1 ) /* login, or single arg shells */ + ) { + + use_default = 0; /* we will use this shell */ + D(("committed to using user's shell")); + if (command) { + arg_no += 2; /* will append "-c" "command" */ + } + + /* allocate an array of pointers long enough */ + + D(("building array of size %d", 2+arg_no)); + args = (const char **) calloc(2+arg_no, sizeof(const char *)); + if (args == NULL) + return NULL; + /* get a string long enough for all the arguments */ + + D(("an array of size %d chars", 2+strlen(tmpb) + + ( command ? 4:0 ))); + line = (char *) malloc(2+strlen(tmpb) + + ( command ? 4:0 )); + if (line == NULL) { + free(args); + return NULL; + } + + /* fill array - tmpb points to start of first non-space char */ + + line[0] = '-'; + strcpy(line+1, tmpb); + + /* append " -c" to line? */ + if (command) { + strcat(line, " -c"); + } + + D(("complete command: %s [+] %s", line, command)); + + tmp = strtok(line, " \t"); + D(("command path=%s", line+1)); + args[0] = line+1; + + if (login) { /* standard procedure for login shell */ + D(("argv[0]=%s", line)); + args[i=1] = line; + } else { /* not a login shell -- for use with su */ + D(("argv[0]=%s", line+1)); + args[i=1] = line+1; + } + + while ((tmp = strtok(NULL, " \t"))) { + D(("adding argument %d: %s",i,tmp)); + args[++i] = tmp; + } + if (command) { + D(("appending command [%s]", command)); + args[++i] = command; + } + D(("terminating args with NULL")); + args[++i] = NULL; + D(("list completed.")); + } + } + + /* should we use the default shell instead of specific one? */ + + if (use_default && !login) { + int last_arg; + + D(("selecting default shell")); + last_arg = command ? 5:3; + + args = (const char **) calloc(last_arg--, sizeof(const char *)); + if (args == NULL) { + return NULL; + } + args[1] = DEFAULT_SHELL; /* mapped to argv[0] (NOT login shell) */ + args[0] = args[1]; /* path to program */ + if (command) { + args[2] = "-c"; /* should perform command and exit */ + args[3] = command; /* the desired command */ + } + args[last_arg] = NULL; /* terminate list of args */ + } + + D(("returning arg list")); + return (const char * const *) args; +} + + +/* ------ abnormal termination ---------------- */ + +static void exit_now(int exit_code, const char *format, ...) +{ + va_list args; + + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); + + if (pamh != NULL) + pam_end(pamh, exit_code ? PAM_ABORT:PAM_SUCCESS); + + /* USER's shell may have completely broken terminal settings + restore the sane(?) initial conditions */ + restore_terminal_modes(); + + exit(exit_code); +} + +/* ------ PAM setup --------------------------- */ + +static struct pam_conv conv = { + misc_conv, /* defined in <pam_misc/libmisc.h> */ + NULL +}; + +static void do_pam_init(const char *user, int is_login) +{ + int retval; + + retval = pam_start(PAM_APP_NAME, user, &conv, &pamh); + if (retval != PAM_SUCCESS) { + /* + * From my point of view failing of pam_start() means that + * pamh isn't a valid handler. Without a handler + * we couldn't call pam_strerror :-( 1998/03/29 (SAW) + */ + fprintf(stderr, PAM_APP_NAME ": pam_start failed with code %d\n", + retval); + exit(1); + } + + /* + * Fill in some blanks + */ + + retval = make_environment(!is_login); + D(("made_environment returned: %s", pam_strerror(pamh, retval))); + + if (retval == PAM_SUCCESS && is_terminal) { + const char *terminal = ttyname(STDIN_FILENO); + if (terminal) { + retval = pam_set_item(pamh, PAM_TTY, (const void *)terminal); + } else { + retval = PAM_PERM_DENIED; /* how did we get here? */ + } + terminal = NULL; + } + + if (retval == PAM_SUCCESS && is_terminal) { + const char *ruser = getlogin(); /* Who is running this program? */ + if (ruser) { + retval = pam_set_item(pamh, PAM_RUSER, (const void *)ruser); + } else { + retval = PAM_PERM_DENIED; /* must be known to system */ + } + ruser = NULL; + } + + if (retval == PAM_SUCCESS) { + retval = pam_set_item(pamh, PAM_RHOST, (const void *)"localhost"); + } + + if (retval != PAM_SUCCESS) { + exit_now(1, PAM_APP_NAME ": problem establishing environment\n"); + } + + /* have to pause on failure. At least this long (doubles..) */ + retval = pam_fail_delay(pamh, SU_FAIL_DELAY); + if (retval != PAM_SUCCESS) { + exit_now(1, PAM_APP_NAME ": problem initializing failure delay\n"); + } +} + +/* + * authenticate_user arranges for the PAM authentication stack to run. + */ +static int authenticate_user(cap_t all, int *retval, const char **place, + const char **err_descr) +{ + *place = "pre-auth cap_set_proc"; + if (cap_set_proc(all)) { + D(("failed to raise all capabilities")); + *err_descr = "cap_set_proc() failed"; + *retval = PAM_SUCCESS; + return 1; + } + + D(("attempt to authenticate user")); + *place = "pam_authenticate"; + *retval = pam_authenticate(pamh, 0); + return (*retval != PAM_SUCCESS); +} + +/* + * user_accounting confirms an authenticated user is permitted service. + */ +static int user_accounting(cap_t all, int *retval, const char **place, + const char **err_descr) { + *place = "user_accounting"; + if (cap_set_proc(all)) { + D(("failed to raise all capabilities")); + *err_descr = "cap_set_proc() failed"; + return 1; + } + *place = "pam_acct_mgmt"; + *retval = pam_acct_mgmt(pamh, 0); + return (*retval != PAM_SUCCESS); +} + +/* + * Find entry for this terminal (if there is one). + * Utmp file should have been opened and rewinded for the call. + * + * XXX: the search should be more or less compatible with libc one. + * The caller expects that pututline with the same arguments + * will replace the found entry. + */ +static const struct utmp *find_utmp_entry(const char *ut_line, + const char *ut_id) +{ + struct utmp *u_tmp_p; + + while ((u_tmp_p = getutent()) != NULL) + if ((u_tmp_p->ut_type == INIT_PROCESS || + u_tmp_p->ut_type == LOGIN_PROCESS || + u_tmp_p->ut_type == USER_PROCESS || + u_tmp_p->ut_type == DEAD_PROCESS) && + !strncmp(u_tmp_p->ut_id, ut_id, UT_IDSIZE) && + !strncmp(u_tmp_p->ut_line, ut_line, UT_LINESIZE)) + break; + + return u_tmp_p; +} + +/* + * Identify the terminal name and the abbreviation we will use. + */ +static void set_terminal_name(const char *terminal, char *ut_line, char *ut_id) +{ + memset(ut_line, 0, UT_LINESIZE); + memset(ut_id, 0, UT_IDSIZE); + + /* set the terminal entry */ + if ( *terminal == '/' ) { /* now deal with filenames */ + int o1, o2; + + o1 = strncmp(DEVICE_FILE_PREFIX, terminal, 5) ? 0 : 5; + if (!strncmp("/dev/tty", terminal, 8)) { + o2 = 8; + } else { + o2 = strlen(terminal) - sizeof(UT_IDSIZE); + if (o2 < 0) + o2 = 0; + } + + strncpy(ut_line, terminal + o1, UT_LINESIZE); + strncpy(ut_id, terminal + o2, UT_IDSIZE); + } else if (strchr(terminal, ':')) { /* deal with X-based session */ + const char *suffix; + + suffix = strrchr(terminal,':'); + strncpy(ut_line, terminal, UT_LINESIZE); + strncpy(ut_id, suffix, UT_IDSIZE); + } else { /* finally deal with weird terminals */ + strncpy(ut_line, terminal, UT_LINESIZE); + ut_id[0] = '?'; + strncpy(ut_id + 1, terminal, UT_IDSIZE - 1); + } +} + +/* + * Append an entry to wtmp. See utmp_open_session for the return convention. + * Be careful: the function uses alarm(). + */ + +#define WWTMP_STATE_BEGINNING 0 +#define WWTMP_STATE_FILE_OPENED 1 +#define WWTMP_STATE_SIGACTION_SET 2 +#define WWTMP_STATE_LOCK_TAKEN 3 + +static int write_wtmp(struct utmp *u_tmp_p, const char **callname, + const char **err_descr) +{ + int w_tmp_fd; + struct flock w_lock; + struct sigaction act1, act2; + int state; + int retval; + + state = WWTMP_STATE_BEGINNING; + retval = 1; + + do { + D(("writing to wtmp")); + w_tmp_fd = open(_PATH_WTMP, O_APPEND|O_WRONLY); + if (w_tmp_fd == -1) { + *callname = "wtmp open"; + *err_descr = strerror(errno); + break; + } + state = WWTMP_STATE_FILE_OPENED; + + /* prepare for blocking operation... */ + act1.sa_handler = SIG_DFL; + sigemptyset(&act1.sa_mask); + act1.sa_flags = 0; + if (sigaction(SIGALRM, &act1, &act2) == -1) { + *callname = "sigaction"; + *err_descr = strerror(errno); + break; + } + alarm(WTMP_LOCK_TIMEOUT); + state = WWTMP_STATE_SIGACTION_SET; + + /* now we try to lock this file-rcord exclusively; non-blocking */ + memset(&w_lock, 0, sizeof(w_lock)); + w_lock.l_type = F_WRLCK; + w_lock.l_whence = SEEK_END; + if (fcntl(w_tmp_fd, F_SETLK, &w_lock) < 0) { + D(("locking %s failed.", _PATH_WTMP)); + *callname = "fcntl(F_SETLK)"; + *err_descr = strerror(errno); + break; + } + alarm(0); + sigaction(SIGALRM, &act2, NULL); + state = WWTMP_STATE_LOCK_TAKEN; + + if (write(w_tmp_fd, u_tmp_p, sizeof(struct utmp)) != -1) { + retval = 0; + } + } while(0); /* it's not a loop! */ + + if (state >= WWTMP_STATE_LOCK_TAKEN) { + w_lock.l_type = F_UNLCK; /* unlock wtmp file */ + fcntl(w_tmp_fd, F_SETLK, &w_lock); + }else if (state >= WWTMP_STATE_SIGACTION_SET) { + alarm(0); + sigaction(SIGALRM, &act2, NULL); + } + + if (state >= WWTMP_STATE_FILE_OPENED) { + close(w_tmp_fd); /* close wtmp file */ + D(("wtmp written")); + } + + return retval; +} + +/* + * XXX - if this gets turned into a module, make this a + * pam_data item. You should put the pid in the name so we can + * "probably" nest calls more safely... + */ +struct utmp *login_stored_utmp=NULL; + +/* + * Returns: + * 0 ok, + * 1 non-fatal error + * -1 fatal error + * callname and err_descr will be set + * Be careful: the function indirectly uses alarm(). + */ +static int utmp_do_open_session(const char *user, const char *terminal, + const char *rhost, pid_t pid, + const char **place, const char **err_descr) +{ + struct utmp u_tmp; + const struct utmp *u_tmp_p; + char ut_line[UT_LINESIZE], ut_id[UT_IDSIZE]; + int retval; + + set_terminal_name(terminal, ut_line, ut_id); + + utmpname(_PATH_UTMP); + setutent(); /* rewind file */ + u_tmp_p = find_utmp_entry(ut_line, ut_id); + + /* reset new entry */ + memset(&u_tmp, 0, sizeof(u_tmp)); /* reset new entry */ + if (u_tmp_p == NULL) { + D(("[NEW utmp]")); + } else { + D(("[OLD utmp]")); + + /* + * here, we make a record of the former entry. If the + * utmp_close_session code is attached to the same process, + * the wtmp will be replaced, otherwise we leave init to pick + * up the pieces. + */ + if (login_stored_utmp == NULL) { + login_stored_utmp = malloc(sizeof(struct utmp)); + if (login_stored_utmp == NULL) { + *place = "malloc"; + *err_descr = "fail"; + endutent(); + return -1; + } + } + memcpy(login_stored_utmp, u_tmp_p, sizeof(struct utmp)); + } + + /* we adjust the entry to reflect the current session */ + { + strncpy(u_tmp.ut_line, ut_line, UT_LINESIZE); + memset(ut_line, 0, UT_LINESIZE); + strncpy(u_tmp.ut_id, ut_id, UT_IDSIZE); + memset(ut_id, 0, UT_IDSIZE); + strncpy(u_tmp.ut_user, user + , sizeof(u_tmp.ut_user)); + strncpy(u_tmp.ut_host, rhost ? rhost : RHOST_UNKNOWN_NAME + , sizeof(u_tmp.ut_host)); + + /* try to fill the host address entry */ + if (rhost != NULL) { + struct hostent *hptr; + + /* XXX: it isn't good to do DNS lookup here... 1998/05/29 SAW */ + hptr = gethostbyname(rhost); + if (hptr != NULL && hptr->h_addr_list) { + memcpy(&u_tmp.ut_addr, hptr->h_addr_list[0] + , sizeof(u_tmp.ut_addr)); + } + } + + /* we fill in the remaining info */ + u_tmp.ut_type = USER_PROCESS; /* a user process starting */ + u_tmp.ut_pid = pid; /* session identifier */ + u_tmp.ut_time = time(NULL); + } + + setutent(); /* rewind file (replace old) */ + pututline(&u_tmp); /* write it to utmp */ + endutent(); /* close the file */ + + retval = write_wtmp(&u_tmp, place, err_descr); /* write to wtmp file */ + memset(&u_tmp, 0, sizeof(u_tmp)); /* reset entry */ + + return retval; +} + +static int utmp_do_close_session(const char *terminal, + const char **place, const char **err_descr) +{ + struct utmp u_tmp; + const struct utmp *u_tmp_p; + char ut_line[UT_LINESIZE], ut_id[UT_IDSIZE]; + + set_terminal_name(terminal, ut_line, ut_id); + + utmpname(_PATH_UTMP); + setutent(); /* rewind file */ + + /* + * if there was a stored entry, return it to the utmp file, else + * if there is a session to close, we close that + */ + if (login_stored_utmp) { + pututline(login_stored_utmp); + + memcpy(&u_tmp, login_stored_utmp, sizeof(u_tmp)); + u_tmp.ut_time = time(NULL); /* a new time to restart */ + + write_wtmp(&u_tmp, place, err_descr); + + memset(login_stored_utmp, 0, sizeof(u_tmp)); /* reset entry */ + free(login_stored_utmp); + } else { + u_tmp_p = find_utmp_entry(ut_line, ut_id); + if (u_tmp_p != NULL) { + memset(&u_tmp, 0, sizeof(u_tmp)); + strncpy(u_tmp.ut_line, ut_line, UT_LINESIZE); + strncpy(u_tmp.ut_id, ut_id, UT_IDSIZE); + memset(&u_tmp.ut_user, 0, sizeof(u_tmp.ut_user)); + memset(&u_tmp.ut_host, 0, sizeof(u_tmp.ut_host)); + u_tmp.ut_addr = 0; + u_tmp.ut_type = DEAD_PROCESS; /* `old' login process */ + u_tmp.ut_pid = 0; + u_tmp.ut_time = time(NULL); + setutent(); /* rewind file (replace old) */ + pututline(&u_tmp); /* mark as dead */ + + write_wtmp(&u_tmp, place, err_descr); + } + } + + /* clean up */ + memset(ut_line, 0, UT_LINESIZE); + memset(ut_id, 0, UT_IDSIZE); + + endutent(); /* close utmp file */ + memset(&u_tmp, 0, sizeof(u_tmp)); /* reset entry */ + + return 0; +} + +/* + * Returns: + * 0 ok, + * 1 non-fatal error + * -1 fatal error + * place and err_descr will be set + * Be careful: the function indirectly uses alarm(). + */ +static int utmp_open_session(pid_t pid, int *retval, + const char **place, const char **err_descr) +{ + const char *user, *terminal, *rhost; + + *retval = pam_get_item(pamh, PAM_USER, (const void **)&user); + if (*retval != PAM_SUCCESS) { + return -1; + } + *retval = pam_get_item(pamh, PAM_TTY, (const void **)&terminal); + if (retval != PAM_SUCCESS) { + return -1; + } + *retval = pam_get_item(pamh, PAM_RHOST, (const void **)&rhost); + if (retval != PAM_SUCCESS) { + rhost = NULL; + } + + return utmp_do_open_session(user, terminal, rhost, pid, place, err_descr); +} + +static int utmp_close_session(const char **place, const char **err_descr) +{ + int retval; + const char *terminal; + + retval = pam_get_item(pamh, PAM_TTY, (const void **)&terminal); + if (retval != PAM_SUCCESS) { + *place = "pam_get_item(PAM_TTY)"; + *err_descr = pam_strerror(pamh, retval); + return -1; + } + + return utmp_do_close_session(terminal, place, err_descr); +} + +/* + * set_credentials raises the process and PAM credentials. + */ +static int set_credentials(cap_t all, int login, + const char **user_p, uid_t *uid_p, + const char **pw_shell, int *retval, + const char **place, const char **err_descr) +{ + const char *user; + char *shell; + cap_value_t csetgid = CAP_SETGID; + cap_t current; + int status; + struct passwd *pw; + uid_t uid; + + D(("get user from pam")); + *place = "set_credentials"; + *retval = pam_get_item(pamh, PAM_USER, (const void **)&user); + if (*retval != PAM_SUCCESS || user == NULL || *user == '\0') { + D(("error identifying user from PAM.")); + *retval = PAM_USER_UNKNOWN; + return 1; + } + *user_p = user; + + /* + * Add the LOGNAME and HOME environment variables. + */ + + pw = getpwnam(user); + if (pw == NULL || (user = x_strdup(pw->pw_name)) == NULL) { + D(("failed to identify user")); + *retval = PAM_USER_UNKNOWN; + return 1; + } + + uid = pw->pw_uid; + if (uid == 0) { + D(("user is superuser: %s", user)); + *retval = PAM_CRED_ERR; + return 1; + } + *uid_p = uid; + + shell = x_strdup(pw->pw_shell); + if (shell == NULL) { + D(("user %s has no shell", user)); + *retval = PAM_CRED_ERR; + return 1; + } + + if (login) { + /* set LOGNAME, HOME */ + if (pam_misc_setenv(pamh, "LOGNAME", user, 0) != PAM_SUCCESS) { + D(("failed to set LOGNAME")); + *retval = PAM_CRED_ERR; + return 1; + } + } + + /* bash requires these be set to the target user values */ + if (pam_misc_setenv(pamh, "HOME", pw->pw_dir, 0) != PAM_SUCCESS) { + D(("failed to set HOME")); + *retval = PAM_CRED_ERR; + return 1; + } + if (pam_misc_setenv(pamh, "USER", user, 0) != PAM_SUCCESS) { + D(("failed to set USER")); + *retval = PAM_CRED_ERR; + return 1; + } + + current = cap_get_proc(); + cap_set_flag(current, CAP_EFFECTIVE, 1, &csetgid, CAP_SET); + status = cap_set_proc(current); + cap_free(current); + if (status != 0) { + *err_descr = "unable to raise CAP_SETGID"; + return 1; + } + + /* initialize groups */ + if (initgroups(pw->pw_name, pw->pw_gid) != 0 || setgid(pw->pw_gid) != 0) { + D(("failed to setgid etc")); + *retval = PAM_PERM_DENIED; + return 1; + } + *pw_shell = shell; + + pw = NULL; /* be tidy */ + + D(("desired uid=%d", uid)); + + /* assume user's identity - but preserve the permitted set */ + if (cap_setuid(uid) != 0) { + D(("failed to setuid: %v", strerror(errno))); + *retval = PAM_PERM_DENIED; + return 1; + } + + /* + * Next, we call the PAM framework to add/enhance the credentials + * of this user [it may change the user's home directory in the + * pam_env, and add supplemental group memberships...]. + */ + D(("setting credentials")); + if (cap_set_proc(all)) { + D(("failed to raise all capabilities")); + *retval = PAM_PERM_DENIED; + return 1; + } + + D(("calling pam_setcred to establish credentials")); + *retval = pam_setcred(pamh, PAM_ESTABLISH_CRED); + + return (*retval != PAM_SUCCESS); +} + +/* + * open_session invokes the open session PAM stack. + */ +static int open_session(cap_t all, int *retval, const char **place, + const char **err_descr) +{ + /* Open the su-session */ + *place = "pam_open_session"; + if (cap_set_proc(all)) { + D(("failed to raise t_caps capabilities")); + *err_descr = "capability setting failed"; + return 1; + } + *retval = pam_open_session(pamh, 0); /* Must take care to close */ + if (*retval != PAM_SUCCESS) { + return 1; + } + return 0; +} + +/* ------ shell invoker ----------------------- */ + +static int launch_callback_fn(void *h) +{ + pam_handle_t *my_pamh = h; + int retval; + + D(("pam_end")); + retval = pam_end(my_pamh, PAM_SUCCESS | PAM_DATA_SILENT); + pamh = NULL; + if (retval != PAM_SUCCESS) { + return -1; + } + + /* + * Restore a signal status: information if the signal is ignored + * is inherited across exec() call. (SAW) + */ + enable_terminal_signals(); + +#ifdef PAM_DEBUG + cap_iab_t iab = cap_iab_get_proc(); + char *text = cap_iab_to_text(iab); + D(("iab = %s", text)); + cap_free(text); + cap_free(iab); + cap_t cap = cap_get_proc(); + text = cap_to_text(cap, NULL); + D(("cap = %s", text)); + cap_free(text); + cap_free(cap); +#endif + + D(("about to launch")); + return 0; +} + +/* Returns PAM_<STATUS>. */ +static int perform_launch_and_cleanup(cap_t all, int is_login, const char *user, + const char *shell, const char *command) +{ + int status; + const char *home; + const char * const * shell_args; + char * const * shell_env; + cap_launch_t launcher; + pid_t child; + cap_iab_t iab; + + /* + * Break up the shell command into a command and arguments + */ + shell_args = build_shell_args(shell, is_login, command); + if (shell_args == NULL) { + D(("failed to compute shell arguments")); + return PAM_SYSTEM_ERR; + } + + home = pam_getenv(pamh, "HOME"); + if ( !home || home[0] == '\0' ) { + fprintf(stderr, "setting home directory for %s to %s\n", + user, DEFAULT_HOME); + home = DEFAULT_HOME; + if (pam_misc_setenv(pamh, "HOME", home, 0) != PAM_SUCCESS) { + D(("unable to set $HOME")); + fprintf(stderr, + "Warning: unable to set HOME environment variable\n"); + } + } + if (is_login) { + if (chdir(home) && chdir(DEFAULT_HOME)) { + D(("failed to change directory")); + return PAM_SYSTEM_ERR; + } + } + + shell_env = pam_getenvlist(pamh); + if (shell_env == NULL) { + D(("failed to obtain environment for child")); + return PAM_SYSTEM_ERR; + } + + iab = cap_iab_get_proc(); + if (iab == NULL) { + D(("failed to read IAB value of process")); + return PAM_SYSTEM_ERR; + } + + launcher = cap_new_launcher(shell_args[0], + (const char * const *) &shell_args[1], + (const char * const *) shell_env); + if (launcher == NULL) { + D(("failed to initialize launcher")); + return PAM_SYSTEM_ERR; + } + cap_launcher_callback(launcher, launch_callback_fn); + + child = cap_launch(launcher, pamh); + cap_free(launcher); + + if (cap_set_proc(all) != 0) { + D(("failed to restore process capabilities")); + return PAM_SYSTEM_ERR; + } + + /* job control is off for login sessions */ + prepare_for_job_control(!is_login && command != NULL); + + if (cap_setuid(TEMP_UID) != 0) { + fprintf(stderr, "[failed to change monitor UID=%d]\n", TEMP_UID); + } + + /* wait for child to terminate */ + status = wait_for_child(child); + if (status != 0) { + D(("shell returned %d", status)); + } + return status; +} + +static void close_session(cap_t all) +{ + int retval; + + D(("session %p closing", pamh)); + if (cap_set_proc(all)) { + fprintf(stderr, "WARNING: could not raise all caps\n"); + } + retval = pam_close_session(pamh, 0); + if (retval != PAM_SUCCESS) { + fprintf(stderr, "WARNING: could not close session\n\t%s\n", + pam_strerror(pamh,retval)); + } +} + +/* -------------------------------------------- */ +/* ------ the application itself -------------- */ +/* -------------------------------------------- */ + +int main(int argc, char *argv[]) +{ + int retcode, is_login, status; + int retval, final_retval; /* PAM_xxx return values */ + const char *command, *shell; + uid_t uid; + const char *place = NULL, *err_descr = NULL; + cap_t all, t_caps; + const char *user; + + all = cap_get_proc(); + cap_fill(all, CAP_EFFECTIVE, CAP_PERMITTED); + cap_clear_flag(all, CAP_INHERITABLE); + + checkfds(); + + /* + * Check whether stdin is a terminal and store terminal modes for later. + */ + store_terminal_modes(); + + /* ---------- parse the argument list and --------- */ + /* ------ initialize the Linux-PAM interface ------ */ + { + parse_command_line(argc, argv, &is_login, &user, &command); + place = "do_pam_init"; + do_pam_init(user, is_login); /* call pam_start and set PAM items */ + user = NULL; /* transient until PAM_USER defined */ + } + + /* + * Turn off terminal signals - this is to be sure that su gets a + * chance to call pam_end() and restore the terminal modes in + * spite of the frustrated user pressing Ctrl-C. + */ + disable_terminal_signals(); + + /* + * Random exits from here are strictly prohibited :-) (SAW) AGM + * achieves this with goto's and a single exit at the end of main. + */ + status = 1; /* fake exit status of a child */ + err_descr = NULL; /* errors haven't happened */ + + if (make_process_unkillable(&place, &err_descr) != 0) { + goto su_exit; + } + + if (authenticate_user(all, &retval, &place, &err_descr) != 0) { + goto auth_exit; + } + + /* + * The user is valid, but should they have access at this + * time? + */ + if (user_accounting(all, &retval, &place, &err_descr) != 0) { + goto auth_exit; + } + + D(("su attempt is confirmed as authorized")); + + if (set_credentials(all, is_login, &user, &uid, &shell, + &retval, &place, &err_descr) != 0) { + D(("failed to set credentials")); + goto auth_exit; + } + + /* + * ... setup terminal, ... + */ + retcode = change_terminal_owner(uid, is_login, &place, &err_descr); + if (retcode > 0) { + fprintf(stderr, PAM_APP_NAME ": %s: %s\n", place, err_descr); + err_descr = NULL; /* forget about the problem */ + } else if (retcode < 0) { + D(("terminal owner to uid=%d change failed", uid)); + goto auth_exit; + } + + /* + * Here the IAB value is fixed and may differ from all's + * Inheritable value. So synthesize what we need to proceed in the + * child, for now, in this current process. + */ + place = "preserving inheritable parts"; + t_caps = cap_get_proc(); + if (t_caps == NULL) { + D(("failed to read capabilities")); + err_descr = "capability read failed"; + goto delete_cred; + } + if (cap_fill(t_caps, CAP_EFFECTIVE, CAP_PERMITTED)) { + D(("failed to fill effective bits")); + err_descr = "capability fill failed"; + goto delete_cred; + } + + /* + * ... make [uw]tmp entries. + */ + if (is_login) { + /* + * Note: we use the parent pid as a session identifier for + * the logging. + */ + retcode = utmp_open_session(getpid(), &retval, &place, &err_descr); + if (retcode > 0) { + fprintf(stderr, PAM_APP_NAME ": %s: %s\n", place, err_descr); + err_descr = NULL; /* forget about this non-critical problem */ + } else if (retcode < 0) { + goto delete_cred; + } + } + +#ifdef PAM_DEBUG + cap_iab_t iab = cap_iab_get_proc(); + char *text = cap_iab_to_text(iab); + D(("pre-session open iab = %s", text)); + cap_free(text); + cap_free(iab); +#endif + + if (open_session(t_caps, &retval, &place, &err_descr) != 0) { + goto utmp_closer; + } + + status = perform_launch_and_cleanup(all, is_login, user, shell, command); + close_session(all); + +utmp_closer: + if (is_login) { + /* do [uw]tmp cleanup */ + retcode = utmp_close_session(&place, &err_descr); + if (retcode) { + fprintf(stderr, PAM_APP_NAME ": %s: %s\n", place, err_descr); + } + } + +delete_cred: + D(("delete credentials")); + if (cap_set_proc(all)) { + D(("failed to raise all capabilities")); + } + retcode = pam_setcred(pamh, PAM_DELETE_CRED); + if (retcode != PAM_SUCCESS) { + fprintf(stderr, "WARNING: could not delete credentials\n\t%s\n", + pam_strerror(pamh, retcode)); + } + + D(("return terminal to local control")); + restore_terminal_owner(); + +auth_exit: + D(("for clean up we restore the launching user")); + make_process_killable(); + + D(("all done - closing down pam")); + if (retval != PAM_SUCCESS) { /* PAM has failed */ + fprintf(stderr, PAM_APP_NAME ": %s\n", pam_strerror(pamh, retval)); + final_retval = PAM_ABORT; + } else if (err_descr != NULL) { /* a system error has happened */ + fprintf(stderr, PAM_APP_NAME ": %s: %s\n", place, err_descr); + final_retval = PAM_ABORT; + } else { + final_retval = PAM_SUCCESS; + } + (void) pam_end(pamh, final_retval); + pamh = NULL; + + if (restore_terminal_modes() != 0 && !status) { + status = 1; + } + +su_exit: + if (status != 0) { + perror(PAM_APP_NAME " failed"); + } + exit(status); /* transparent exit */ +} diff --git a/contrib/sucap/sucap.pamconfig b/contrib/sucap/sucap.pamconfig new file mode 100644 index 0000000..02b70f2 --- /dev/null +++ b/contrib/sucap/sucap.pamconfig @@ -0,0 +1,6 @@ +#%PAM-1.0 +auth required pam_cap.so config=/etc/security/capability.conf +auth required pam_unix.so +account required pam_unix.so +password required pam_unix.so +session required pam_unix.so |