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;
}
Simple SSL Client - ssl_client.c

#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;
}
Simple SSL Server - ssl_server.c

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
Generating OpenSSL Private Key and Certificate

Next, install the OpenSSL development libraries before compiling the client and server. (See Listing 4)

$ sudo apt install libssl-dev
Installing OpenSSL Development Library

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
Compiling SSL Client and Server

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.

simple_test

Server and Client SSL Message Exchange

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
Attach to ssl_client with gdb

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
Setting Breakpoint @ SSL_write()

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
Inspecting Value of RSI Register

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!"
Printing Plaintext Buffer

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
Attach GDB to ssl_server

Set a breakpoint at the start of SSL_read() before running the server. (See Listing 11)

(gdb) b SSL_read
(gdb) run
Setting Breakpoint @ SSL_read()

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
Saving Buffer Address for Reference

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
...
Finding Offset of Return Instruction

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
Setting Breakpoint @ SSL_read() + 74 & Resuming Execution

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!"
Reading Plaintext Buffer Reference

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.

6. Resources

  1. libssl-example.zip