I want to override the execve() syscall by using LD_PRELOAD and can't figure out why it sometimes works and sometimes doesn't.
Consider this very simple code overriding execve() (I'll keep it complete so you can try it if you like):
#define _GNU_SOURCE
#include <unistd.h>
#include <dlfcn.h>
typedef ssize_t (*execve_func_t)(const char* filename, char* const argv[], char* const envp[]);
static execve_func_t old_execve = NULL;
int execve(const char* filename, char* const argv[], char* const envp[]) {
printf("Running hook\n");
old_execve = dlsym(RTLD_NEXT, "execve");
return old_execve(filename, argv, envp);
}
(compile with: gcc -std=c99 -o exec.so -shared exec.c -Wall -Wfatal-errors -fPIC -g -ldl)
and this very simple test program:
#define _GNU_SOURCE
#include <unistd.h>
#include <dlfcn.h>
int main() {
char* args[] = {"ls", "/usr", NULL};
char* envp[] = {"LD_PRELOAD=/path/to/exec.so", NULL};
execve("/usr/bin/ls", args, envp);
return 0;
}
Now, when I do export LD_PRELOAD=/path/to/exec.so in my shell, I would expect any binary I run to first execute the hook. That is not true already, which confuses me: edit: Ok, this part is clear now. The issue below is still unsolved.
» strace -f -e trace=execve ./test
execve("./test", ["./test"], [/* 58 vars */]) = 0
Running hook
execve("/usr/bin/ls", ["ls", "/usr"], [/* 1 var */]) = 0
arm-none-eabi avr bin games include lib lib32 lib64 libexec local python sbin share src usr x86_64-pc-linux-gnu
+++ exited with 0 +++
As you see, the hook is only run for the second execve, not for the first.
Still unclear:
What confuses me even more however is that in some cases, the code is not preloaded ever, not even for the child processes; for example, when running ls /usr with Python's subprocess module, this happens:
» strace -f -e trace=execve /usr/bin/python -c "import subprocess; subprocess.Popen(['ls', '/usr'])"
execve("/usr/bin/python", ["/usr/bin/python", "-c", "import subprocess; subprocess.Po"...], [/* 58 vars */]) = 0
strace: Process 8350 attached
[pid 8350] execve("/usr/local/sbin/ls", ["ls", "/usr"], [/* 58 vars */]) = -1 ENOENT (No such file or directory)
[pid 8350] execve("/usr/local/bin/ls", ["ls", "/usr"], [/* 58 vars */]) = -1 ENOENT (No such file or directory)
[pid 8350] execve("/usr/bin/ls", ["ls", "/usr"], [/* 58 vars */]) = 0
arm-none-eabi avr bin games include lib lib32 lib64 libexec local python sbin share src usr x86_64-pc-linux-gnu
[pid 8350] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=8350, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++
How is that possible? It's the exact same syscall with the exact same environment for the calling process, but it does something different. I'd be happy about any pointers about this.
Ok, the solution is actually quite simple: Python calls execv, not execve; and the standard output printed is taken up by the code calling the process and not printed to the terminal. That's why it appears to not work (while it actually does).
Related
Let's have a look at this demonstration program
#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(void) {
int fd = openat(AT_FDCWD, "/dev/stdout", O_RDONLY|O_PATH|O_DIRECTORY);
if (fd == -1) {
perror("openat");
return 1;
}
return 0;
}
I've done a ltrace of this program, and here is the important part:
When compiled using a 3.10.0-693 linux kernel the output is
__libc_start_main([ "./a.out" ] <unfinished ...>
openat(0xffffff9c, 0x400670, 0x210000, 0x4005e0) = 3
+++ exited (status 0) +++
When compiled using a 4.14.15-1 linux kernel the output is
__libc_start_main(0x40059d, 1, 0x7ffcaccd9e98, 0x4005e0 <unfinished ...>
openat(0xffffff9c, 0x400670, 0x210000, 0x4005e0) = -1
perror("openat"openat: Not a directory
) = <void>
+++ exited (status 1) +++
So it seems the openat() recognized /dev/stdout as a directory in 3.10.0-693 linux kernel. From a logical point of view this is quiet unexpected.
I could not find this documented anywhere and it feels a bit like an edge case. Can anyone explain what exactly is going on here?
Edit: strace the program on kernel 3.10.0-693 produces:
mprotect(0x7f568f480000, 4096, PROT_READ) = 0
munmap(0x7f568f473000, 47716) = 0
openat(AT_FDCWD, "/dev/stdout", O_RDONLY|O_PATH|O_DIRECTORY) = 3
exit_group(0) = ?
+++ exited with 0 +++
I wrote the following code expecting to spawn a /bin/sh from another user.
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv, char **envp)
{
setresgid(getegid(), getegid(), getegid());
setresuid(geteuid(), geteuid(), geteuid());
execve("/bin/sh", argv, envp);
return 0;
}
I then changed the owner to match with my target user and changed permissions (too much, I know)
chown usertarget:globalgroup ./shell
chmod 777 ./shell
chmod +s ./shell
ls -lah shell
Everything is fine according to me. However, It keeps opening a shell as my current user, not the target one.
I already tried to hardcode the userid of my target user and a few other things (setuid function, ...) but nothing seems to work...
Anyone has an idea or anything that could help me investigate this problem ?
EDIT #1
baseuser#machine:/tmp/tata$ ls -lah shell2
-rwsrwsrwx 1 targetuser globalgroup 7.2K Aug 18 18:21 shell2
baseuser#machine:/tmp/tata$ id
uid=1507(baseuser) gid=1314(globalgroup) groups=1314(globalgroup),100(users)
baseuser#machine:/tmp/tata$ ls -lah shell2
-rwsrwsrwx 1 targetuser globalgroup 7.2K Aug 18 18:21 shell2
baseuser#machine:/tmp/tata$ ./shell2
====== WELCOME USER ======
baseuser#machine:/tmp/tata$ id -a
uid=1507(baseuser) gid=1314(globalgroup) groups=1314(globalgroup),100(users)
baseuser#machine:/tmp/tata$
Well in facts, the parition was mounted with nosuid option. This can be checked through mount command
I am trying to intercept execve() via execl(). Here's my wrapper call (built as a shared library — libexec.so).
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
static int (*real_exec)(const char*, char *const [], char *const []) = 0;
static void __attribute__((constructor))init(void) {
real_exec = (int (*)(const char*, char *const [], char *const []))dlsym(RTLD_NEXT, "execve");
}
int execve(const char* arg, char *const argv[], char *const envp[]) {
printf ("In wrapped execve\n");
return (*real_exec)(arg, argv, envp);
}
and here's my program that execs.
//run.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
int pid;
pid = fork();
if (pid == 0) {
execl("/usr/bin/date", "date", NULL);
} else {
wait (NULL);
}
return 0;
}
AFAIK, all other exec* calls are wrapper over execve() system call. I also validated the program above by running with strace.
> strace -f -e execve ./run
execve("./run", ["./run"], 0x7ffcefad6a28 /* 61 vars */) = 0
strace: Process 1491914 attached
[pid 1491914] execve("/usr/bin/date", ["date"], 0x7fff28393cc8 /* 61 vars */) = 0
Tuesday 16 February 2021 12:52:16 PM UTC
[pid 1491914] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1491914, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++
But when I run the program the following way,
> LD_PRELOAD=/home/user/libexec.so ./run
the call is not getting intercepted. i.e I don't see In wrapped execve\n getting printed.
What am I missing here? If I instead directly call execve() in run.c, it works.
Secondly, does LD_PRELOAD also follow child processes? Are the calls made by the children and descendents also intercepted?
Short Answer: The issue is that with the above implemented 'libexec' will only intercepted execve calls between the main program to execve. Calls inside 'libc' between execl and execve will not be intercepted to the libexec wrapper.
Long Answer:
With LD_LIBRARY_PATH=libexec.so, the following calling tree is established: "main" -> "libexec.so" -> "libc.so". Calls from "main" will be intercepted by libexec, but calls inside "libc.so" (e.g., execl -> execve) are not intercepted (by default).
The solution is to add a wrapper to "execl" to the libexec.so, following the same structure as the execve wrapper already implemented.
I'm trying to compile a tool that uses gps.h, but my compilation seems to fail each time when it tries to link to libgps. The error message I receive is:
/opt/openwrt-sdk/staging_dir/toolchain-arm_cortex-a9+vfpv3_gcc-7.3.0_musl_eabi/bin/../lib/gcc/arm-openwrt-linux-muslgnueabi/7.3.0/../../../../arm-openwrt-linux-muslgnueabi/bin/ld: cannot find -lgps
This is the command I'm compiling with:
arm-openwrt-linux-gcc -o ./bin/eagle src/main.c -I./src -I/opt/openwrt-sdk/staging_dir/target-arm_cortex-a9+vfpv3_musl_eabi/usr/include -static -L/opt/openwrt-sdk/staging_dir/target-arm_cortex-a9+vfpv3_musl_eabi/usr/lib -lpthread -lgps
Basic code for reference:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <gps.h>
int main(void)
{
int rc;
struct gps_data_t gps_data;
if ((rc = gps_open("localhost", "2947", &gps_data)) == -1)
{
printf("code: %d, reason: %s\n", rc, gps_errstr(rc));
return 1;
}
gps_stream(&gps_data, WATCH_ENABLE | WATCH_JSON, NULL);
return 0;
}
And some directory listings in my toolchain - as far as I can tell, libgps has compiled successfully:
# ls -lah /opt/openwrt-sdk/staging_dir/target-arm_cortex-a9+vfpv3_musl_eabi/usr/include/ | grep gps
-rw-rw-r-- 1 root root 80K Sep 7 2017 gps.h
# ls -lah /opt/openwrt-sdk/staging_dir/target-arm_cortex-a9+vfpv3_musl_eabi/usr/lib/ | grep gps
lrwxrwxrwx 1 root root 16 Oct 17 18:46 libgps.so -> libgps.so.23.0.0
lrwxrwxrwx 1 root root 16 Oct 17 18:46 libgps.so.23 -> libgps.so.23.0.0
-rwxr-xr-x 1 root root 101K Oct 17 18:46 libgps.so.23.0.0
Many thanks in advance for any help.
Your link command line contains '-static', which prevents linking against *.so shared libraries (aka shared objects, hence 'so'), but still permits linking against *.a static-link libraries (aka archives).
See: https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html
In the directory whose contents you listed, and which the link command line is directed towards using the '-L' flag, there is a shared object libgps.so, but there is no static-link library libgps.a .
This leaves the linker no way to satisfy the link-time dependency on libgps. The only way to satisfy it, using libgps.so, has been disabled using '-static'.
To fix, either:
Modify the tool's compile recipe, so as to remove '-static' from the link command line, so as to enable use of the shared object, or
Modify gpsd's compile recipe, so as to cause a static-link library libgps.a to built, either in addition to or instead of libgps.so.
I understand that execve() and family require the first argument of its argument array to be the same as the executable that is also pointed to by its first argument. That is, in this:
execve(prog, args, env);
args[0] will usually be the same as prog. But I can't seem to find information as to why this is.
I also understand that executables (er, at least shell scripts) always have their calling path as the first argument when running, but I would think that the shell would do the work to put it there, and execve() would just call the executable using the path given in its first argument ("prog" from above), then passing the argument array ("args" from above) as one would on the command line.... i.e., I don't call scripts on the command line with a duplicate executable path in the args list....
/bin/ls /bin/ls /home/john
Can someone explain?
There is no requirement that the first of the arguments bear any relation to the name of the executable:
int main(void)
{
char *args[3] = { "rip van winkle", "30", 0 };
execv("/bin/sleep", args);
return 1;
}
Try it - on a Mac (after three tests):
make x; ./x & sleep 1; ps
The output on the third run was:
MiniMac JL: make x; ./x & sleep 1; ps
make: `x' is up to date.
[3] 5557
PID TTY TIME CMD
5532 ttys000 0:00.04 -bash
5549 ttys000 0:00.00 rip van winkle 30
5553 ttys000 0:00.00 rip van winkle 30
5557 ttys000 0:00.00 rip van winkle 30
MiniMac JL:
EBM comments:
Yeah, and this makes it even more weird. In my test bash script (the target of the execve), I don't see the value of what execve has in arg[0] anywhere -- not in the environment, and not as $0.
Revising the experiment - a script called 'bash.script':
#!/bin/bash
echo "bash script at sleep (0: $0; *: $*)"
sleep 30
And a revised program:
int main(void)
{
char *args[3] = { "rip van winkle", "30", 0 };
execv("./bash.script", args);
return 1;
}
This yields the ps output:
bash script at sleep (0: ./bash.script; *: 30)
PID TTY TIME CMD
7804 ttys000 0:00.11 -bash
7829 ttys000 0:00.00 /bin/bash ./bash.script 30
7832 ttys000 0:00.00 sleep 30
There are two possibilities as I see it:
The kernel juggles the command line when executing the script via the shebang ('#!/bin/bash') line, or
Bash itself dinks with its argument list.
How to establish the difference? I suppose copying the shell to an alternative name, and then using that alternative name in the shebang would tell us something:
$ cp /bin/bash jiminy.cricket
$ sed "s%/bin/bash%$PWD/jiminy.cricket%" bash.script > tmp
$ mv tmp bash.script
$ chmod +w bash.script
$ ./x & sleep 1; ps
[1] 7851
bash script at sleep (0: ./bash.script; *: 30)
PID TTY TIME CMD
7804 ttys000 0:00.12 -bash
7851 ttys000 0:00.01 /Users/jleffler/tmp/soq/jiminy.cricket ./bash.script 30
7854 ttys000 0:00.00 sleep 30
$
This, I think, indicates that the kernel rewrites argv[0] when the shebang mechanism is used.
Addressing the comment by nategoose:
MiniMac JL: pwd
/Users/jleffler/tmp/soq
MiniMac JL: cat al.c
#include <stdio.h>
int main(int argc, char **argv)
{
while (*argv)
puts(*argv++);
return 0;
}
MiniMac JL: make al.c
cc al.c -o al
MiniMac JL: ./al a b 'c d' e
./al
a
b
c d
e
MiniMac JL: cat bash.script
#!/Users/jleffler/tmp/soq/al
echo "bash script at sleep (0: $0; *: $*)"
sleep 30
MiniMac JL: ./x
/Users/jleffler/tmp/soq/al
./bash.script
30
MiniMac JL:
That shows that it is the shebang '#!/path/to/program' mechanism, rather than any program such as Bash, that adjusts the values of argv[0]. So, when a binary is executed, the value of argv[0] is not adjusted; when a script is executed via the shebang, the argument list is adjusted by the kernel; argv[0] is the binary listed on the shebang; if there is an argument after the shebang, that becomes argv[1]; the next argument is the name of the script file, followed by any remaining arguments from the execv() or equivalent call.
MiniMac JL: cat bash.script
#!/Users/jleffler/tmp/soq/al -arg0
#!/bin/bash
#!/Users/jleffler/tmp/soq/jiminy.cricket
echo "bash script at sleep (0: $0; *: $*)"
sleep 30
MiniMac JL: ./x
/Users/jleffler/tmp/soq/al
-arg0
./bash.script
30
MiniMac JL:
According to this, the first argument being the program name is a custom.
by custom, the first element should be
the name of the executed program (for
example, the last component of path)
That said, these values could be different. If for example, the program was launched from a symbolic link. The program name might be different than that of the link used to launch it.
And, you are right. The shell would normally do the work of setting up the first argument. In this case however, the use of execve circumvents the shell altogether - which is why you need to set it up yourself.
It allows you to specify the exact path to the executable to be loaded, but also allows for a "beautified" name to be presented in tools such as ps or top.
execl("/bin/ls", "ls", "/home/john", (char *)0);
That allows a program to have many names and work slightly differently depending on using which name it was called.
Imaging trivial program, e.g. print0.c compiled into print0:
#include <stdio.h>
int main(int argc, char **argv)
{
printf("%s\n",argv[0]);
return 0;
}
Running it as ./print0 would print ./print0 Make a symbolic link e.g. print1 to it and now use name ./print1 to run it - it would print "./print1".
Now that was with a symlink. But with exec*() function, you can tell program its name explicitly.
Artifact from *NIX, but nice to have nevertheless.