Skip to content

Mitigations (ASLR, NX, etc.)

Rahul Sridhar edited this page Oct 14, 2018 · 3 revisions

Back in the mid 90's, when Aleph One first published "Smashing the Stack for Fun and Profit", binary exploitation was pretty easy. (This is of course neglecting the fact that the knowledge of how to actually do it wasn't as widespread or refined as it is now). You could pretty much disassemble any executable, find a gets or a strcpy without a bounds check and get root via shellcode. But, as is always the case in this field of ours, defensive measures were eventually developed and implemented. Modern binary exploitation is comparatively more difficult. This page will go through some of the more common defense measures and talk about how to get around them.

Detecting defenses w/ checksec

The right way to check for defenses on a binary is to use checksec.sh, a nifty little shell script that tells you everything. checksec now comes installed with pwntools so you should be able to use it immediately by just typing checksec <binary>.

[~]> checksec leakRop
[*] '/home/fortenforge/chals/rop/leakRop/leakRop'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[~]>

Note that checksec will not inform you about ASLR, because that protection is a property of the machine and not the binary. To check if ASLR is turned on on a machine, run:

cat /proc/sys/kernel/randomize_va_space

If you see a 0, then ASLR is turned off. If you see a 1 or a 2, then ASLR is on. In a CTF, where you don't have access to the remote machine, organizers will often explicitly tell you whether ASLR is on or off in the challenge description. (Another sign is if they provide you with the shared libraries that the binary is linked with; this is a good indication that ASLR is on).

NX (aka DEP)

In a standard shellcode attack, you write assembly into a buffer on the stack and overwrite the return address with the address of the buffer so that the instruction pointer starts executing instructions off of the stack (remember, usually the instruction pointer is confined to the .text region of memory).

An easy defense is to mark the stack as not executable (NX) so that if the instruction pointer is ever set to point to the stack, the processor will refuse to execute instructions and will just segfault. A less-discussed feature of NX is that it also allows you to set regions of memory as non-writable, so usually the stack is marked writable but not executable and the .text section is marked as executable but not writable.

This is all implemented with some page-table tomfoolery. Note that NX is referred to as "Data Execution Prevention" (DEP) on Windows.

How do you get around NX? In a word, ROP. Note that if you can make a syscall using ROP, you can even call mprotect and make the stack executable again.

Most compilers turn on NX by default. In order to compile a binary with it off pass the -z execstack flag to gcc / clang.

Stack Canaries (aka Stack Cookies)

Stack Canaries are random, integer values that the compiler will place in memory just before the return address on the stack for certain functions. Before the function returns, it will check if the canary's value has changed, and will immediately exit instead of returning, preventing any control over the return address.

Below is an example of a stack canary inserted by gcc: note that the canary's value is read from the fs segment into [rbp-0x8] and then checked at the end of the function.

00000000000006da <main>:
 6da:	55                   	push   rbp
 6db:	48 89 e5             	mov    rbp,rsp
 6de:	48 83 ec 20          	sub    rsp,0x20
 6e2:	64 48 8b 04 25 28 00 	mov    rax,QWORD PTR fs:0x28
 6e9:	00 00 
 6eb:	48 89 45 f8          	mov    QWORD PTR [rbp-0x8],rax
 6ef:	31 c0                	xor    eax,eax
                     <snip>
 70e:	48 8b 55 f8          	mov    rdx,QWORD PTR [rbp-0x8]
 712:	64 48 33 14 25 28 00 	xor    rdx,QWORD PTR fs:0x28
 719:	00 00 
 71b:	74 05                	je     722 <main+0x48>
 71d:	e8 7e fe ff ff       	call   5a0 <__stack_chk_fail@plt>
 722:	c9                   	leave  
 723:	c3                   	ret

Stack canary values are generally random with a last byte of 0x00 to terminate strings early. To bypass stack canaries, you generally have to figure out how to leak their value. (In some rare cases, you may be able to exploit a vulnerability in the stack canary-checking assembly to cause a leak).

Most compilers turn on stack canaries by default. In order to compile a binary with them, pass the -fno-stack-canary flag to gcc / clang.

ASLR

ASLR stands for Address Space Layout Randomization. It does exactly what it says it does: it randomizes the layout of the process's address space. Generally, the last word and first byte of an address will remain the same (although this various from platform to platform). Also, not all parts of the address space will be randomized:

Section ?
Stack yes
Heap yes
Text no
PLT / GOT no
data / bss no
static library no
shared library yes

With ASLR on (and without an information leak), you can only use ROP gadgets from the text section and any statically linked libraries. This makes ROP much harder. If you can bypass ASLR (usually by leaking a pointer from a shared library), you can then mine gadgets from all of libc, and usually jump straight to one_gadget to win.

PIE

PIE stands for "position-independent executable". It was created after ASLR, and it essentially allows ASLR to be more effective by allowing more parts of the address space to be randomized. All shared libraries are position-independent by definition, since they need to work regardless of where the linker decides to place them in memory. PIE on a non-library allows the TEXT section to be randomized. PIE also randomizes the location of the GOT, making GOT overwrites / reads much trickier to pull off.

You can turn PIE on by passing the -fPIE flag to gcc.

RELRO

NX mitigates shellcode, ASLR and PIE mitigate ROP, and RELRO, which stands for "Relocation Read Only" mitigates the underrated GOT Overwrite attack. It comes in two flavors. Partial RELRO, marks the non-PLT GOT as read-only and does a couple other things, but is mostly useless. Full RELRO actually marks the GOT for the PLT as read-only (forcing the linker to fully populate the GOT before the program begins execution), and basically prevents a GOT overwrite.

You can read more about the PLT and GOT here.

You can turn on full RELRO with -Wl,-z,relro,-z,now and partial RELRO with gcc -Wl,-z,relro.