Backwards compatibility is a hell of a drug

How to recover the private key of a (.pfx) certificate?

This post was most recently updated on January 22nd, 2021.

3 min read.

Recently, I ran into a problem that I definitely caused myself. I had a certificate, that was created to authenticate an app against Azure AD. The encryption key for the .pfx file was, however, lost.

I could just create a new certificate, configure the app service to use that and replace the old certificate… But that’s manual labor and where’s the fun in that?

I preferred a solution that included a bit more coding!

After a bit of googling, I found a nice, programmatic solution to simply brute-force the private key by trying every single key in existence. Not very elegant, but I suppose it should do the trick?

The bad thing was the sample code was fairly old – around 10 years old .NET 4 code, to be exact.

It still worked, however! The executable I compiled from the solution was able to try around 20k passwords per minute. That’s a bit low compared with the benchmarks the author posts (from 2012!), but I suppose my laptop’s single-core performance of unoptimized code (debug build) IS going to be fairly bad.

Anyway – I wanted to take a look at modernizing the app a little, since I already had the code on my machine and everything! 😅

The modernization process

It’s remarkable, how much of the .NET world is backwards compatible, but there are some rabbit holes you don’t want to dive into. One of them is the old .csproj -file format. As the old file looks somewhat like this:

This is the .csproj format shipped with... Visual Studio 2010? Or even earlier?
This is the .csproj format shipped with… Visual Studio 2010? Or even earlier?

There’s so much that would probably need to be changed to get this thing running in .NET Core or .NET5, and I don’t understand half of the properties. Hell, the format is likely years older than my career as .NET developer (I migrated from PHP and Java in 2011)!

So in short, I decided to nope that upgrade path out of the window and start with a fresh project file.

.NET5 supports Windows Forms, so simply creating a new WinForms project in .NET5 and copy-pasting the code was the easiest way to proceed.

Automatically generated Program.cs -file had one additional line compared to over 10 years old code :)
Automatically generated Program.cs -file had one additional line compared to over 10 years old code :)

The code itself is remarkably transparent and quite simple. And simply making it multithreaded took about half an hour of back-and-forth (because I’ll be damned if I remember how to do anything without fiddling with it for a while), and I ended up using Tasks (and ThreadPool) for multithreading – because it’s easy and I don’t need to worry about maximum number of threads, pooling, or actually pretty much anything at all.

So, this is what I added (and made the method async):

Task.Run(() => {
 if (TestPassword(pathToCertBx.Text, pw, logPathBx.Text, i))
 { password_found = true; }
});

I had to add or change 5 lines. Or 6, if you count adding a using-statement for System.Threading… 😅

Conclusion

After my code changes, the tool was able to try around 160-200k passwords per minute and was using around 50-75% of the CPU power of my machine with 20-30 threads. That’s an 8-10 times improvement, while still leaving the machine completely usable. Not insignificant, but also not quite good enough.

With a run-of-the-mill developer laptop, cracking a password of proper complexity will take months. And while a lot of people will use “aaaa” as their password for dev artifacts, I’m more of a “correct horse battery staple”* person – which means I’d be done quicker by creating a new certificate and delivering it to Microsoft’s Data Center in Dublin myself, even if I had to sneak over a few borders by foot.

Still, it was useful to see how much the cracking would be improved by simple tweaks to the code.

The code – both the original solution and the slightly modified version I created – are available in this GitHub repository:

https://github.com/koskila/CertificatePasswordRecovery

In the end, I created a new certificate and went on with the configuration of the app service – but this was an interesting detour for an hour or two :)

References & footnotes

* My actual passwords are machine-generated, this is a joke, don’t @ me.

mm
4.9 12 votes
Article Rating
Subscribe
Notify of
guest
9 Comments
most voted
newest oldest
Inline Feedbacks
View all comments