Monday, 30 March 2026

Blog 3 - Windows Vulnerability Research - Certificates and Asn.1

Disclaimer: This blog series won't be very technical. It is intended to be easy to understand and follow. Like most of my blogs, I have no reason to be peacocking with technical jargon like all the infosec losers.

Overview and donation information: https://weirdquadratic.blogspot.com/p/blog-overview.html

To the reader: Feel free to share anything I write with those who need it. And be sure to make local copies as I am sure it is a matter of time until this blog will be taken down as well.

Introduction

In the previous blog we found an integer overflow by providing custom user mapping data via the Supplemental Data handshake message. Which allowed us to overflow an unsigned short length field which was passed to a memcpy() call and subsequently set a field in a structure that is then passed as an argument for a LsaLogonUser() call. However, to get this deep into the code, we had to use a valid client certificate. The authenticated attack surface is still interesting, as it could allow us to perform a domain level privilege escalation attack. However, the true doomsday-type bugs are found in the fully unauthenticated part. Lets begin by digging deeper into certificates and what we can do with them.

Enabling pageheap and application verifier

For any target you are researching, at a minimum you should have pageheap enabled.

Both application verifier and pageheap can be enabled with the gflags application.

https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/gflags

You can download gflags as part of the windows debugging tools suite, which comes bundled with either the Windows SDK or WDK.

https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools

Pageheap has all different sorts of tricks to detect heap corruptions. And application verifier can help to find a whole range of other issues. You can find plenty of information about this online, hence I won't cover it here.

One thing to keep in mind is that Pageheap adds a "fill pattern" (for example 0xc0c0c0c0) in different places. One being uninitialized heap memory. This means that if you spot code using a page heap fill pattern in your debugger, it likely means it copied uninitialized data (or read outside a bound) somewhere. Hence it is important to keep an eye out for these.

Install gflags on your Windows Server VM and then launch gflags and enable pageheap and application verifier for lsass.exe using the GUI (you can use command line too):

Next be sure to restart your VM.

A closer look at certificates

If we run the proof of concept from the last blog (https://github.com/BigPolarBear1/Blog/blob/main/Blog%202/tls_integer_truncation.py)
and capture the result with Wireshark we will see the following TLS handshake messages:

The main code that routes all these handshake messages to the correct function and manages the overall state for TLS 1.2 can be found in the following function:

CSsl3TlsServerContext::ProcessHandshake

You can read up more about the TLS handshake online. I won't spent too much time on it. In the above Wireshark capture you see two "Certificate" handshake messages. First we have one send by the server after the Server Hello message. This is simply the certificate associated with the website. A browser can use this to verify it is talking to the correct website and traffic isn't being intercepted and/or modified.

Further down after the Supplemental Data message we see another Certificate message. This is the certificate send by the client for client authentication. While TLS mutual authentication (the server requesting a client certificate) is usually only used to protect access to resources, as seen commonly in corporate environments, its use is still widespread enough to be of interest to an attacker. Especially since protecting a resource with mutual authentication usually implies that the resource being protected has some importance and thus value to an attacker.

The initial parsing of a user supplied certificate is done in CSsl3TlsContext::DigestRemoteCertificate

Opening schannel.dll in Binary Ninja and navigating to CSsl3TlsServerContext::ProcessHandshake we can see where this function is called:

Lets put a breakpoint on CSsl3TlsContext::DigestRemoteCertificate, run the PoC from previous blog and create a function trace.

First place a breakpoint in your debugger (see blog 1 and 2 on setting up a debugger using either windbg or binja):

bp CSsl3TlsContext::DigestRemoteCertificate

Run the PoC from previous blog from your Windows Enterprise machine and make sure the IP at the bottom of the PoC is correct:

python tls_integer_truncation.py

After the breakpoint hits, run the following command to create a function trace:

wt -i ntdll -i KERNELBASE -i CRYPT32 -i verifier

This should yield a function trace similar to the one below:

If you omit i -CRYPT32 this trace ends up being much larger and we will also see all the ASN.1 parsing related function calls:

ASN.1 is a standard for encoding and decoding data. I highly recommend the following introduction on ASN.1: https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/

Looking at the above function traces, we can somewhat make sense of what is happening by just looking at the symbolic names. It is basically going to perform ASN.1 decoding of all the different certificate fields and copy the decoded information over to an internal object in memory. Which then gets used later in DoCertificateMapping() and all the functions we saw in the previous blog. Knowing where all these fields are populated is important, hence why I quickly mentioned it.

Signature Algorithms

Looking at the earlier Wireshark capture again. After the Certificate message send by the client, the client also sends a Certificate Verify message.

A beautiful property of asymmetric cryptography is the fact that having just a public key, using mathematical properties, one can verify, if a signature is indeed created by someone who possesses the matching private key, without the verifier having knowledge of this private key.

This is what Certificate Verify does. As defined by the following RFC: https://datatracker.ietf.org/doc/html/rfc5246#page-62

Looking at the client certificate being sent in the Certificate message in Wireshark, we see it is comprised of 4 sub fields:

The signedCertificate field, is also commonly known as the TBSCertificate field (TBS = to be signed).

Next we see the algorithmIdentifier field, this is basically information about the algorithm used to create the signature.

Ignore the padding field, this is not commonly used I believe. This does have the interesting property that it does not affect the signature. And seems to be a common mechanism for hiding exploit payloads.

And at the bottom we have the encrypted field. Which is the signature itself.

The way a signature is created is very simple. It creates a hash of all the fields inside SignedCertificate, using the hashing algorithm indicated by the algorithmIdentifier field.
Next it encrypts the generated hash using the private key (which must be the private key matching the public key listed inside SignedCertificate) and this result ends up being the encrypted field.

This encrypted field is then used during the Certificate Verify step of the TLS handshake to make sure that the certificate is signed using the correct private key and in addition it is also used to guarantee integrity of all the data inside "signedCertificate" to make sure no modifications have occurred. The actual math behind digital signature algorithms is very cool, but a little out of scope for these blogs. But perhaps in the future it could be a cool topic to write some python and do a deep dive into all this math (in a way that makes sense to non-math folks or amateur math enthusiasts such as myself).

Next we will write some code that lets us create a custom "signedCertificate" at the byte level and use this blob of bytes to generate a signature for the final certificate. The problem with many public APIs like OpenSsl is that it is very difficult to create signed certificates using malformed data, but as a security researcher, this capability is a must have. Luckily the python cryptography module exposes all the functionality we need.

By signing our custom certificates correctly, it should allow us to get past the certificate verification step and call into DoCertificateMapping(), while being able to modify all the individual fields of a certificate at the byte level.

Asn.1 and generating custom certificates

I here present a script, to create certificates at the byte level and then sign them:

https://github.com/BigPolarBear1/Blog/blob/main/Blog%203/buildcert.py

The code that represents the certificate as a tree data structure is some old code I had laying around from long gone glory days.. which I quickly repurposed for this blog.
For using the cryptography python module to then self sign the generated certificate, surprisingly the free version of Claude was able to spit out the correct code for this.
While trivial to just google it myself, that did surprise me and it is probably the first time in my career AI turned out to be of any use at all. But again, when I then tried to have it create math related code, it completely missed the mark. It appears AI is very strong when there are plenty of examples online, and you can even see exactly which code it scraped from the internet is being used.
However when it comes to a truly niche topic with very little public code samples.. it just produces hallucinated garbage. Which was quite a relief to see actually.
Yes, AI can save time if you use it as a glorified search engine.. but aside from that, I don't see much utility in it and I honestly hate it and will never use it aside from quickly looking up very basic things when I'm too lazy to google it myself.

Lets quickly go over the code an explain what happens:

The very first function call is to buildcert().

sng_cert=Asnc("sgnd_cert") is the parent node of the tree data structure. Then it defines all the other nodes in the tree and defines their parent node.

Scrolling down in buildcert() we then see calls to set_data. This sets the data for each node and also defines the asn.1 type. One important thing to note here are the following lines:

This reads the public key from a certificate encoded as a pem file and returns a byte object to be embedded into the certificate we are generating with our code. We do this because the public key here must match with the private key it is signed with else the verification step will fail and we'll never be able to hit DoCertificateMapping().

The naming conventions used are not great as when I wrote this, I had not intended to ever share it. But shortly after I wrote it my entire life collapsed and I never really finished doing what I had intended to do with this project. But we are slowly working toward that now, and eventually you will see what the plan here was :).

Then next we have a call to calculatelength() and build().
Calculatelength() will basically propagate all the individual length fields of each node in the tree and adjust the length fields of all the parent nodes. Then in build() we flatten everything into a byte object. It is very simple.

What this also allows us to do is fuzz for example at both the node and data level. This was my initial motivation to represent it like this. But now I am sharing it, so feel free to cause mayhem. Anything related to asn.1 parsing is usually an interesting target to fuzz.

Lastly, we will sign it using our private key, this adds the Encrypted field and AlgorithmIdentifier field, discussed earlier, to the certificate and we save the completed certificate to disk.

Bringing everything together

To build a certificate and use it with our PoC from the previous blog, follow these steps:

Step 1:

In buildcert.py change the following fields:

certificatepath="certificate.pem"
privitekeypath="private.key"
outputname="blah.der"

I have used the same key and certificate from the client certificate we generated in the previous blog. You can use an arbitrary key and certificate to self-sign, but do keep in mind that certificate chain building will fail then.

Step 2:

run: python buildcert.py

Modify the proof of concept from the previous blog (https://github.com/BigPolarBear1/Blog/blob/main/Blog%203/tls_integer_truncation.py)
And make sure we point it to the certificate we just generated:

t = TLSClientAutomaton(server="172.26.178.47", dport=443, version="tls12",server_name="bear.com",mycert="blah.der",mykey="private.key")

In addition, make sure the IP is set to the IP of the windows server VM.

Step 3:

run: python tls_integer_truncation.py

If all goes well, the TLS handshake should succeed, meaning we succesfully performed client authentication. When recording the traffic with Wireshark, we will see our bogus issuer appear which we have set in buildcert.py (if not something went wrong):

The next blog

We'll stop here. The main purpose of this blog was showing how we can modify and send custom self-signed certificates that we can modify at the byte level. In the next blog, we will use this as a tool to find some bugs in certificate chain building related code. 

No comments:

Post a Comment

Note: only a member of this blog may post a comment.