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
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 400670And after running pocrypter with some random key, it will look like this:400da1: c9 leaveq 400da2: c3 retq
$ 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 4010b1401093: 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.
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.■
CLICKS: 4885