Introduction

In our last post, we saw how to make a simple virus that propagates inside ELF 64-bits files but we saw at the end that the way we did it was not really stealthy and the whole point of a malware is to be stealthy to avoid being spotted and removed. To remedy to that, we will now use a new technique that won’t raise any suspicions when the infected binary is being inspected with the readelf command. It means neither the number of segments nor their permissions should be changed and the entrypoint will also remain the same. Since our last post has been really detailed about the code, we will now just see the concepts to write a better virus. If you are interested in the code, I did a virus that implements all of these techniques that you can find here.

How do we do that ?

We will break down this problem in two parts, the first one will be finding a place where we can inject ourself and then we will find a way to get control of the execution flow to execute our payload. To avoid changing the number of load segment, since they are the only one that are mapped into memory, we know that we are going to have to put them here. There are two well-known infection techniques that would respect the constraints and that are safe:

  • The reverse text segment padding
  • The text segment padding

Those techniques are really efficient and simple to implement but they have the major inconvenient that the padding size depends on the file and we might not be able to inject our payload. That’s why we will implement a handcrafted technique of mine which is the code bouncing one. We will explain why later but that technique is not optimal so we will only use it as a backup solution if we cannot insert ourself on the text segment padding.

The optimal solution : the text segment padding

The two known techniques evoked above are quite similar in terms of result so I chose the second one which is the “normal” text segment padding insertion.

The ELF format has been made so that memory mapping of file parts is being made easy thanks to the congruency between offsets and address, modulo a page size, which is usually 4096 bytes. That being said, since everything on the same page has the same permissions, code and data cannot be on the same page. That would be a disaster for security if data had execution permissions or if we could rewrite the code. To put code and data on different pages while keeping the offsets aligned on the memory, the only solution is to put some padding after the code segment. Those are unused null bytes, that we can replace with our code.

pages

As we can see, if the padding is large enough, we can put our payload inside it and make a few modification to some of the ELF structures to map it inside an executable memory segment which is exactly what we want. To check that we can put our payload inside the padding we can just compute this formula : padding_size = rodata_phdr.p_off - (text_phdr.p_off + test_phdr.p_filesz).

Once we know that we can infect the file using this technique, we just have to :

  • copy the payload in the padding area.
  • increase the text segment size so that our payload gets mapped into memory at runtime
  • increase the last section of the segment (usually .fini) so that our payload fit in a section to avoid any suspicion.

Now, if everything has been done correctly, the only remaining thing to do is to hijack the execution flow but first let’s see whta we can do in case our payload doesn’t fit.

The backup solution : the code bouncing

When we don’t have enough place to insert ourselves in the text segment padding, there is still somewhere we can go whatever the size of our payload is : the end of the last segment. Indeed, we can extend it as much as we want to adapt to any payload but the last load segment is usually the data segment which brings two problems:

  • the BSS : this section used for uninitialized global and static variables is present in memory but not in the file
  • the permissions: we said that we don’t want to grant any additional permission to our load segments to avoid being spotted with a readelf

That’s where the code bouncing technique comes in.

First, let’s insert our payload in the data segment. As we said earlier, we want to put our code at the end of the last segment which is the data segment. The data segments usually ends with the BSS section. This section is used for uninitialized static and global variables. Since those are not stored on the stack, the program has to store them somewhere writable so in the data segment. However, since those are uninitialized, storing them in the file would be useless and consume unnecessary space. To address that problem, the BSS section has been created and it will only be present in memory. That is why the filesz and memsz attributes of the Program Header structure are sometimes different.

pages

The problem with that situation is that this memory zone used for the BSS is usually zero-filled and it could be used by the program anyway which would result in our payload being overridden like so :

pages

To avoid this problem, we cannot move the BSS after the payload since it would break all the references to the variables present inside. Something we can do though is writing the BSS. It will ensure the integrity of our code while having no effect on the BSS itself. However it means adding some space in the middle of the file, which will break some offsets and potentially some important part of the files like the section header table. Because we do not want that, we will be careful to conserve the file integrity.

It will mean shifting everything that comes after the BSS before writing it and the payload and once we’ve done that, we will update the offsets of every section that follows the BSS and the offset of the section header table in the ELF header. With that done, the file structure will remain the same and nothing should be broke, at least not on the execution point of view because nothing is mapped after the BSS. Since we’ve updated the offsets, we can still use readelf -t to check the sections. Everything should be coherent for now but we have to modify the BSS section header which will bring some little abnormal things to readelf. We have to change its type to SHT_PROGBITS instead of SHT_NOBITS to get it mapped into memory. We also have to update the size of the BSS to include our payload but also the sizes of the data segment without forgetting that it already counted the BSS but only in the p_memsz field. Once that is done, we have our code placed in memory but on a non-executable segment, which is a problem because we will segfault if we execute it. We cannot change it in the file to avoid being spotted too easily but we can try to change it at runtime. That is what the code bouncing technique is about.

To resolve our current issue, we will insert a short chunk of code in the text padding segment that will grant the execution permission to the data segment using the mprotect syscall. Once it has made the data segment executable, it will just jump on it to execute it.

pages

What about the execution flow ?

We achieved our first goal : we have an executable payload mapped into memory no matter its size, the only limit is that the code padding is able to host the code bounce code chunk (≈ 20 bytes). All of that without modifying the number of load segments nor their permissions. There is not any suspicious red flag when looking at the readelf command on an infected program. For now, our payload is useless though since it is not being executed. Our second task will be executing it without changing the entrypoint.

To do so, we have to find something that gets executed no matter what but that is not the entrypoint. Have you ever tried to compile a simple Hello World in C and then inspect what is in the binary. If not, you should and you will see that there is actually way more than just the main. There is a lot happening before the main gets executed. But the entrypoint is not even main, it usually points on _start, a wrapper that will call main with the correct arguments. But before that, the loader is doing a lot, it has to map everything, update the relocation call some initialization functions etc… If you want to know more what happens before the execution of main you can (and you should) read this article.

Now that we know all of that, we have a pretty good idea about how we can hijack the execution of a program without changing the entrypoint. There are many ways to do it, I have chosen the constructor hijacking solution. Every program usually has a constructor, some have more (usually C++ programs). If you ever wandered in an executable using objdump, you have probably already seen the frame_dummy function that is useless to most of the programs but it is there. It is the only constructor for a normal C program. The addresses of the constructors are stored in a function pointer array present in the .init_array section. The _init function, called at the very beginning of a program calls each one of this array entries. To get our payload executed, we simply have to override the first entry of this array with the address of our payload. The advantage of this double technique is that both have their starting point at the same place (in the text segment padding) so we don’t have to take the payload place into account when we override the address. Now this should work for normal executables but there is still a problem to handle. On PIE executable, the .init_array entries are in the relocation table (in the .rela.dyn section) which means that the loader is actually gonna replace their address by the value indicated in the relocation field so we also have to modify the r_addend field of these to indicate to the program where the function is gonna be once the executable has been mapped in a random place.

Conclusion

Now that all of that is patched, we have a working ELF infector that does not show anything suspicious on the readelf command and that does not change the entrypoint either. That is a good start for a stealth malware. On our way to stealthiness, we will learn a few more things like code obfuscation or oligomorphism (self-encryption) but first, let’s have some fun and let’s add a backdoor to our virus to gain remote access on infected computers.