To The Point…er

There are many circumstances where we must dig into the network traffic generated by malware samples. And in lots of cases, we don’t have the benefit of the traffic being unencrypted. Perhaps you do not want, or cannot, run a MITM (man in the middle) proxy to handle this decryption for you. 

We recently began looking at one way to handle this. Enter Frida (https://frida.re), the “Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers”.  Without diving too deeply into Frida, it is an easy to use toolkit that allows for the injection of javascript in to applications running on many platforms with the express purpose of inspecting or modifying any aspect of the running program or its loaded libraries. That description doesn’t do justice to how awesome it can be, so you should definitely take a look at the documentation to get a better feel for what it can help you with. We will discuss how we used it to extract the SSL/TLS secrets from the underlying windows

As with any research project, we began to search for any information on this topic and to see if anyone had looked at this before. No point in recreating the wheel. We came across some amazing research into this topic by George Noseevich (@webpentest on twitter).  He provides 2 articles that go deep into the technical detail of how Windows generates the various components needed for encrypting TLS. (http://b.poc.fun/decrypting-schannel-tls-part-1/) . We attempted to use the script that he provided (https://github.com/sldlb/win-frida-scripts/tree/master/lsasslkeylog-easy) to dump out the data that we needed. We gave it a test run to see if it would provide us with the what we were looking for but ran in to an issue that required a bit of debugging. We were not getting the expected output. 

We began debugging the script by printing out the buf_type values to compare them against what we knew to be the correct ones defined in ncrypt.h.

#define NCRYPTBUFFER_SSL_CLIENT_RANDOM                      20
#define NCRYPTBUFFER_SSL_SERVER_RANDOM                      21
#define NCRYPTBUFFER_SSL_HIGHEST_VERSION                    22

We also leveraged the buf2hex function to print out the parameter list buffer for some inspection.  We noticed some very strange values being presented when trying to extract the server random values as noted in the image below.

A value of 20 aligns with what is expected to be the NCRYPTBUFFER_SSL_CLIENT_RANDOM value, but we expect to see a value of 21 for the NCRYPT_SSL_SERVER_RANDOM.  The buf_type of 15680304 is completely wrong and unexpected. While we were able to gather client random values, we still needed to figure out how to pull out the server random so we dug a bit more. We added a bit more debugging to the script and dumped the buf_buf value out immediately before the buf_type was printed to attempt to identify why we were seeing such odd values.  We struck gold with this by finding what appeared to be an offset issue when we noticed that the next buf_buf in the for loop appeared to be off by about 4 bytes.  This is apparent in the screenshot shown next.

What we ended up realizing was that the code actually does work, but only on 64 bit Windows.   Pointers on a 32 bit Windows are only 4 bytes whereas they are 8 bytes on 64 bit Windows systems that this code was developed on. The struct that George Noseevich was kind enough to place in the comments clearly defines it as 2 ULONG’s (4 bytes each)  as well as one pointer. 

                /*
                    typedef struct _NCryptBufferDesc {
                        ULONG         ulVersion;
                        ULONG         cBuffers;
                        PNCryptBuffer pBuffers;
                    } NCryptBufferDesc, *PNCryptBufferDesc;

                    typedef struct _NCryptBuffer {
                        ULONG cbBuffer;
                        ULONG BufferType;
                        PVOID pvBuffer;
                    } NCryptBuffer, *PNCryptBuffer;
                 */

While the hard coded value of 16 for the “var buf” variable creation will work on 64 bit windows, it did not for our 32 bit test system. A minor oversight on our part that lead us down a small rabbit hole, but ended up giving us a more portable piece of code in the long run. We added this simple method to return the needed offset:

import platform

def get_platform_arch():
    bitness = platform.architecture()[0]
    if bitness == '32bit':
        return 12 #sets var buf to the correct offset if 32 bit
    else:
        return 16 #sets var buf to the correct offset if 64 bit

Which was then dynamically inserted in to the frida script later:

var buf = buffers.add(""" + str(get_platform_arch()) + """*i);

Maybe this will help someone looking to do the same. Just make sure to pip install frida-tools and use the latest python version supported by Frida.

 

All third-party trademarks referenced by Cofense whether in logo form, name form or product form, or otherwise, remain the property of their respective holders, and use of these trademarks in no way indicates any relationship between Cofense and the holders of the trademarks.