For a little more in-depth view at what happens when we turn on stack protection (-fstack-protector-all), let's look at the values of the gs:0x14 memory location. We'll see if the assumption that it changes with each function call is correct (and if so, then we have a real conundrum on our hands involving memory in general). In order to measure this, the following C code can be slipped into a recursive function which just prints the value of gs:0x14.
For 64-bit code:
unsigned long int a = 0;
if(sizeof(unsigned int) != sizeof(unsigned long))
asm ("movq %%fs:0x28, %0\n":"=r"(a));
And for 32-bit code:
unsigned int a = 0;
if(sizeof(unsigned int) == sizeof(unsigned long))
asm ("movl %%gs:0x14, %0\n":"=r"(a));
These little snippets populate 'a' with the stack canary value. Note: 64-bit uses fs:0x28 instead of gs:0x14.
Running this reveals the following neat information:
[02:20:12][aconole@ssh:~]
$ ./sp32 4
gs:0x14[3829412976 / E4403470]
gs:0x14[3829412976 / E4403470]
gs:0x14[3829412976 / E4403470]
gs:0x14[3829412976 / E4403470]
gs:0x14[3829412976 / E4403470]
This tells us that the canary is global to the context in which the functions are running. Is this the same across threads? A simple change to the rig I used to print the values for 3 different threads reveals that yes - this canary is global to the process:
[02:25:47][aconole@ssh:~]
$ ./sp32_t 1
gs:0x14[1475995573 / 57F9E7B5]
gs:0x14[1475995573 / 57F9E7B5]
gs:0x14[1475995573 / 57F9E7B5]
gs:0x14[1475995573 / 57F9E7B5]
gs:0x14[1475995573 / 57F9E7B5]
gs:0x14[1475995573 / 57F9E7B5]
The same can be seen for 64-bit code:
[02:27:03][aconole@ssh:~]
$ ./sp64 4
fs:0x28[15354006399877715714 / d5146378bfc91702]
fs:0x28[15354006399877715714 / d5146378bfc91702]
fs:0x28[15354006399877715714 / d5146378bfc91702]
fs:0x28[15354006399877715714 / d5146378bfc91702]
fs:0x28[15354006399877715714 / d5146378bfc91702]
And threaded:
[02:27:31][aconole@ssh:~]
$ ./sp64_t 1
fs:0x28[15000647472177854910 / d02d01662c05b9be]
fs:0x28[15000647472177854910 / d02d01662c05b9be]
fs:0x28[15000647472177854910 / d02d01662c05b9be]
fs:0x28[15000647472177854910 / d02d01662c05b9be]
fs:0x28[15000647472177854910 / d02d01662c05b9be]
fs:0x28[15000647472177854910 / d02d01662c05b9be]
So, now we know some important things about the canary for 64-bit and 32-bit.
#1 - if we can obtain the value while the system is running, then there's no worries on modifying the stack.
#2 - We know from where this value is obtained.
#3 - We know how to retrieve this value in a fancy rig.
The next question, before we jump headfirst into probabilities and statistics for the segment register offset value is: Can we generically modify this global canary?
Here are two generic routines to do so:
void reassign_sc_32()
{
unsigned int a = 0;
if(sizeof(unsigned int) == sizeof(unsigned long))
asm ("movl %0, %%gs:0x14\n"::"r"(a));
}
void reassign_sc_64()
{
unsigned long int a = 0;
if(sizeof(unsigned int) != sizeof(unsigned long))
asm ("movq %0, %%fs:0x28\n"::"r"(a));
}
We can simply call: reassign_sc_64(); reassign_sc_32(); and the code should modify the canary value to 0 on the correct platform. A test reveals:
[02:32:30][aconole@ssh:~]
$ ./sp32_c_t 1
gs:0x14[0 / 0]
gs:0x14[0 / 0]
gs:0x14[0 / 0]
gs:0x14[0 / 0]
gs:0x14[0 / 0]
gs:0x14[0 / 0]
64-bit also yields the same results:
[02:34:05][aconole@ssh:~]
$ ./sp64_c_t 1
fs:0x28[0 / 0]
fs:0x28[0 / 0]
fs:0x28[0 / 0]
fs:0x28[0 / 0]
fs:0x28[0 / 0]
fs:0x28[0 / 0]
So, we can modify the canary value - OUTSTANDING! Lets turn on the stack protector and see it in action:
[02:35:21][aconole@ssh:~]
$ ./sp32_c_t 1
*** stack smashing detected ***: ./sp32_c_t terminated
Aborted
Hrrm. Not quite what I had expected. Looks like we're going to have to delve into the internals of the stack protector after all, which is something I was hoping to avoid.
The code for stack-protector.c below:
#include
#include
int stack_prot_64(int num_left)
{
unsigned long int a = 0;
if(sizeof(unsigned int) != sizeof(unsigned long))
asm ("movq %%fs:0x28, %0\n":"=r"(a));
if(!num_left)
{
return printf("fs:0x28[%lu / %lx]\n", a, a);
}
stack_prot_64(num_left-1);
return printf("fs:0x28[%lu / %lx]\n", a, a);
}
int stack_prot_32(int num_left)
{
unsigned int a = 0;
asm ("movl %%gs:0x14, %0\n":"=r"(a));
if(!num_left)
{
return printf("gs:0x14[%lu / %X]\n", a, a);
}
stack_prot_32(num_left-1);
return printf("gs:0x14[%lu / %X]\n", a, a);
}
void reassign_sc_32()
{
unsigned int a = 0;
asm ("movl %0, %%gs:0x14\n"::"r"(a));
}
void reassign_sc_64()
{
unsigned long int a = 0;
if(sizeof(unsigned int) != sizeof(unsigned long))
asm ("movq %0, %%fs:0x28\n"::"r"(a));
}
void *run32(void *a)
{
#ifdef GAPING_SECURITY_HOLE
reassign_sc_32();
#endif
stack_prot_32(atoi(a));
return NULL;
}
void * run64(void *a)
{
#ifdef GAPING_SECURITY_HOLE
reassign_sc_64();
#endif
stack_prot_64(atoi(a));
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t t1,t2,t3;
if(argc <= 1)
return printf("error: give a number of functions.\n");
if(sizeof(unsigned int) == sizeof(unsigned long))
{
pthread_create(&t1, NULL, run32, argv[1]);
pthread_create(&t2, NULL, run32, argv[1]);
pthread_create(&t3, NULL, run32, argv[1]);
} else
{
pthread_create(&t1, NULL, run64, argv[1]);
pthread_create(&t2, NULL, run64, argv[1]);
pthread_create(&t3, NULL, run64, argv[1]);
}
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
return 0;
}
-Aaron