GNU/Linux Crypters. The PIE case
SECURITY
GNU/Linux Crypters. The PIE case
2021-03-11
By
picoFlamingo

Revisiting my good old' Pocrypt2 project, I just noticed it has stopped working. After a closer look to the problem I figured out that the issue just happens when trying to crypt PIE(Position Independent Executable) binaries.... keep reading to find out why and how I fixed it.

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!

Header Image Credits: Nicolas HIPPERT

 
Tu publicidad aquí :)