Jynx2 Sneak Peek & Analysis
Jynx2 is the second installment in the LD_Preload Jynx Rootkit series first released October 19, 2011 at blackhatacademy.org. See references for earlier versions and additional information.
Features:
- Hooks accept() for socket connections
- Multi-factor authentication
- Suid Privesc Drop
- Process hiding
- File hiding
C’s accept() function is the function used when a socket connection is received and initiated by the server. This is typically used for all TCP-related server-side functionality written in C, and by overriding it, we can determine if this is regular traffic for the port, or if it’s the rootkit owner attempting to log in. In order to determine whether or not to hijack the socket’s file descriptor, we check against the client-side port (defined by highport and lowport in the pre-compile configuration of the rootkit) attempting to open the connection. If the port is correct, the file descriptor is hijacked and the connection and related processes becomes hidden from a typical administrator. Otherwise, the connection is passed to the service daemon and the service operates normally for the user connecting to the service, as if no rootkit is present.
Due to the accept() hook, this rootkit does not require any modification of existing firewalls. This makes it particularly effective when the server is behind a network layer appliance type firewall, as no holes need to be poked. Any existing service may be hooked; so long as the service is restarted it will grant access as the service’s username, and suid shell drop is available. While this feature is similar to ncom’s accept() hook, the use of SSL is a vast improvement over it.
Files and processes are hidden by several factors in the pre-compile configuration phase, along with a default password (DEFAULT_PASS). The factors used after authentication for hiding files and processes include a “Magic string” and a “Magic GID”. This means that any files beginning with a particular string (“XxJynx” by default) or owned by a particular group will automatically be hidden from the root user.
# cd ~user/ # mkdir XxJynx_myfiles # ls | grep myfiles #
However, due to the nature of the kit, we can still see this file when we are logged in from the netcat shell (an attacker’s perspective). This was fixed with the file “reality.so” installed to its INSTALL directory on installation of the kit.
# ncat --ssl localhost 80 -p1021 cd ~user/ # ls | grep -i jynx XxJynx_myfiles
Like many things on UNIX (and Linux by extension), processes are represented as files. This is done the /proc filesystem in Linux. Every process is has a directory corresponding to its PID in /proc.
For example:
$ sleep 100 [1] 3700 $ ls /proc/3700 attr coredump_filter io mountstats pagemap stat autogroup cpuset limits net personality statm auxv cwd loginuid ns root status cgroup environ maps numa_maps sched syscall clear_refs exe mem oom_adj sessionid task cmdline fd mountinfo oom_score smaps wchan comm fdinfo mounts oom_score_adj stack
The files contain various pieces of information about the process, such as memory space, environment variables and current working directory. Since there is no API within Linux for viewing process information, all one must do to hide a process is hide its entry in the /proc file system. Since each the proc entries are owned by the owner of that process, any process started under the magic GID/UID will be hidden just the same as a file would be.
Processes spawned by the backdoor or by the jynx user are also hidden from listing in /proc:
jynx@kingmaker:/home/jynx2$ sleep 1000 & [1] 30835 jynx@kingmaker:/home/jynx2$ ls /proc/30835 ls: cannot access /proc/30835: No such file or directory
By setting the LD_PRELOAD environment variable to the reality.so file, we can see hidden files, processes, and folders:
jynx@kingmaker:/home/jynx2$ LD_PRELOAD=/XxJynx/reality.so ls /proc/30835 attr auxv clear_refs comm cpuset environ fd io loginuid mem mounts net numa_maps oom_score pagemap root sessionid stack statm syscall wchan autogroup cgroup cmdline coredump_filter cwd exe fdinfo limits maps mountinfo mountstats ns oom_adj oom_score_adj personality sched smaps stat status task
Processes owned by the magic GID or spawned by the backdoor are hidden similarly from ps:
# su jynx $ sleep 100 # ps aux | grep sleep user 30827 0.0 0.0 18252 1612 pts/2 S+ 18:43 0:00 grep sleep
There is no sleep process from the jynx user. Similarly, using the backdoor:
# ncat --ssl localhost 80 -p1021 whoami www-data # ps aux | grep bash user 29683 0.0 0.1 21064 3992 pts/3 Ss 17:20 0:00 bash root 29786 0.0 0.0 19448 2176 pts/3 S 17:20 0:00 bash user 29995 0.0 0.1 21064 3996 pts/1 Ss 17:34 0:00 bash root 30124 0.0 0.0 19448 2212 pts/1 S 17:35 0:00 bash user 30516 1.6 0.1 31500 4588 pts/2 Ss 18:07 0:00 bash user 30608 0.0 0.0 18256 1612 pts/2 S+ 18:07 0:00 grep bash
As we can see here, there is no bash process running in ps for www-data.
Additionally, by setting the environment variable matching the magic string, we’re able to obtain root privileges with the backdoor using a suid binary:
# ncat --ssl localhost 80 -p 1021 # whoami www-data # XxJynx=hax gpasswd / stdin: is not a tty # whoami root
The privilege escelation backdoor uses preloaded setuid bins to produce a root shell. For example, gpasswd, which is used in the above example:
$ ls -lah $(which gpasswd) -rwsr-xr-x 1 root root 59K Feb 16 2011 /usr/bin/gpasswd
LD_PRELOAD will not normally apply to setuid binaries unless certain conditions are met, most notably the shared library must be placed in /lib and /usr/lib. However, this restriction does not apply to shared libraries placed in /etc/ld.so.preload. In each hooked/preloaded function there is a function which checks the environment variable XxJynx (which is set in config.h) for a specific value. If that value is set, it will spawn a root shell if it has the permissions to do so.
Detection:
- LDD
Perhaps the simplest method of detection is with ldd, this is a simple ldd of the “ls” coreutil binary.
# ldd /bin/ls linux-vdso.so.1 => (0x00007fff86dff000) /XxJynx/jynx2.so (0x00007f6ce5a3e000) libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f6ce5806000) librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f6ce55fe000) libacl.so.1 => /lib/x86_64-linux-gnu/libacl.so.1 (0x00007f6ce53f6000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6ce506f000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f6ce4e6a000) libssl.so.1.0.0 => /usr/lib/x86_64-linux-gnu/libssl.so.1.0.0 (0x00007f6ce4c18000) /lib64/ld-linux-x86-64.so.2 (0x00007f6ce5c48000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f6ce49fc000) libattr.so.1 => /lib/x86_64-linux-gnu/libattr.so.1 (0x00007f6ce47f7000) libcrypto.so.1.0.0 => /usr/lib/x86_64-linux-gnu/libcrypto.so.1.0.0 (0x00007f6ce4431000) libz.so.1 => /usr/lib/libz.so.1 (0x00007f6ce421a000)
This, of course could be changed to point to /etc/ld.so.preload, however if you try to access the file, it won’t exist. So, one method of detection would be to determine the presence of ld.so.preload etc or any library inside of an `ldd’ listing that cannot be read by your user using bash, claiming that the file does not exist.
- strace
We ran “strace nc -l -p 6001”, as even netcat will be hooked, to show an example of the accept() hook. Here, we show netcat binding and listening on the port, then waiting for connection
bind(3, {sa_family=AF_INET, sin_port=htons(6001), sin_addr=inet_addr("0.0.0.0")}, 16) = 0 listen(3, 10) = 0 fcntl64(3, F_GETFL) = 0x2 (flags O_RDWR) fcntl64(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0 select(4, [3], NULL, NULL, NULL) = 1 (in [3])
Now we can see the connection being accepted in strace:
accept(3, {sa_family=AF_INET, sin_port=htons(1021), sin_addr=inet_addr("127.0.0.1")}, [16]) = 4
Once the password is entered, we see the kit hijacking the file descriptor::
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xb729d728) = 8527 close(-1) = -1 EBADF (Bad file descriptor)
Then moving to protect the connection:
select(4, [3], NULL, NULL, NULL) = ? ERESTARTNOHAND (To be restarted) --- {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=8527, si_status=1, si_utime=7, si_stime=0} (Child exited) --- waitpid(-1, NULL, WNOHANG) = 8527 waitpid(-1, NULL, WNOHANG) = -1 ECHILD (No child processes) sigreturn() = ? (mask now [])
At this point in the sequence, an attacker has already entered root access level on the infected system, yet the connection does not appear in netstat, nor any additional PID’s appear in /proc or processes in ps/top. So we can easily make a comparison based from the strace with netstat in order to locate an attacker logging into a compromised machine.
- Netstat and pcap discrepancies
Another method of detection includes the comparison of pcap data with netstat data; however it is also difficult to determine what the attacker was doing due to the shell being SSL encrypted. Our PCAP file contains a recording of the infected host’s traffic logs. Though this rootkit may hide from netstat, it does not yet hide from pcap. The SSL hook does not require the support of SSL within the service, only that SSL be installed on the system being infected.
Removal
The older method of removing jynx will no longer work:
echo "" > /etc/ld.so.preload
For any ld_preload rootkit, the best method of removal is by mounting the drive from a livecd and deleting it that way, due to the number of potential function hooks that could be embedded within the kit. This particular kit does not protect itself from symbolic links, and therefore can be easily removed. For ease of removal, you can run the following commands to remove Jynx2
ln -s /etc/ld.so.preload ./rootkit.so echo "" > ./rootkit.so rm -vf rootkit.so