How to connect ALL linux system calls during binary execution

I am trying to change the default behavior for a linux system call. At the moment, I am trying to connect and add a simple print statement before they are called. I know about the standard "wrapper" of the GCC linker option and how it can be used to wrap wrappers. Link to GCC linker options . This works great for open (), fstat (), fwrite (), etc. (Where I actually connect libc shells).

UPDATE:

The limitation is that NOT all system calls connect to this approach. To illustrate this, take a simple statically compiled binary. When we try to add shells, they are obtained from the calls we enter after main () (see strace output, shown below).

> strace ./sample execve("./sample", ["./sample"], [/* 72 vars */]) = 0 uname({sys="Linux", node="kumar", ...}) = 0 brk(0) = 0x71f000 brk(0x7201c0) = 0x7201c0 arch_prctl(ARCH_SET_FS, 0x71f880) = 0 readlink("/proc/self/exe", "/home/admin/sample"..., 4096) = 41 brk(0x7411c0) = 0x7411c0 brk(0x742000) = 0x742000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fbcc54d1000 write(1, "Hello from the wrapped readlink "..., 36Hello from the wrapped readlink : ) = 36 readlink("/usr/bin/gnome-www-browser", "/etc/alternatives/gnome-www-brow"..., 255) = 35 write(1, "/etc/alternatives/gnome-www-brow"..., 36/etc/alternatives/gnome-www-browser ) = 36 exit_group(36) = ? +++ exited with 36 +++ 

If we carefully observe the binary code, then the first “uncaught” call to readlink () comes from these lines (system call 89, that is, 0x59), - the part of the code associated with the linker (ie _dl_get_origin) performs the readlink ( ) for its functioning, These implicit syscall (although they are present in binary code) never connect to our "wrapper" approach.

  000000000051875c <_dl_get_origin>: 51875c: b8 59 00 00 00 mov $0x59,%eax 518761: 55 push %rbp 518762: 53 push %rbx 518763: 48 81 ec 00 10 00 00 sub $0x1000,%rsp 51876a: 48 89 e6 mov %rsp,%rsi 51876d: 0f 05 syscall 

How to extend the idea of ​​wrapping to system calls like readlink () (including all implicit calls)?

+6
source share
1 answer

ld there is an option for wrapping, quote from the manual :

- wrap symbol

Use the wrapper function for the character. Any undefined reference to a character will be resolved by __wrap_symbol. Any undefined reference to __real_symbol will be resolved to the character. This can be used to provide a wrapper for a system function. The wrapper function should be called __wrap_symbol. If he wants to call a system function, he must call __real_symbol.

It works great with system calls. Here is an example with readlink :

 #include <stdio.h> #include <string.h> #include <unistd.h> ssize_t __real_readlink(const char *path, char *buf, size_t bufsiz); ssize_t __wrap_readlink(const char *path, char *buf, size_t bufsiz) { puts("Hello from the wrapped readlink :"); __real_readlink(path, buf, bufsiz); } int main(void) { const char testLink[] = "/usr/bin/gnome-www-browser"; char buf[256]; memset(buf, 0, sizeof(buf)); readlink(testLink, buf, sizeof(buf)-1); puts(buf); } 

To pass an option to the linker from the compiler, use the -Wl option:

 $ gcc test.c -oa -Wl,--wrap=readlink $ ./a Hello from the wrapped readlink : /etc/alternatives/gnome-www-browser 

The idea is that __wrap_func is your wrapper of functions. The __real_func component binds to the real func function. And every func call in the code will be replaced with __wrap_func .

UPD: You may notice that a binary compiled statically causes another readlink that is not intercepted. To understand the reason, just do a little experiment - compile the code into an object file and list the characters, for example:

 $ gcc test.c -c -o ao -Wl,--wrap=readlink $ nm ao 0000000000000037 T main U memset U puts U readlink U __real_readlink U __stack_chk_fail 0000000000000000 T __wrap_readlink 

Interestingly, you will not see links to a bunch of functions that are visible with strace before entering the main function - for example. uname() , brk() , access() , etc. This is because the main function is not the first code that is called in your binary. A bit of research on objdump will show you that the first function is called _start .

Now let's take another example: override the _start function:

 $ cat test2.c #include <stdio.h> #include <unistd.h> void _start() { puts("Hello"); _exit(0); } $ gcc test2.c -oa -nostartfiles $ strace ./a execve("./a", ["./a"], [/* 69 vars */]) = 0 brk(0) = 0x150c000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece55d000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=177964, ...}) = 0 mmap(NULL, 177964, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f3ece531000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\320\37\2\0\0\0\0\0"..., 832) = 832 fstat(3, {st_mode=S_IFREG|0755, st_size=1840928, ...}) = 0 mmap(NULL, 3949248, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f3ecdf78000 mprotect(0x7f3ece133000, 2093056, PROT_NONE) = 0 mmap(0x7f3ece332000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1ba000) = 0x7f3ece332000 mmap(0x7f3ece338000, 17088, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f3ece338000 close(3) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece530000 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece52e000 arch_prctl(ARCH_SET_FS, 0x7f3ece52e740) = 0 mprotect(0x7f3ece332000, 16384, PROT_READ) = 0 mprotect(0x600000, 4096, PROT_READ) = 0 mprotect(0x7f3ece55f000, 4096, PROT_READ) = 0 munmap(0x7f3ece531000, 177964) = 0 fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 10), ...}) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece55c000 write(1, "Hello\n", 6Hello ) = 6 exit_group(0) = ? +++ exited with 0 +++ $ 

What was it?! We just redefined the first function in the binary and still see the system calls - why?

In fact, this is because the calls are not made by your application, but rather by the kernel before your application is loaded into memory and allowed to work.

UPD: as we saw earlier, functions are not called by your application. Honestly, I could not find what is done for static binaries after the shell calls execve for your application, but from the list it looks like every call you see made by the kernel itself - without any side application, such as a dynamic linker that are not needed for static binaries (and because there are functions like brk that work with data segments).

Regardless of the fact that you, of course, cannot easily change this behavior, you will need a hack. Since if you can easily override the function for the code that is executed before your binary launch, that is, from another binary file, it will be a big black hole in security, imagine: as soon as you need root privileges, you will redefine the function with one, to execute your code, and wait a while while some root daemon executes the script, and thus launches your code into the game.

+1
source

Source: https://habr.com/ru/post/989990/


All Articles