This is my first post so don’t expect anything much, just sharing an idea is all. Shoutout to @Leeky and @jeff for helping out in regards to OS detection as I’m pretty unfamiliar with it. I recently came across an interesting project called cosmopolitan. The part about this project that makes it interesting is that the binaries compiled using comopolitan is that it’s a compile once and run anywhere. What this means is that you can run the binary without it being compiled specifically for a single OS, but rather is able to be executed using a polyglot format called αcτµαlly pδrταblε εxεcµταblε (pretty aesthetic name, I know) or APE for short. In a nutshell, polyglot formats are files that can be interpreted in different formats depending on what program opens that file. The case of binaries compiled with cosmopolitan falls under this case as different OS loaders are able to properly execute the binary.
I kept thinking of what could be made with such a project as there was no longer a limitation of compiling OS specific binaries. One of the things that came to mind was multi platform malware. I could easily see how malware could be made “multi platform” through scripts but I’ve never heard of the binary itself being multi platform. Realizing this, I tried to see if there were any caveats using this project. Well, there were quite a few but they shouldn’t mean much in MalDev:
- The project doesn’t support any desktop GUIs hence “textmode” in their github description.
- The binary “will assimilate itself as a conventional resident of your platform after the first run” this means, after the first run, the binary is no longer multi platform sadly. Needless to say, if we require that the binary propagates, we need save a copy of it before execution. The only thing I could think of is the binary copying itself on disk during runtime, note that this may be entirely wrong as I’m assuming that the assimilation process happens at the end of execution.
Note that I'm no way responsible for any damage the reader may do using this technique and I'm assuming the reader is a responsible person.
Setup
Now enough intro, let’s get started. We’re going to be developing in Linux since it’s far easier to set up due to the fact that developing on Mac or Windows would require installing gnu linux toolchain before compiling cosmopolitan. To get started we run the following as mentioned on the github:
mkdir mal && cd mal
wget https://justine.lol/cosmopolitan/cosmopolitan-amalgamation-0.3.zip
unzip cosmopolitan-amalgamation-0.3.zip
And that’s it, we just need to stay in the current directory in order for this to work.
Execution?
It is commonly known that malware has OS specific techniques and the only thing in our way is to know when to use these OS specific techniques. The only challenge now is to when to use these OS specific techniques. There are two ways we could do this:
-
A high level method such as looking for presence of paths such as /root, /lib, /bin vs C:\ (thanks uncle jeff)
-
A low level method of using Assembly to detect the running OS (thanks Leeky)
Well the point of the post is not to write full fledged malware but rather prove that it is possible to produce a binary that does its malicious job regardless of the OS. In accordance to this, we’ll try to execute payload that pops a shell. This doesn’t seem malicious you say? Well you can swap the payload for a reverse shell, a forkbomb or a nasty disk wipe if they make the binary qualify as malware.
High Level
Now let’s test if our OS detection techniques work before executing some payload. We’re going to start off with the first method since it seems to be the easier of the two to implement. The source code and the command to compile the source is as follows:
int main() {
char * OS = "None";
if(isdirectory("C:\\"))
OS = "Windows";
else if(isdirectory("/root"))
OS = "Linux";
printf("The current OS is %s\n", OS);
}
gcc -g -Os -static -nostdlib -nostdinc -fno-pie -no-pie -mno-red-zone \
-fno-omit-frame-pointer -pg -mnop-mcount \
-o test.com.dbg test.c -fuse-ld=bfd -Wl,-T,ape.lds \
-include cosmopolitan.h crt.o ape.o cosmopolitan.a
objcopy -S -O binary test.com.dbg test.com
It’s nice how we don’t have to manually write out includes. Anyway, the code is fairly straightforward. The checking is done through looking for the existence of a particular root directory. If you run this binary in a Windows machine, “The current OS is Windows” should be printed to stdout, otherwise it will output “The current OS is Linux”. Upon execution, we get the following output on the Windows host and Linux VM respectively.
C:\Users\crimsonRain\Desktop>test.com
The current OS is Windows
crimsonRain@Desktop:~/mal$./test.com
The current OS is Linux
Happily enough, this works on both Windows 10 and on Ubuntu. Note that I compiled the binary in WSL then copied it to the directory /mnt/c/Users/crimsonRain/Desktop. Since our first method works, let’s now try the second one.
Low Level
As expected, this was the lengthiest to implement as there is a lack of shellcode in regards to OS detection. Though this is the only available post on Multi OS Shellcode, it sadly doesn’t work for Windows 10. Luckily enough, using this post which was referenced in the previous one we are able to customize the given shellcode to suit our needs. The following is the shellcode creation process.
bits 32
arch_detect:
xor eax, eax
dec eax
jnz determine_32_os
determine_64_os:
mov eax, ds
test eax, eax
jnz win64_code
jmp lin64_code
determine_32_os:
mov eax, fs
test eax, eax
jz lin32_code
win32_code:
xor eax, eax
ret
lin64_code:
xor eax, eax
mov al, 1
ret
win64_code:
xor eax, eax
mov al, 2
ret
lin32_code:
xor eax, eax
mov al, 3
ret
Let’s go through the code for a bit. If we run this in a 32 bit system, the jnz under arch_detect would have eax set as -1. If we run this in a 64 bit system however, the dec eax instruction becomes the REX.W prefix for the jnz as outline in prl’s answer. The next part is the determine_64_os label, if we are in a Linux system, the ds segment register is nearly always zero while the opposite is true for Windows. The determine_32_os label is somewhat similar where a Linux system sets the fs segment register to 0 while once again the opposite is true for Windows. The rest of the code is self explanatory as we set 0, 1, 2, 3 as markers to identify the running OS. Continuing on:
nasm -felf32 shellcode.asm
objcopy -O binary -j .text shellcode.o
We have proper shellcode that we want to use, we then get the binary data as a hex string and use it in the following source code:
int get_os() {
unsigned char * exec = "\x31\xc0\x48\x75\x08\x8c"
"\xd8\x85\xc0\x75\x10\xeb"
"\x09\x8c\xe0\x85\xc0\x74"
"\x0d\x31\xc0\xc3\x31\xc0"
"\xb0\x01\xc3\x31\xc0\xb0"
"\x02\xc3\x31\xc0\xb0\x03"
"\xc3";
int (*_get_os)() = (int(*)())exec;
return _get_os();
}
int main() {
printf("%d\n", get_os());
return 0;
}
As we can see from the source code, we use the classical shellcode execution technique in order to invoke the shellcode that we crafted. Upon execution, we get the following output on the Windows host and Linux VM respectively.
C:\Users\crimsonRain\Desktop>test.com
1
crimsonRain@Desktop:~/mal$./test.com
2
And yes, I almost forgot to mention the systems are both 64 bit, so this method of OS detection works. We can conclude that both high and low level methods of OS detection work properly but their use case will be discussed further.
High Level vs Low Level?
- Speed and Size - It’s always a given that Low Level is king in this domain since we take control of constructing assembly instructions rather than handing it off to a compiler. So if you really want these tweaks in performance, you’re better off using the Low Level method of OS detection.
- Ease of Development - Using the High Level method is best if the programmer lacks the appropriate skill to write assembly or wants to save time and effort as crafting shellcode can be fairly frustrating.
- Portability - You could say this is a consequence of point 2, where if we used the High Level approach, we would just need to add a function that expects another parameter for another given OS. This is not the case with the Low Level approach as we are required to look for assembly tricks that take advantage of the differences in the given environments of each OS.
Payload Time
Now we have all the pieces in place and we can finally construct a binary that guarantees execution of payload on either a Windows or Linux system. The following is the final source code with an inclusion of payload:
int get_os() {
unsigned char * exec = "\x31\xc0\x48\x75\x08\x8c"
"\xd8\x85\xc0\x75\x10\xeb"
"\x09\x8c\xe0\x85\xc0\x74"
"\x0d\x31\xc0\xc3\x31\xc0"
"\xb0\x01\xc3\x31\xc0\xb0"
"\x02\xc3\x31\xc0\xb0\x03"
"\xc3";
int (*_get_os)() = (int(*)())exec;
return _get_os();
}
int main() {
int OS = get_os();
if(OS == 1 || OS == 3)
system("/bin/bash");
else
system("cmd.exe");
return 0;
}
Conclusion
It was never thought that cross platform binaries would be a thing, but surprisingly enough, they are here. But through utilizing both an archaic technique of low level OS detection and the new technology of multi platform binaries, we are able to show that it is possible to achieve Multi Platform Execution of a given payload. This idea could be further extended to conditionally executing whole routines for a given OS or be able to make most devices in a network a victim of malware since we don’t need any third party libraries, no interpreter and no virtual machine.
Well, I hope you enjoyed reading this small post and feel free to point out any mistakes as I’m also learning. And finally, thank you @0x00pf for writing out the Programming for Wanabes series which inspired me to write this post in order to share what I learnt.