Sniffing SSL Connections in Memory
1. Introduction
Hey y’all! It’s been a while since I posted anything in my blog due to my busy work schedule, and just a teensy-weeny bit of burnout 🤪.
Today, I thought it would be interesting to share a simple technique to inspect SSL connections in a process’s memory. In particular, we will be targeting OpenSSL’s library functions, SSL_read() and SSL_write(), in order to read its plaintext buffer.
2. Preparing the Experiment
To demonstrate this technique, we will need to prepare a client and a server written in C. (See Listings 1 and 2)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define HOST "127.0.0.1"
#define PORT 4443
void init_openssl() {
SSL_load_error_strings();
OpenSSL_add_ssl_algorithms();
}
void cleanup_openssl() {
EVP_cleanup();
}
SSL_CTX *create_context() {
const SSL_METHOD *method;
SSL_CTX *ctx;
method = TLS_client_method();
ctx = SSL_CTX_new(method);
if (!ctx) {
perror("Unable to create SSL context");
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
return ctx;
}
int main() {
int sock;
struct sockaddr_in addr;
char buf[1024];
init_openssl();
SSL_CTX *ctx = create_context();
sock = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
inet_pton(AF_INET, HOST, &addr.sin_addr);
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, sock);
if (SSL_connect(ssl) <= 0) {
ERR_print_errors_fp(stderr);
} else {
const char *msg = "Hello from client!";
SSL_write(ssl, msg, strlen(msg));
int bytes = SSL_read(ssl, buf, sizeof(buf));
buf[bytes] = 0;
printf("Received: %s\n", buf);
}
SSL_free(ssl);
close(sock);
SSL_CTX_free(ctx);
cleanup_openssl();
return 0;
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define PORT 4443
void init_openssl() {
SSL_load_error_strings();
OpenSSL_add_ssl_algorithms();
}
void cleanup_openssl() {
EVP_cleanup();
}
SSL_CTX *create_context() {
const SSL_METHOD *method;
SSL_CTX *ctx;
method = TLS_server_method(); // for TLS server
ctx = SSL_CTX_new(method);
if (!ctx) {
perror("Unable to create SSL context");
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
return ctx;
}
void configure_context(SSL_CTX *ctx) {
// Load certificate and private key
if (SSL_CTX_use_certificate_file(ctx, "cert.pem", SSL_FILETYPE_PEM) <= 0 ||
SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
}
int main() {
int sock;
struct sockaddr_in addr;
char buf[1024];
int bytes;
init_openssl();
SSL_CTX *ctx = create_context();
configure_context(ctx);
sock = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sock, (struct sockaddr*)&addr, sizeof(addr));
listen(sock, 1);
printf("Server listening on port %d...\n", PORT);
struct sockaddr_in client_addr;
uint len = sizeof(client_addr);
int client = accept(sock, (struct sockaddr*)&client_addr, &len);
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, client);
if (SSL_accept(ssl) <= 0) {
ERR_print_errors_fp(stderr);
} else {
bytes = SSL_read(ssl, buf, sizeof(buf));
buf[bytes] = 0;
printf("Received: %s\n", buf);
SSL_write(ssl, buf, strlen(buf)); // echo back
}
SSL_free(ssl);
close(client);
close(sock);
SSL_CTX_free(ctx);
cleanup_openssl();
return 0;
}
The server will also require a private key and a certificate generated with openssl. (See Listing 3)
$ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
Next, install the OpenSSL development libraries before compiling the client and server. (See Listing 4)
$ sudo apt install libssl-dev
Lastly, compile both the client and the server. (See Listing 5)
$ gcc ssl_server.c -o ssl_server -lssl -lcrypto
$ gcc ssl_client.c -o ssl_client -lssl -lcrypto
Now, run the server before the client. If everything is working as intended, the server will receive the client’s message and echo it back to the client.

3. Inspecting Buffer in Client
To capture the plaintext buffer before it is encrypted, we just have to print the buffer right at the start of SSL_write()! Simple, isn’t it?
Start the server first, then attach to the client using gdb. (See Listing 6)
$ gdb ssl_client
Next, we will set a breakpoint at the start of SSL_write before running the binary. (See Listing 7)
(gdb) b SSL_write
(gdb) run
Once the breakpoint is hit, print the value of the second argument to SSL_write(). In my case, I am using the x86_64 architecture in Linux and therefore, the 2nd argument of SSL_write() refers to the RSI register. (See Listing 8)
(gdb) info registers rsi
rsi 0x82205b 8527963
The value of RSI shown in the previous listing refers to the address of the buffer / the pointer to the string in plaintext. Inspecting the plaintext buffer is straightforward from here. (See Listing 9)
(gdb) x/s $rsi
0x82205b: "Hello from client!"
4. Inspecting Buffer in Server
Now that we’ve captured the plaintext message on the client side, let’s do the same on the server. This time, however, we will require two breakpoints within SSL_read().
Firstly, attach to the server with gdb. (See Listing 10)
$ gdb ssl_server
Set a breakpoint at the start of SSL_read() before running the server. (See Listing 11)
(gdb) b SSL_read
(gdb) run
Upon hitting the breakpoint (by running our client), we save the address stored in the RSI register for later reference. (See Listing 12)
# Hit Breakpoint 1 (SSL_read)
(gdb) set $buf = $rsi
Since the plaintext will be written to the buffer at the end of SSL_read(), we set a breakpoint at the ret instruction. Thus, we need to find the offset where this instruction occurs by reading its disassembly. In my case, this occurs at SSL_read + 74. (See Listing 13)
(gdb) disass SSL_read
...
0x00007ffff7f3a5fa <+74>: ret
...
After setting a 2nd breakpoint on the return instruction, resume the execution of the server. (See Listing 14)
(gdb) b *(SSL_read + 74)
(gdb) continue
When we hit our 2nd breakpoint, we can read the plaintext string from our buffer address saved earlier. (See Listing 15)
# Hit Breakpoint 2 (SSL_read + 74)
(gdb) x/s $buf
0x7fffffffd860: "Hello from client!"
5. Conclusion
In short, you’ve learned how to inspect TLS messages by setting appropriate breakpoints in SSL_read() and SSL_write(). I sure hope this short exercise has been fun and enlightening! Keep reading and stay curious.