I wrote that crypter code some years ago, actually, it was a remake of an even older crypter. At that time, PIE binaries were not that popular so I didn't pay much attention to them. Nowadays it is the other way around, so I better fix my crypter. I swear this was not programmed obsolescence.
Identifying the problem
I just notice the program stop working, but I had to figure out why, so I wrote a pretty basic example to see what was wrong with it. Actually I did some boring research to narrow the problem down to the symbols exported by my linker script. So, this is my test program:
#include <stdio.h>
#include <stdint.h>
extern void* ss_size;
extern void* ss_addr;
#define SECURE_FUNC __attribute__((section(".secure")))
SECURE_FUNC int
secure_func (char *s)
{
printf ("This function is secured: %s\n",s );
return 0;
}
int
main ()
{
printf ("Secured Segment size (size1 symbol) : 0x%lx bytes @ %p\n",
(uint64_t)((uint64_t)&ss_size), &ss_addr);
}
Then, if I compile it as normal binary, everything works as expected:
$ gcc -g -no-pie -o min min.c -Wl,test.lds $ ./min Secured Segment size (size1 symbol) : 0x2b bytes @ 0x400592
But if I compile a PIE binary I get this:
$ gcc -g -pie -o min.pie min.c -Wl,test.lds $ ./min.pie Secured Segment size (size1 symbol) : 0x5593f18a702b bytes @ 0x5593f18a76f2
So the issue is pretty obvious.... the size is completely wrong. Actually, it looks like the size
value has also been randomised... Yes, every time you run the program you get different values, however, those 0x5.....
addresses are typical PIE run-time addresses.
Is it the file or the loader?
So, the first thing I wonder was whether the issue was in the binary file, or it was in the way it is loaded into memory. So, first I checked the output of readelf
:
$ readelf -s min | grep size 62: 000000000000002b 0 NOTYPE GLOBAL DEFAULT ABS ss_size $ readelf -s min.pie | grep size 62: 000000000000002b 0 NOTYPE GLOBAL DEFAULT ABS ss_size
That seems to be the right size for the .secure
section. Let's check it
$ readelf -S min | grep -A1 "secure" [14] .secure PROGBITS 0000000000400592 00000592 000000000000002b 0000000000000000 AX 0 0 1 $ readelf -S min.pie | grep -A1 "secure" [15] .secure PROGBITS 0000000000000702 00000702 000000000000002b 0000000000000000 AX 0 0 1
So everything seems to be in order. Let's check what happen at run time. First, let's check the normal binary.
$ gdb -q ./min Reading symbols from ./min...done. (gdb) p &ss_size $1 = (uint64_t *) 0x2b (gdb) b main Breakpoint 1 at 0x4004eb: file min.c, line 22. (gdb) r Starting program: /mnt/mia/work/projects/blog/crypters/pie/min Breakpoint 1, main () at min.c:22 22 printf ("Secured Segment size (size1 symbol) : 0x%lx bytes @ %p %lx\n", (uint64_t)((uint64_t)&ss_size), &ss_addr, *&ss_size); (gdb) p &ss_size $2 = (uint64_t *) 0x2b (gdb)
Everything looks fine. Let's see what happens with the PIE one:
$ gdb -q ./min.pie Reading symbols from ./min.pie...done. (gdb) p &ss_size $1 = (uint64_t *) 0x2b (gdb) b main Breakpoint 1 at 0x64e: file min.c, line 22. (gdb) r Starting program: /mnt/mia/work/projects/blog/crypters/pie/min.pie Breakpoint 1, main () at min.c:22 22 printf ("Secured Segment size (size1 symbol) : 0x%lx bytes @ %p %lx\n", (uint64_t)((uint64_t)&ss_size), &ss_addr, *&ss_size); (gdb) p &ss_size $2 = (uint64_t *) 0x55555555402b
So we see that the symbol is properly read from the file, but, once we run the program and it gets mapped into some random memory address, the symbol is also mapped in that range of addresses (just print the address of the main function to verify that p main
).
A new Linker Script
In order to solve this I wrote a new linker script that instead of exporting the size of the section, exports the start address and the end address, for the relevant segments. This way, as both addresses will be increased by the same amount at run-time, I can just substract them and get the size of the section.
So, the new linker script looks like this:
SECTIONS {
ss_start = ADDR (.secure);
ss_end = ADDR(.secure) + SIZEOF(.secure);
sro_start = ADDR (.rodata);
sro_end = ADDR(.rodata) + SIZEOF(.rodata);
}
No big surprise. Now we just need to add a small modification to the stub
function.
A new stub
The first thing we need to do, is to change the names of the symbols to match the new ones in the linker script. Remember, those are declared as external variables at the beginning of the file:
extern void* ss_start;
extern void* ss_end;
extern void* sro_start;
extern void* sro_end;
And now, we just need to calculate the size in our constructor substracting both values and everything else will just be the same. I will show the whole function down below for the reader's convenience.
static void poc_stub (void) __attribute__ ((constructor));
static void poc_stub (void)
{
memset (key, 0, MAX_KEY);
key[0] = _K[0];
key[1] = _K[1];
key[2] = _K[2];
key[3] = _K[3];
key[4] = _K[4];
key[5] = _K[5];
key[6] = _K[6];
key[7] = _K[7];
long ssize,rosize;
ssize = (long) ((uint64_t)&ss_end-(uint64_t)&ss_start);
rosize = (long) ((uint64_t)&sro_end-(uint64_t)&sro_start);
decrypt_mem ((void*)((uint64_t)&ss_start), (int)ssize);
decrypt_mem ((void*)((uint64_t)&sro_start), (int)rosize);
}
With these small changes, the concept works the same for PIE and non-PIE binaries.... So, let me introduce pocrypt3
.
And that's it. See you whenever the binary format or the loader changes again!
■