Enforcing SSL CA verification

Overview

I started this project with a friend of mine as a school project for a security course during my last year of undergrad. The project is called Certificate Authority Verification (CAV). It is a tool that allows you to intercept the certificate verification process of any non-browser application using OpenSSL as a shared library. Interception is made possible by building CAV as a shared object and using Linux’s LD_PRELOAD environment variable to override the certificate verification functionality of OpenSSL and enforce verification against a trusted store of your choice.

NOTE: I'm no C guru or SSL for that matter, I'm experimenting with this stuff and CAV is not something that should be trusted

Motivation

SSL is heavily used in providing secure communication over the Internet. Certificate verification is one of the main components in SSL. Ensuring the correctness of the verification process ensures the security of SSL. Therefore, knowing how to properly use SSL and verify certificates is an essential part in establishing secure SSL connections. The problem with SSL is that most software developers misunderstand it and use it improperly (myself included), which then leads to vulnerability to MITM attacks. This post is inspired by two academic papers "The Most Dangerous Code in the World: Validating SSL Certificates in Non-Browser Software" and "Securing SSL Certificate Verification through Dynamic Linking".

Intercepting dynamically linked libraries

For any dynamically linked library, you can override its functionality using the Linux LD_PRELOAD environment variables.

The first step to override an existing library functionality is building you own library, as a shared object, that contains the same symbols (function name) of the existing dynamically linked library. Then you set the LD_PRELOAD environment variables to contain your library:

export LD_PRELOAD=/absolute/path/to/liboverride.so:$LD_PRELOAD

Setting LD_PRELOAD to contain your library means that the dynamic linked/loader will load your liboverride.so before all other libraries. Therefore, the symbols that you override will be invoked first. That is all you need to know about LD_PRELOAD to understand this blog post. If you're interested in it, you can learn more about it here.

Looking up symbol names

The LD_PRELOAD environment variable allows you to override a function completely. However, I just want to intercept it, verify the presented certificate chain, and then perform the equivalent of invoking super() to return execution to the original function. The second part of the puzzle is the piece of code that allows me to return execution to the original function. For that, we use C's dlsym().

The function dlsym() allows you to lookup a symbol in a open, or to be opened, object file based on the string name of the symbol. You can tell the compiler to open the object file libx.so at run-time and invoke function hello_world from it. Taken from The Open Group docs, here's an example of how to use dlsym().

void *handle;
int (*fptr)(int), *iptr, result;

/* open the needed symbol table */
handle = dlopen("/usr/home/me/libfoo.so", RTLD_LOCAL | RTLD_LAZY);

/* find the address of the function my_function */
fptr = (int (*)(int))dlsym(handle, "my_function");

/* find the address of the data object my_object */
iptr = (int *)dlsym(handle, "my_OBJ");

/* invoke my_function, passing the value of my_OBJ as the parameter */
result = (*fptr)(*iptr);

The core of CAV

In my case, the open object file is already a dynamically linked library. Therefore, I don't have to invoke dlopen() on a given object, because it is already open. Here my snippet of the code that hijacks OpenSSL verification:

/*
*   These typedefs just point to aliases with function types and arguments
*   identical to the functions being hijacked.
*/
typedef long (*orig_SSL_get_verify_result_f_type)(const SSL *ssl);
typedef int (*orig_SSL_connect_f_type)(SSL *s);

long SSL_get_verify_result(const SSL *ssl) {

    DEBUG_PRINT("%s\n","Hijacked");
    int err = 0;

    if (0 != (err = verify_cert(ssl))) {
        return err;
    } else {
        DEBUG_PRINT("%s\n","Return execution to OpenSSL");

        /*  Equavilent of saying of:
        *     SSL * orig_SSL_get_verify_result;
        */
        orig_SSL_get_verify_result_f_type orig_SSL_get_verify_result;

        /*  Equavilent of saying of:
        *     long (*orig_SSL_get_verify_result)(const SSL *ssl);
        *     orig_SSL_get_verify_result = &SSL_get_verify_result;
        */
        orig_SSL_get_verify_result =
            (orig_SSL_get_verify_result_f_type)dlsym(RTLD_NEXT,"SSL_get_verify_result");

        return orig_SSL_get_verify_result(ssl);
    }
}

The above code does three things:

  1. Redefines the original function SSL_get_verify_result()
  2. Enforce certificate verification by calling verify_cert(ssl)
  3. Resolve the original function pointer orig_SSL_get_verify_result(ssl) and call it

You can also see that the code fails SSL_get_verify_result() if it does not pass the custom checking verify_cert(ssl). One other thing I should mention here is the RTLD_NEXT handle used in finding the original function symbol. RTLD_NEXT tells your program to look for the specified symbol in the next object in the load order. Since we used LD_PRELOAD to give the library libcav.so higher load order, we use RTLD_NEXT to look for the symbol in the next object file that contains the symbol. That is, ignore the symbol defined in the current object and look for the symbol in the next object that contains the symbol name.

CAV Verification

CAV verifies the peer certificates based on a configurable on-disk trusted store. CAV looks for a $HOME/.cavrc file that contains the path to your trusted store. Here is what the file should look like:

CA_FILE /path/to/trusted/certificate/file
CA_DIR /path/to/trusted/certificate/directory
LOG /path/to/log/file

The value of CA_DIR could be something like /etc/ssl/certs on Ubuntu or any specific path that contains PEM certificate files you trust. If you build CAV and set the LD_PRELOAD environment variable to contain libcav.so, then CAV will try to look for the $HOME/.cavrc file and load your trusted store and verify the peer certificate chain against it.

How does CAV verify the peer's certificate? Here's the body of the verify_cert() function:

int verify_cert(const SSL *s) {

    init_config_file();
    int err = 0;

    /* Find the peer certificate chain */
    STACK_OF(X509) * sk = SSL_get_peer_cert_chain(s);

    if (NULL == sk) {
        DEBUG_PRINT("%s\n", "Certificate chain is not available");
        return (err = -1);
    } else {
        DEBUG_PRINT("%s\n", "Found peer certificate chain");
    }

    if (0 != (err = verify_X509_cert_chain(sk))) {
        DEBUG_PRINT("%s\n", "Failed to verify X509 certificate chain");
        return (err = -1);
    }

    DEBUG_PRINT("%s\n", "Successfully verified X509 certificate chain");

    return 0;
}

The above code shows that you can get the certificate chain, including the user certificate, from the SSL connection object. After you get the certificate stack sk, verify_X509_cert_chain() iterates over all certificates and loads an SSL store in memory to check each certificate against the loaded store. The loaded store is the result of loading the CA_DIR path taken from $HOME/.cavrc.

If you're interested in trying out CAV, you can view the source here. If you have Vagrant, you don't have to setup the project. You can just vagrant up && vagrant ssh and starting hacking it. I've put instructions on how to setup the project and see a demo of CAV in the project readme.