PoCrypt. A Proof Of Concept For Dynamically Decrypting Linux Binaries
SECURITY
PoCrypt. A Proof Of Concept For Dynamically Decrypting Linux Binaries
2016-01-26
By
picoFlamingo

Lately I had been reading about polymorphic code and how a program can change itself at run-time. It is an exciting topic. Finally found some time to build my own proof of concept and go one step forward. This is far of being usable in the wild, but it is, I think, a good starting point to understand how this technology works.
The objective is to produce an executable with some parts of its code (some functions) crypted. This basically implies the modification of the process code at run-time. The code will be crypted in the disk and difficult to disassemble without encryption key. However, when the application is executed, something has to be executed to decrypted the code and enable the normal execution of the application.

The source code I will be using in this post can be found at github.

THE STRATEGY

What we are going to do is to force the code we want protected, into a special ELF section, other than the usual .text. Then, using a external application we will crypt just that section. Finally, the secured application will be extended to call a decoding function at start-up to decrypt the secured code so it can be executed.

For our simple naive case we will just decode the function at the very beginning of our main, but you may just decode the function just before running it and even crypt them again after execution... Anyway, as said this post is just about the proof of concept not about how to use this in a real world use case.

The code that does all the work is in the files pocrypt.c and pocrypt.h. The target.c is the program we want to modify and procrypter is the program that encrypts the ELF sections in another executable.

THE TARGET APPLICATION

Let's start writing what will be our target application. We do not need anything special, so let's go for the classical hello world and let's add a function which will implement a very secret algorithm we do not want anybody to look at... the function we want crypted.
#include <stdio.h>

void
my_secure_function (char *str)
{
  printf ("You said: %s\n", str);
}


int 
main ()
{
  printf ("Hello. I'm a target\n");

  my_secure_function ("Bye!\n");

  return 0;
}

The first thing we have to do is to force our secure function into a segment we can easily identify. This process depends on your specific development environment. I'm targeting GNU/Linux with gcc so this is easily solved with a gcc attribute.

#define SECURE_FUNC __attribute__((section(".secure")))
This definition can be found in pocrypt.h. Adding that modifier to any function definition will push that code into the .secure section. The declaration of our my_secure_function will then become:
SECURE_FUNC void
my_secure_function (char *str)
{
  printf ("You said: %s\n", str);
}
Now, let's see what is this really doing. Let's compile the target application and then, let's take a look inside using readelf.
$ make
$ readelf -S target
There are 37 section headers, starting at offset 0x4740:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
(...)

  [13] .text             PROGBITS         0000000000400720  00000720
       000000000000065f  0000000000000000  AX       0     0     16
  [14] .secure           PROGBITS         0000000000400d7f  00000d7f
       000000000000002a  0000000000000000  AX       0     0     1
  [15] .fini             PROGBITS         0000000000400dac  00000dac
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000400db8  00000db8
       0000000000000164  0000000000000000   A       0     0     8
(...)
  [36] .strtab           STRTAB           0000000000000000  00005998
       0000000000000399  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
You can see the new .secure section just after the default .text. Both with read (Allocation) and eXecutable permissions... That is code.

ENCODING AN ELF SECTION

Next step is to encode the section. In other words, we have to open the ELF binary, find out where the section is stored within the file and then crypt it. This can be done in many different ways. In this PoC, we are opening the file and then mmapping it. This allows us to access the file as if it were a memory block. Besides, our file in the disk will be automatically updated when we close the file in our program, applying any change we had done in memory.

Finding the section is quite straightforward:

  • Calculate the pointer to the list of Sections
  • Calculate the pointer to the string table (to retrieve sections names)
  • Loop through the section list until we find the section we want

Check the ELF spec to find out the details of the format and how do you calculate those pointers.

Once we have found the section record within the ELF file, we have all the information we need. That is, the offset in the file (and therefore in our mmapped block) and the size of the section.

With that information we just XOR the memory block, and then close the file. The file will be updated. If you set a define symbol DUMP at compile time, the pocrypter application will dump the contents of the section been XORed before and after for comparison. The line is commented out in the Makefile if you want to try and you are not sure how.

Additional, objdump will show us how the machine code associated to the function looks like.

This is what objdump reports on the original target file (your may be different):

Disassembly of section .secure:

$ objdump -d target
(...)
0000000000400d7f <my_secure_function>:
  400d7f:       55                      push   %rbp
  400d80:       48 89 e5                mov    %rsp,%rbp
  400d83:       48 83 ec 10             sub    $0x10,%rsp
  400d87:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
  400d8b:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  400d8f:       48 89 c6                mov    %rax,%rsi
  400d92:       bf bc 0d 40 00          mov    $0x400dbc,%edi
  400d97:       b8 00 00 00 00          mov    $0x0,%eax
  400d9c:       e8 cf f8 ff ff          callq  400670 
  400da1:       c9                      leaveq 
  400da2:       c3                      retq   

And after running pocrypter with some random key, it will look like this:

$ pocrypter abcd ./target 
$ objdump -d target
(...)

Disassembly of section .secure:

0000000000401088 <my_secure_function>:
  401088:       34 2a                   xor    $0x2a,%al
  40108a:       ea                      (bad)  
  40108b:       81 29 e1 8f 74 29       subl   $0x29748fe1,(%rcx)
  401091:       eb 1e                   jmp    4010b1 
  401093:       9c                      pushfq 
  401094:       d9 b6 73 24 61 2a       fnstenv 0x2a612473(%rsi)
  40109a:       e8 31 99 2a ea          callq  ffffffffea6aa9d0 <_end+0xffffffffea0a8920>
  40109f:       b2 29                   mov    $0x29,%dl
  4010a1:       eb a4                   jmp    401047 <__fstat+0x7>
  4010a3:       dc 61 62                fsubl  0x62(%rcx)
  4010a6:       63 64 89 f1             movslq -0xf(%rcx,%rcx,4),%esp
  4010aa:       95                      xchg   %eax,%ebp
  4010ab:       9b                      fwait
  4010ac:       9e                      sahf   
  4010ad:       da 63 64                fisubl 0x64(%rbx)
  4010b0:       61                      (bad)  
  4010b1:       62                      (bad)  
  4010b2:       aa                      stos   %al,%es:(%rdi)
  4010b3:       a7                      cmpsl  %es:(%rdi),%ds:(%rsi)

RUNNING THE ENCRYPTED

Now that our binary file has been selectively encoded we need to do some small changes on target.c to let it decrypt its own code. This is the fixed part you usually see on cryptographic polymorphic engines. One of the things an AntiVirus software can use as a signature, as this part does not change... not on a naive application like this.

So, our target has to be modified like this:

#include <stdio.h>
#include "pocrypt.h"

SECURE_FUNC void
my_secure_function (char *str)
{
  printf ("You said: %s\n", str);
}

/* End of secure region mark */
SECURE_FUNC void dummy (){};

int 
main ()
{
  printf ("Hello. I'm a target\n");
  if (argc != 2)
    {
      fprintf (stderr, "I need a key\n");
      return 1;
    }

  dump_mem ((unsigned char*)&my_secure_function, (unsigned char*)&dummy);
  set_key(argv[1]);
  decrypt_mem ((unsigned char*)&my_secure_function, (unsigned char*)&dummy);
  dump_mem ((unsigned char*)&my_secure_function, (unsigned char*)&dummy);

  my_secure_function ("Bye!\n");

  return 0;
}

The dump_mem function is just to show that the memory is being changed at run-time and to check that the decoding was correct, comparing it with the similar dumps produced by pocryper. They can be safely removed removing the DUMP define in the Makefile. There are three important parts are:

  • 1. We have to set the password. This may be skipped if we use a fixed password (that makes the whole concept very easy to break) or storing the password, somehow in the executable. For our purposes it is OK to ask for a password as a parameter.
  • 2. The call to decrypt_mem at the beginning of the main function. This function receives two pointers and decodes all the memory between those two memory addresses.
  • 3. The third part is the addition of a dummy function. This function is used to mark up the end of the area to decode. This does not work in all cases but it is probably the simplest solution.There are other options but exploring that is out of the scope of this text.

RUN-TIME DECODING

There is one last piece on the puzzle we yet have to talk about. The .secure section, as created by the compiler is a section containing code, and therefore, it does not have write permissions. In order to re-write it we need to first change the permissions for that memory block before being able to revert to the original un-encrypted code.

Changing permissions of memory blocks can be achieved with the system call mprotect. The only thing you need to know about this system call is that the starting address you provide has to be page-aligned. In other words, you have to provide a pointer that points to the beginning of a memory page.

This is one way of doing it (you can find this code in the function decrypt_mem in pocrypt.c):

  size_t pagesize = sysconf(_SC_PAGESIZE);
  uintptr_t pagestart = (uintptr_t)ptr & -pagesize;
  int psize = (((ptr1 - (unsigned char*)pagestart)));

  /* Make the pages writable...*/
  if (mprotect ((void*)pagestart, psize, PROT_READ | PROT_WRITE | PROT_EXEC) < 0)
    perror ("mprotect:");

First we ask the OS about the page size. That is a system dependant value and needs to be queried. Then we calculate our page aligned pointer, as required by mprotect.

Given a pointer ptr, what we are doing is to do a perform a bitwise AND with the complement of the page size. Suppose that the page size for our machine is 4K (4096 current Linux value) or 0x1000 in hexadecimal. The complement becomes 0xFFFFF000. In other words, we are deleting the last four nibbles of our pointer.

Note: The size of the block has to be calculated from the new start pointer which is now, page aligned.

With all that information we can call mprotect to enable writing in that area of the memory and effectively decode the memory.

From this point on, the application can run as usually.

CAVEATS

During the testing I found something curious. When the application was ready I removed the generation of debug information and also enabled speed optimisation -O2. Then the program stop working. Two things were happening, as far as I can tell:
  • First, gcc reordered the function. The dummy function I used to mark the end of my memory block was generated before my_secure_function. That produced a negative value for the memory block size and the decoder never got executed
  • Second, and more surprising. Taking into account what I had just said (the reorder of the functions), you will expect that, once the application was crypted, it will never work (as it will never be decrypted). However, what happened was exactly the opposite. The target application worked fine all the time. Looking into the assembler, I found my_secret_function there. properly scrambled but the code in main was optimised for calling directly printf. I haven't investigate this in detail but, at first glance, looks like gcc in-lined the function.

So, as I said at the very beginning, this is just a PoC to illustrate roughly the process. Making a robust implementation of a crypter takes quite some more time.

CONCLUSIONS

Hope this post helped to understand how a crypter or packer (that is a bit more complex but similar principle) could be implemented. There is a chance that using any other cryptographic algorithm than XOR will break the system, but I haven't checked it yet. The concern is about the dynamic relocation. I haven't looked into it yet... Anyway, if somebody finds out before me, please drop me a note.