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:
- Redefines the original function
SSL_get_verify_result()
- Enforce certificate verification by calling
verify_cert(ssl)
- 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.