Add test_ptrace program to test ptrace-based intercept support.
This commit is contained in:
1
MANIFEST
1
MANIFEST
@@ -1204,6 +1204,7 @@ src/openbsd.c
|
|||||||
src/parse_args.c
|
src/parse_args.c
|
||||||
src/preload.c
|
src/preload.c
|
||||||
src/preserve_fds.c
|
src/preserve_fds.c
|
||||||
|
src/regress/intercept/test_ptrace.c
|
||||||
src/regress/net_ifs/check_net_ifs.c
|
src/regress/net_ifs/check_net_ifs.c
|
||||||
src/regress/noexec/check_noexec.c
|
src/regress/noexec/check_noexec.c
|
||||||
src/regress/ttyname/check_ttyname.c
|
src/regress/ttyname/check_ttyname.c
|
||||||
|
@@ -161,6 +161,8 @@ CHECK_NOEXEC_OBJS = check_noexec.o exec_common.o exec_preload.o
|
|||||||
|
|
||||||
CHECK_TTYNAME_OBJS = check_ttyname.o ttyname.o
|
CHECK_TTYNAME_OBJS = check_ttyname.o ttyname.o
|
||||||
|
|
||||||
|
TEST_PTRACE_OBJS = test_ptrace.o
|
||||||
|
|
||||||
LIBOBJDIR = $(top_builddir)/@ac_config_libobj_dir@/
|
LIBOBJDIR = $(top_builddir)/@ac_config_libobj_dir@/
|
||||||
|
|
||||||
VERSION = @PACKAGE_VERSION@
|
VERSION = @PACKAGE_VERSION@
|
||||||
@@ -235,6 +237,9 @@ check_noexec: $(CHECK_NOEXEC_OBJS) $(top_builddir)/lib/util/libsudo_util.la sudo
|
|||||||
check_ttyname: $(CHECK_TTYNAME_OBJS) $(top_builddir)/lib/util/libsudo_util.la
|
check_ttyname: $(CHECK_TTYNAME_OBJS) $(top_builddir)/lib/util/libsudo_util.la
|
||||||
$(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_TTYNAME_OBJS) $(TEST_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(TEST_LIBS)
|
$(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CHECK_TTYNAME_OBJS) $(TEST_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(TEST_LIBS)
|
||||||
|
|
||||||
|
test_ptrace: $(TEST_PTRACE_OBJS) $(top_builddir)/lib/util/libsudo_util.la
|
||||||
|
$(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(TEST_PTRACE_OBJS) $(TEST_LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(TEST_LIBS)
|
||||||
|
|
||||||
pre-install:
|
pre-install:
|
||||||
|
|
||||||
install: install-binaries install-rc @INSTALL_INTERCEPT@ @INSTALL_NOEXEC@
|
install: install-binaries install-rc @INSTALL_INTERCEPT@ @INSTALL_NOEXEC@
|
||||||
@@ -966,6 +971,28 @@ tcsetpgrp_nobg.i: $(srcdir)/tcsetpgrp_nobg.c $(incdir)/compat/stdbool.h \
|
|||||||
$(CC) -E -o $@ $(CPPFLAGS) $<
|
$(CC) -E -o $@ $(CPPFLAGS) $<
|
||||||
tcsetpgrp_nobg.plog: tcsetpgrp_nobg.i
|
tcsetpgrp_nobg.plog: tcsetpgrp_nobg.i
|
||||||
rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/tcsetpgrp_nobg.c --i-file $< --output-file $@
|
rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/tcsetpgrp_nobg.c --i-file $< --output-file $@
|
||||||
|
test_ptrace.o: $(srcdir)/regress/intercept/test_ptrace.c \
|
||||||
|
$(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
|
||||||
|
$(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \
|
||||||
|
$(incdir)/sudo_event.h $(incdir)/sudo_fatal.h \
|
||||||
|
$(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \
|
||||||
|
$(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
|
||||||
|
$(srcdir)/exec_intercept.h $(srcdir)/exec_ptrace.c \
|
||||||
|
$(srcdir)/exec_ptrace.h $(srcdir)/sudo.h $(srcdir)/sudo_exec.h \
|
||||||
|
$(top_builddir)/config.h $(top_builddir)/pathnames.h
|
||||||
|
$(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/regress/intercept/test_ptrace.c
|
||||||
|
test_ptrace.i: $(srcdir)/regress/intercept/test_ptrace.c \
|
||||||
|
$(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \
|
||||||
|
$(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \
|
||||||
|
$(incdir)/sudo_event.h $(incdir)/sudo_fatal.h \
|
||||||
|
$(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \
|
||||||
|
$(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
|
||||||
|
$(srcdir)/exec_intercept.h $(srcdir)/exec_ptrace.c \
|
||||||
|
$(srcdir)/exec_ptrace.h $(srcdir)/sudo.h $(srcdir)/sudo_exec.h \
|
||||||
|
$(top_builddir)/config.h $(top_builddir)/pathnames.h
|
||||||
|
$(CC) -E -o $@ $(CPPFLAGS) $<
|
||||||
|
test_ptrace.plog: test_ptrace.i
|
||||||
|
rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/intercept/test_ptrace.c --i-file $< --output-file $@
|
||||||
tgetpass.o: $(srcdir)/tgetpass.c $(incdir)/compat/stdbool.h \
|
tgetpass.o: $(srcdir)/tgetpass.c $(incdir)/compat/stdbool.h \
|
||||||
$(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
|
$(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
|
||||||
$(incdir)/sudo_debug.h $(incdir)/sudo_event.h \
|
$(incdir)/sudo_debug.h $(incdir)/sudo_event.h \
|
||||||
|
@@ -27,6 +27,7 @@ enum intercept_state {
|
|||||||
RECV_CONNECTION,
|
RECV_CONNECTION,
|
||||||
POLICY_ACCEPT,
|
POLICY_ACCEPT,
|
||||||
POLICY_REJECT,
|
POLICY_REJECT,
|
||||||
|
POLICY_TEST,
|
||||||
POLICY_ERROR
|
POLICY_ERROR
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -790,10 +790,13 @@ ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure)
|
|||||||
char *pathname, **argv, **envp, *buf;
|
char *pathname, **argv, **envp, *buf;
|
||||||
int argc, envc, syscallno;
|
int argc, envc, syscallno;
|
||||||
struct sudo_ptrace_regs regs;
|
struct sudo_ptrace_regs regs;
|
||||||
|
bool path_mismatch = false;
|
||||||
|
bool argv_mismatch = false;
|
||||||
char cwd[PATH_MAX];
|
char cwd[PATH_MAX];
|
||||||
unsigned long msg;
|
unsigned long msg;
|
||||||
bool ret = false;
|
bool ret = false;
|
||||||
struct stat sb;
|
struct stat sb;
|
||||||
|
int i;
|
||||||
debug_decl(ptrace_intercept_execve, SUDO_DEBUG_UTIL);
|
debug_decl(ptrace_intercept_execve, SUDO_DEBUG_UTIL);
|
||||||
|
|
||||||
/* Do not check the policy if we are executing the initial command. */
|
/* Do not check the policy if we are executing the initial command. */
|
||||||
@@ -888,21 +891,29 @@ ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure)
|
|||||||
sudo_warnx("%s", U_(closure->errstr));
|
sudo_warnx("%s", U_(closure->errstr));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (closure->state == POLICY_ACCEPT) {
|
switch (closure->state) {
|
||||||
|
case POLICY_TEST:
|
||||||
|
path_mismatch = true;
|
||||||
|
argv_mismatch = true;
|
||||||
|
if (closure->command == NULL)
|
||||||
|
closure->command = pathname;
|
||||||
|
if (closure->run_argv == NULL)
|
||||||
|
closure->run_argv = argv;
|
||||||
|
FALLTHROUGH;
|
||||||
|
case POLICY_ACCEPT:
|
||||||
/*
|
/*
|
||||||
* Update pathname and argv if the policy modified it.
|
* Update pathname and argv if the policy modified it.
|
||||||
* We don't currently ever modify envp.
|
* We don't currently ever modify envp.
|
||||||
*/
|
*/
|
||||||
bool path_mismatch = strcmp(pathname, closure->command) != 0;
|
if (strcmp(pathname, closure->command) != 0)
|
||||||
bool argv_mismatch = false;
|
path_mismatch = true;
|
||||||
int i;
|
|
||||||
|
|
||||||
for (i = 0; closure->run_argv[i] != NULL && argv[i] != NULL; i++) {
|
for (i = 0; closure->run_argv[i] != NULL && argv[i] != NULL; i++) {
|
||||||
if (strcmp(closure->run_argv[i], argv[i]) != 0) {
|
if (strcmp(closure->run_argv[i], argv[i]) != 0) {
|
||||||
argv_mismatch = true;
|
argv_mismatch = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path_mismatch || argv_mismatch) {
|
if (path_mismatch || argv_mismatch) {
|
||||||
/*
|
/*
|
||||||
* Need to rewrite pathname and/or argv.
|
* Need to rewrite pathname and/or argv.
|
||||||
@@ -993,9 +1004,11 @@ ptrace_intercept_execve(pid_t pid, struct intercept_closure *closure)
|
|||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
break;
|
||||||
/* If denied, fake the syscall and set return to EACCES */
|
default:
|
||||||
|
/* If rejected, fake the syscall and set return to EACCES */
|
||||||
ptrace_fail_syscall(pid, ®s, EACCES);
|
ptrace_fail_syscall(pid, ®s, EACCES);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = true;
|
ret = true;
|
||||||
|
215
src/regress/intercept/test_ptrace.c
Normal file
215
src/regress/intercept/test_ptrace.c
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Todd C. Miller <Todd.Miller@sudo.ws>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is an open source non-commercial project. Dear PVS-Studio, please check it.
|
||||||
|
* PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Test program to exercise seccomp(2) and ptrace(2) intercept code.
|
||||||
|
*
|
||||||
|
* Usage: test_ptrace [-v] [command]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Ignore architecture restrictions and define this unilaterally. */
|
||||||
|
#define HAVE_PTRACE_INTERCEPT
|
||||||
|
#include "exec_ptrace.c"
|
||||||
|
|
||||||
|
static sig_atomic_t got_sigchld;
|
||||||
|
static int verbose;
|
||||||
|
int sudo_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER;
|
||||||
|
|
||||||
|
sudo_dso_public int main(int argc, char *argv[]);
|
||||||
|
|
||||||
|
static void
|
||||||
|
handler(int signo)
|
||||||
|
{
|
||||||
|
if (signo == SIGCHLD)
|
||||||
|
got_sigchld = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
intercept_closure_reset(struct intercept_closure *closure)
|
||||||
|
{
|
||||||
|
memset(closure, 0, sizeof(*closure));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
intercept_check_policy(const char *command, int argc, char **argv, int envc,
|
||||||
|
char **envp, const char *runcwd, void *v)
|
||||||
|
{
|
||||||
|
struct intercept_closure *closure = v;
|
||||||
|
struct stat sb1, sb2;
|
||||||
|
bool is_denied;
|
||||||
|
debug_decl(intercept_check_policy, SUDO_DEBUG_EXEC);
|
||||||
|
|
||||||
|
/* Fake policy decisions. */
|
||||||
|
is_denied = stat(command, &sb1) == 0 && stat("/usr/bin/who", &sb2) == 0 &&
|
||||||
|
sb1.st_ino == sb2.st_ino && sb1.st_dev == sb2.st_dev;
|
||||||
|
if (is_denied) {
|
||||||
|
sudo_debug_printf(SUDO_DEBUG_DIAG, "denied %s", command);
|
||||||
|
closure->state = POLICY_REJECT;
|
||||||
|
} else {
|
||||||
|
sudo_debug_printf(SUDO_DEBUG_DIAG, "allowed %s", command);
|
||||||
|
closure->state = POLICY_TEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug_return_bool(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
init_debug_files(struct sudo_conf_debug_file_list *file_list,
|
||||||
|
struct sudo_debug_file *file)
|
||||||
|
{
|
||||||
|
debug_decl(init_debug_files, SUDO_DEBUG_EXEC);
|
||||||
|
|
||||||
|
TAILQ_INIT(file_list);
|
||||||
|
switch (verbose) {
|
||||||
|
case 0:
|
||||||
|
file->debug_flags = NULL;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
file->debug_flags = "exec@diag";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
file->debug_flags = "exec@info";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
file->debug_flags = "exec@debug";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
file->debug_file = "/dev/stderr";
|
||||||
|
TAILQ_INSERT_HEAD(file_list, file, entries);
|
||||||
|
|
||||||
|
debug_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
struct sudo_conf_debug_file_list debug_files;
|
||||||
|
struct sudo_debug_file debug_file;
|
||||||
|
const char *base, *shell = _PATH_SUDO_BSHELL;
|
||||||
|
struct intercept_closure closure = { 0 };
|
||||||
|
sigset_t blocked, empty;
|
||||||
|
struct sigaction sa;
|
||||||
|
pid_t child, pid;
|
||||||
|
int ch, status;
|
||||||
|
debug_decl_vars(main, SUDO_DEBUG_MAIN);
|
||||||
|
|
||||||
|
initprogname(argc > 0 ? argv[0] : "test_ptrace");
|
||||||
|
|
||||||
|
while ((ch = getopt(argc, argv, "v")) != -1) {
|
||||||
|
switch (ch) {
|
||||||
|
case 'v':
|
||||||
|
verbose++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fprintf(stderr, "usage: %s [-v] [command]\n", getprogname());
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
argc -= optind;
|
||||||
|
argv += optind;
|
||||||
|
|
||||||
|
if (argc > 0)
|
||||||
|
shell = argv[0];
|
||||||
|
base = strrchr(shell, '/');
|
||||||
|
base = base ? base + 1 : shell;
|
||||||
|
|
||||||
|
/* Set debug level based on the verbose flag. */
|
||||||
|
init_debug_files(&debug_files, &debug_file);
|
||||||
|
sudo_debug_instance = sudo_debug_register(getprogname(),
|
||||||
|
NULL, NULL, &debug_files, -1);
|
||||||
|
if (sudo_debug_instance == SUDO_DEBUG_INSTANCE_ERROR)
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
|
||||||
|
/* Block SIGCHLD and SIGUSR during critical section. */
|
||||||
|
sigemptyset(&empty);
|
||||||
|
sigemptyset(&blocked);
|
||||||
|
sigaddset(&blocked, SIGCHLD);
|
||||||
|
sigaddset(&blocked, SIGUSR1);
|
||||||
|
sigprocmask(SIG_BLOCK, &blocked, NULL);
|
||||||
|
|
||||||
|
/* Signal handler sets a flag for SIGCHLD, nothing for SIGUSR1. */
|
||||||
|
memset(&sa, 0, sizeof(sa));
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
sa.sa_flags = SA_RESTART;
|
||||||
|
sa.sa_handler = handler;
|
||||||
|
sigaction(SIGCHLD, &sa, NULL);
|
||||||
|
sigaction(SIGUSR1, &sa, NULL);
|
||||||
|
|
||||||
|
/* Fork a shell. */
|
||||||
|
child = fork();
|
||||||
|
switch (child) {
|
||||||
|
case -1:
|
||||||
|
sudo_fatal("fork");
|
||||||
|
case 0:
|
||||||
|
/* child */
|
||||||
|
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1)
|
||||||
|
sudo_fatal("%s", "unable to set no_new_privs bit");
|
||||||
|
if (!set_exec_filter())
|
||||||
|
_exit(EXIT_FAILURE);
|
||||||
|
|
||||||
|
/* Suspend child until tracer seizes control and sends SIGUSR1. */
|
||||||
|
sigsuspend(&empty);
|
||||||
|
execl(shell, base, NULL);
|
||||||
|
sudo_fatal("execl");
|
||||||
|
default:
|
||||||
|
/* Parent attaches to child and allows it to continue. */
|
||||||
|
if (exec_ptrace_seize(child) == -1)
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wait for SIGCHLD. */
|
||||||
|
for (;;) {
|
||||||
|
sigsuspend(&empty);
|
||||||
|
if (!got_sigchld)
|
||||||
|
continue;
|
||||||
|
got_sigchld = 0;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
do {
|
||||||
|
pid = waitpid(-1, &status, __WALL|WNOHANG);
|
||||||
|
} while (pid == -1 && errno == EINTR);
|
||||||
|
if (pid <= 0) {
|
||||||
|
if (pid == -1 && errno != ECHILD)
|
||||||
|
sudo_fatal("waitpid");
|
||||||
|
/* No child to wait for. */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WIFEXITED(status)) {
|
||||||
|
sudo_debug_printf(SUDO_DEBUG_DIAG, "%d: exited %d",
|
||||||
|
pid, WEXITSTATUS(status));
|
||||||
|
if (pid == child)
|
||||||
|
return WEXITSTATUS(status);
|
||||||
|
} else if (WIFSIGNALED(status)) {
|
||||||
|
sudo_debug_printf(SUDO_DEBUG_DIAG, "%d: killed by signal %d",
|
||||||
|
pid, WTERMSIG(status));
|
||||||
|
if (pid == child)
|
||||||
|
return WTERMSIG(status) | 128;
|
||||||
|
} else if (WIFSTOPPED(status)) {
|
||||||
|
/* XXX - verbose info to stderr, not just debug log */
|
||||||
|
exec_ptrace_handled(pid, status, &closure);
|
||||||
|
} else {
|
||||||
|
sudo_fatalx("%d: unknown status 0x%x", pid, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -97,7 +97,9 @@ union sudo_token_un {
|
|||||||
#if defined(_PATH_SUDO_INTERCEPT) && defined(__linux__)
|
#if defined(_PATH_SUDO_INTERCEPT) && defined(__linux__)
|
||||||
# if defined(HAVE_DECL_SECCOMP_SET_MODE_FILTER) && HAVE_DECL_SECCOMP_SET_MODE_FILTER
|
# if defined(HAVE_DECL_SECCOMP_SET_MODE_FILTER) && HAVE_DECL_SECCOMP_SET_MODE_FILTER
|
||||||
# if defined(__amd64__) || defined(__i386__) || defined(__aarch64__)
|
# if defined(__amd64__) || defined(__i386__) || defined(__aarch64__)
|
||||||
|
# ifndef HAVE_PTRACE_INTERCEPT
|
||||||
# define HAVE_PTRACE_INTERCEPT 1
|
# define HAVE_PTRACE_INTERCEPT 1
|
||||||
|
# endif /* HAVE_PTRACE_INTERCEPT */
|
||||||
# endif /* __amd64__ || __i386__ || __aarch64__ */
|
# endif /* __amd64__ || __i386__ || __aarch64__ */
|
||||||
# endif /* HAVE_DECL_SECCOMP_SET_MODE_FILTER */
|
# endif /* HAVE_DECL_SECCOMP_SET_MODE_FILTER */
|
||||||
#endif /* _PATH_SUDO_INTERCEPT && __linux__ */
|
#endif /* _PATH_SUDO_INTERCEPT && __linux__ */
|
||||||
|
Reference in New Issue
Block a user