Debugging Stack Canaries on x86 Linux
On x86 Linux, stack canaries are kept in thread local storage and accessed through the gs
segment register (e.g. gs:0x14
). If you have ever found yourself in a debugging session and wondering how to print out that value then this is the post for you.
Thread local storage is initialized by the set_thread_area(2)
systemcall which takes a user_desc
struct as its only argument.
struct user_desc {
unsigned int entry_number;
unsigned long base_addr;
unsigned int limit;
unsigned int seg_32bit:1;
unsigned int contents:2;
unsigned int read_exec_only:1;
unsigned int limit_in_pages:1;
unsigned int seg_not_present:1;
unsigned int useable:1;
};
The second field, base_addr
is the address of a data structure which will hold our stack canary (among other things).
One way to find the canary value is to catch the set_thread_area
syscall and save the base_addr
. After that, base_addr + 0x14
will be where the canary is.
Here is an exmple program:
#include <stdio.h>
void a()
{
printf("Hello, world!\n");
}
int main()
{
a();
}
We can compile it with -fstack-protector-all
to ensure that every function contains a canary, even our trivially safe a()
function. And -m32
to ensure it is 32 bit.
† gcc -m32 -fstack-protector-all canary-test.c
† r2 -d a.out
[0xf77bf990]> dcs set_thread_area
child stopped with signal 133
--> SN 0xf77bf8fd syscall 243 set_thread_area (0xffffffda 0xffa20d90)
[0xf77bf8fd]>
[0xf77bf8fd]> pd -18
,==< 0xf77bf8b4 7f00 jg 0xf77bf8b6
`--> 0xf77bf8b6 0000 add byte [eax], al
| 0xf77bf8b8 e83e860100 call 0xf77d7efb
| 0xf77bf8bd 83c410 add esp, 0x10
`=< 0xf77bf8c0 ebe5 jmp 0xf77bf8a7
0xf77bf8c2 8b4004 mov eax, dword [eax + 4] ; [0x4:4]=-1 ; 4
0xf77bf8c5 89e3 mov ebx, esp
0xf77bf8c7 898660080000 mov dword [esi + 0x860], eax
0xf77bf8cd 8912 mov dword [edx], edx
0xf77bf8cf 895208 mov dword [edx + 8], edx
0xf77bf8d2 8b86e8feffff mov eax, dword [esi - 0x118]
0xf77bf8d8 c70424ffffff. mov dword [esp], 0xffffffff
0xf77bf8df 894210 mov dword [edx + 0x10], eax
0xf77bf8e2 89542404 mov dword [esp + 4], edx
0xf77bf8e6 b8f3000000 mov eax, 0xf3
0xf77bf8eb c7442408ffff. mov dword [esp + 8], 0xfffff
0xf77bf8f3 c744240c5100. mov dword [esp + 0xc], 0x51
0xf77bf8fb cd80 int 0x80
0xf77bf8fb cd80 int 0x80 This is the syscall invokation.
0xf77bf8e6 b8f3000000 mov eax, 0xf3 This is the syscall number, 0xf3 (243) which is set_thread_area (check `/usr/include/asm/unistd_32.h`).
0xf77bf8c5 89e3 mov ebx, esp This is the user desc struct, it is stored on the stack.
0xf77bf8e2 89542404 mov dword [esp + 4], edx This is where the base_addr field is set (user_desc+4).
So printing out edx
should give us the base_addr we want.
[0xf77bf8fd]> dr edx
0xf75f5700
So user_addr
is 0xf75f5700
.
Next we break on a function (in this case a()
), and single step until the cookie is in a register we can print out (eax
).
[0xf77bf8fd]> db sym.a
[0xf77bf8fd]> c
Selecting and continuing: 3685
hit breakpoint at: 804843b
[0x0804843b]> ds ; pd 1 @ `dr eip`
| 0x0804843b b 55 push ebp
[0x0804843b]> ds ; pd 1 @ `dr eip`
| 0x0804843c 89e5 mov ebp, esp
[0x0804843b]> ds ; pd 1 @ `dr eip`
| 0x0804843e 83ec18 sub esp, 0x18
[0x0804843b]> ds ; pd 1 @ `dr eip`
| 0x08048441 65a114000000 mov eax, dword gs:[0x14] ; [0x14:4]=-1 ; 20
[0x0804843b]> ds ; pd 1 @ `dr eip`
| 0x08048447 8945f4 mov dword [local_ch], eax
[0x0804843b]> dr eax
0xffa9c800
If we add 0x14
to the base_addr
we noted earlier do we get the same cookie?
[0x0804843b]> pv4 @ 0xf75f5700 + 0x14
0xffa9c800
Yes.
The same process can be done with gdb
(gdb) catch syscall set_thread_area
Catchpoint 1 (syscall 'set_thread_area' [243])
(gdb) r
Starting program: a.out
Catchpoint 1 (call to syscall set_thread_area), 0xf7fda8fd in ?? ()
...
(gdb) disassemble 0xf7fda8c5,0xf7fda8fd
Dump of assembler code from 0xf7fda8c5 to 0xf7fda8fd:
0xf7fda8c5: mov %esp,%ebx
0xf7fda8c7: mov %eax,0x860(%esi)
0xf7fda8cd: mov %edx,(%edx)
0xf7fda8cf: mov %edx,0x8(%edx)
0xf7fda8d2: mov -0x118(%esi),%eax
0xf7fda8d8: movl $0xffffffff,(%esp)
0xf7fda8df: mov %eax,0x10(%edx)
0xf7fda8e2: mov %edx,0x4(%esp)
0xf7fda8e6: mov $0xf3,%eax
0xf7fda8eb: movl $0xfffff,0x8(%esp)
0xf7fda8f3: movl $0x51,0xc(%esp)
0xf7fda8fb: int $0x80
(gdb) p/x $edx
$1 = 0xf7fd5440
(gdb) b a
Breakpoint 2 at 0x8048441
(gdb) c
Continuing.
Breakpoint 2, 0x08048441 in a ()
(gdb) disassemble
Dump of assembler code for function a:
0x0804843b <+0>: push %ebp
0x0804843c <+1>: mov %esp,%ebp
0x0804843e <+3>: sub $0x18,%esp
=> 0x08048441 <+6>: mov %gs:0x14,%eax
0x08048447 <+12>: mov %eax,-0xc(%ebp)
0x0804844a <+15>: xor %eax,%eax
0x0804844c <+17>: sub $0xc,%esp
0x0804844f <+20>: push $0x8048530
0x08048454 <+25>: call 0x8048310 <puts@plt>
0x08048459 <+30>: add $0x10,%esp
0x0804845c <+33>: nop
0x0804845d <+34>: mov -0xc(%ebp),%eax
0x08048460 <+37>: xor %gs:0x14,%eax
0x08048467 <+44>: je 0x804846e <a+51>
0x08048469 <+46>: call 0x8048300 <__stack_chk_fail@plt>
0x0804846e <+51>: leave
0x0804846f <+52>: ret
End of assembler dump.
(gdb) si
0x08048447 in a ()
(gdb) p/x $eax
$2 = 0x9b291e00
(gdb) p/x *(0xf7fd5440 + 0x14)
$4 = 0x9b291e00