Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PE signature validation doesn't detect malware signed with sigthief #1071

Closed
mosse-security opened this issue Jun 3, 2019 · 17 comments
Closed

Comments

@mosse-security
Copy link

mosse-security commented Jun 3, 2019

PE signature validation doesn't detect malware signed with Sigthief.

YARA Rule:

import "pe"

rule fea_pe_improperly_signed {
  condition:
    uint16(0) == 0x5A4D and pe.number_of_signatures > 0

    and not for all i in (0..pe.number_of_signatures - 1):
    (
      pe.signatures[i].valid_on(pe.timestamp)
    )
}

How to reproduce:

1 - Sign Mimikatz using Sigthief
2 - Run the rule provided above against the binary

Expected Results:

YARA reports the binary to be improperly signed.

Current Results:

YARA reports the signature as valid. Probably because the timestamp is valid.

Risk:

It's very likely that dozens of security professionals are using the valid_on() function to try and validate whether a PE is properly signed or not.

Recommendations:

We recommend that the documentation should specify any of the limitations for valid_on(). Also, if YARA could have a way to formally validate whether a binary is properly signed or not, that would be very handy.

@wxsBSD
Copy link
Collaborator

wxsBSD commented Jun 3, 2019

Do you have a hash of a sample which triggers this?

@mosse-security
Copy link
Author

Here's a Mimikatz sample where a Microsoft signature has been applied using Sigthief:

https://www.virustotal.com/gui/file/cb9e7cae11788bb2cd3a41536ec072e89c0aa691d396a9d24b1d8ccc4418a638/detection

@mosse-security
Copy link
Author

This may be of interest to you too:

>c:\yara\yara64.exe -s -w -D c:\yara\debug.yara e308e12336fb5d097c9512d8f48b2d86
pe
        number_of_signatures = 1
        signatures
                [0]
                        thumbprint = "9dc17888b5cfad98b3cb35c1994e96227f061675"
                        issuer = "/C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=Microsoft Code Signing PCA"
                        subject = "/C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=Microsoft Corporation"
                        version = 3
                        algorithm = "sha1WithRSAEncryption"
                        serial = "33:00:00:01:b1:dd:ed:ba:54:e9:65:b8:5f:00:01:00:00:01:b1"
                        not_before = 1531426279
                        not_after = 1564171879

@wxsBSD
Copy link
Collaborator

wxsBSD commented Jun 4, 2019

Thanks! I’ll take a look in a couple of hours.

@wxsBSD
Copy link
Collaborator

wxsBSD commented Jun 4, 2019

Unless I'm not understanding this properly, there is no bug here. The timestamp on the file is 1544396227, which is clearly within the not_before and not_after range, so the loop condition evaluates to true, but then you are negating the whole thing and your rule ends up not matching.

Is your issue that the signature doesn't match the binary? If so, that is entirely out of the scope of YARA signature parsing. YARA is currently extracting signature data and exposing it so you can write more precise and expressive rules. At no point has YARA claimed to validate any signatures, which might be where your confusion is coming from. The valid_on() function is only checking if the argument provided is within the range specified on the signature (https://yara.readthedocs.io/en/v3.8.1/modules/pe.html#c.signatures.valid_on) and nothing else.

You're not the first to bring up the point of validation of signatures though. It's something that could be added but I've not done it yet because if YARA tells you the signature is valid I'm concerned people will take it to mean "this is trustworthy", which is clearly not the case. The question of trustworthiness is up to the user and their OS, or whatever is providing the trusted certificate store and it also changes over time as certificates are added or removed from the trusted certificate store.

There is an argument to be made that we could maybe have a pe.signatures[i].valid() which just verifies the signature matches what YARA calculates it should be, but that we should make it very clear that it doesn't imply any validation up the chain of trust. I'm open to considering that, and possibly writing it, but I think @plusvic should chime in here.

@mosse-security
Copy link
Author

Hi wxsBSD!

First and foremost, thank you for looking into this quickly and being opened to the discussion.

Based on your response, it seems that we are on the same page :)

I'll summarise my position in case that helps in any way:

(a) YARA would do well to update the documentation to clearly inform the reader that valid_on() does not validate whether the file is properly signed or not.

I've come across several 3rd party YARA rules that seem to have misunderstood this method. It's obviously not YARA's fault, but it's still something where you guys can make a positive difference with very little effort and there seems to be a case for taking action.

(b) There's an opportunity for YARA to help the community by offering a way to validate whether a file is properly signed or not, in a similar manner that sigcheck.exe offers.

Numerous security analysts and security products use digital signatures to inform whether a file can be more trusted or not, hence this feature could prove very helpful and popular with the community.

Also, it would allow analysts to run searches such as "I'm looking for binaries that are signed with a certificate that looks like it comes from Microsoft, but the signature doesn't match the file".

If you think that you could benefit from an additional engineer to help with this task, or other tasks, please ping me privately as we may be able to provide somebody pro-bono later in the year.

@plusvic
Copy link
Member

plusvic commented Jun 5, 2019

The actual verification of PE signatures, going up the signing chain up to a root certificate trusted by the operating system is a major undertake. In Windows you can simply use the WinVerifyTrust API, but in other operating systems you need to implement all that logic by yourself, far from easy. Also, as @wxsBSD mentioned the trustworthiness of a file is ultimately determined by the certificate store in your OS, so the same PE file could be "trusted" by one system and "not trusted" by some other, with the same YARA rule yielding different results. So, I wouldn't implement a full verification process like sigcheck.exe does (in fact sigcheck.exe uses WinVerifyTrust under the hood, that's why porting sigcheck.exe to some other OS is not straightfoward).

Having said that, we still can consider the pe.signatures[i].valid() function that @wxsBSD said, as in "lets verify that the hash in this signature matches the actual file", with no further attempt to guarantee that the signer is actually who it claims to be. However, my impression is that without actually validating the certificate chain this is like a glorified checksum. Anyone wanting to pass a file as signed by Microsoft could generate a "valid" signature where the hashes are correct, and you won't know that it's a fake signature until you try to validate the chain.

@wxsBSD
Copy link
Collaborator

wxsBSD commented Jun 5, 2019

That’s a good point about it being a glorified checksum that I had not considered. Thanks for pointing it out!

@hugmyndakassi
Copy link

@plusvic I saw #1623 and hope that it gets merged. It would also address this issue.

However, my impression is that without actually validating the certificate chain this is like a glorified checksum.

Ignoring the terminology (hash vs. checksum) for the moment, it's about making sure that the signed message has not been tampered with, no? The signature certifies that the signed hash (etc.) describes content provided by the signer (as far as the collision resistance of the used hash algo goes, that is). The signer requires a secret key to sign the content and the certificate details are "just" embedded to tie it to some external trusted entity (usually a CA that has verified that the signer is who s/he claims to be).

For example there is no technical difference between a "standard" class 3 certificate and an EV certificate. The differences arise from the certificate chain, EKUs set and the OIDs. In a sense these are "logical" differences (just like the judgment whether a particular certificate chain terminates validly).

Anyone wanting to pass a file as signed by Microsoft could generate a "valid" signature where the hashes are correct, and you won't know that it's a fake signature until you try to validate the chain.

How? Maybe you could explain this a little further, I don't quite get it. Are you suggesting that collision attacks have become feasible with SHA-256?

There should be only two ways to create a valid signature:

  1. you have the certificate and a private key and are therefore the legitimate signer
  2. you try to create a collision by mutating/forging the input (mathematically very unlikely)

Clearly 1. is out (or irrelevant, as we're talking forged signatures). So your claim seems to be that someone is able to take the PE file and replace it by another PE file (minus PE checksum and minus the security directory size and RVA/offset) that will give the exact same hash, right?

But a signature can be validated without taking the certificate chain into account. If the hash was tampered with, it won't validate. So the only loophole here is for an attacker to forge a PE file that will give the exact same hash that is already imbued in the signature.

So if I use SigThief I can only ever fool very superficial checks like "is there a security directory in this PE image?" or "is the signature of this hash valid?". Neither is sufficient on their own. Code that validates the signature on a given PE would have to (i) compute the hash over the PE image, then (ii) validate the signature on its own and then (iii) tie it together by checking that the computed hash matches the one contained in the signature (i.e. of what has been signed). And only then it's more than a "glorified checksum". But none of this needs to involve the certificate chain.

I agree that checking against a certificate chain is probably outside of what Yara could ever hope to offer across all supported target systems, but that would only be the cherry on the top.

As a side-note: ELAM drivers can be additionally signed by self-signed certificates which are then later taken into account when protecting user mode processes. In these cases the signatures need to be valid, too, but there isn't even a certificate chain available to check against.

@HoundThe
Copy link
Contributor

Ad questions about

Anyone wanting to pass a file as signed by Microsoft could generate a "valid" signature where the hashes are correct, and you won't know that it's a fake signature until you try to validate the chain.

In the previous comment.

I think it meant, that anyone can compute the correct hashes and make a ''valid'' signature out of them. Now how do you know the signature is trustworthy? You validate the certificate chain up to a point where you reach a point that is already trusted, some root certificate on your system. Now because this is unfeasible to implement within YARA, anyone can create their own fake chain (that they have keys for), bypassing the checks, as the hashes - signature match. And you can't prove that the chain is not trustworthy.

@plusvic
Copy link
Member

plusvic commented Apr 13, 2022

The previous comment went unnoticed to me, so I didn't respond earlier, but the answer from @HoundThe is exactly what I meant.

@hugmyndakassi
Copy link

hugmyndakassi commented Apr 13, 2022

I think it meant, that anyone can compute the correct hashes and make a ''valid''
signature out of them.

What, how? A different hash invalidates the signature. And to create a new signature you need the private key matching your certificate. So catch 22, right?

Please enlighten me. I must be missing some revolutionary new method here how this can be faked. You claim to be able to make valid (or "valid" ... not sure what the distinction is) signatures out of them. I wonder how, in absence of a private key!?

If you switch out the hash for another (because you modified the PE file), the signature will no longer validate. And that can indeed be done without any knowledge about the certificate chain. Just by knowing the public key to validate the signature (well, technically it's a little more involved).

Now, let's take the quoted statement from @plusvic into account:

Anyone wanting to pass a file as signed by Microsoft could generate a "valid" signature where the hashes are correct, and you won't know that it's a fake signature until you try to validate the chain.

If you manage to create a PE file whose contents will yield the exact same hash we all are indeed in trouble. Actually cryptography as a whole and large parts of contemporary digital infrastructure are in big trouble then. It either requires a successful preimage attack or a successful collision attack on SHA-2 with 256 bit digests. You'd be sure to get some attention if you successfully mounted one of those.

That's a huge honking if, though. In fact that if is so big (and the chances of it happening soooo tiny) that we are betting on it practically never happening, which is why we decided SHA-2 with 256 bit digests is secure enough for the time being.

If you don't get the exact same hash, though? Sorry: that makes the signature invalid. So no fake signatures today.

Now how do you know the signature is trustworthy?

You seem to be conflating two concepts. One is the validity of the signature, the other is trust. And the validity can be checked by purely technical means and cannot be easily faked (again, unless I am missing something crucial and/or revolutionary here).

  1. computing the hash over the PE image
  2. checking if the hash matches the signed hash
  3. validating the signature

... before you haven't done all of these three things, you needn't worry about any details of the certificate or trust chains. And we're talking about point 3 which is currently missing.

You validate the certificate chain up to a point where you reach a point that is already trusted, some root certificate on your system.

No, trust can equally be gained from knowing the public key of the signer. Or really whatever trait (or better yet: traits) you decide. There is no need to involve any CA, even. Just because WinVerifyTrust() does it one way and Microsoft only lets particular CAs into that club, doesn't mean that this Microsoft-y concept of trust is the only permissible one.

I can very much decide to trust my own self-signed certificate and happily use that to code-sign software not made by myself. But it makes a whole lot of a difference if the signature is valid or not prior to attempting to establish trust with some CAs.

The previous comment went unnoticed to me, so I didn't respond earlier, but the answer from @HoundThe is exactly what I meant.

@plusvic but the argument so far makes no real sense as long as we are talking about Authenticode signatures.

  • The message that gets signed for a PE file is its hash (with the known gaps during computation)
  • The signature requires possession of the private key and a matching certificate to create
    • The signature can be validated solely with the knowledge of the public key
    • The certificate (enclosed) contains the public key, subject name etc., issuer, validity period and some more "bureaucratic" stuff

Please explain to me in steps how you can fake valid signatures? Show me a paper describing it or an actual attack that does it.

Sure, I could create my own self-signed CA, then issue a certificate that names Microsoft or whatever. However signatures made with that certificate will still require a.) a private key and matching certificate to be created and b.) can still be valid or invalid in the sense described by the initial post.

Essentially your claim seems to be that you can transplant a signature from a Microsoft-signed file to another file, re-compute the hash ... switch it out in the signature and be done with it. Well, I claim that the signature will not validate after such tampering.

valid != trusted

I think that's the core misconception here.

But there is very much value in knowing if a signature is at least valid as long as I have access to the thumbprint of the certificate or the public key value of the signer's key pair. You can leave the decision about trust to the users.

That's exactly why I brought up the example of the ELAM drivers. The respective signatures are made with self-signed certificates. No CA (or if, then a self-signed not publicly recognized one) involved. However, the binaries are typically also validly signed as far as WinVerifyTrust() is concerned. This document refers to this as secondary signature. But that secondary signature can be valid or invalid as far as the signed message (or rather its digest) is concerned, despite not being trusted by Windows.

@HoundThe
Copy link
Contributor

What, how? A different hash invalidates the signature. And to create a new signature you need the private key matching your certificate. So catch 22, right?

The point was, that without validating the chain, you don't have any trust in the owner of the keys, meaning that you can modify the file, compute a new signature and sign it yourself with your made-up certificates.

Of course, I agree that checking for signature validity - if everything matches - is a good thing, that is why the PR exists. But I think the original point was, that a valid signature doesn't mean anything without a trust anchor. But it is of course better than being invalid in the first place.

So as you mentioned in the end - valid != trusted. That's why I said you can create a valid signature because you can create corresponding key pairs and certificates such that the certificate is valid, but not trustworthy.

No, trust can equally be gained from knowing the public key of the signer. Or really whatever trait (or better yet: traits) you decide. There is no need to involve any CA, even. Just because WinVerifyTrust() does it one way and Microsoft only lets particular CAs into that club, doesn't mean that this Microsoft-y concept of trust is the only permissible one.

Yes, the root certificate was just an example, you can obviously have different points of trust if you want.

Essentially your claim seems to be that you can transplant a signature from a Microsoft-signed file to another file, re-compute the hash ... switch it out in the signature, and be done with it. Well, I claim that the signature will not validate after such tampering.

No, I claim I can switch out the Authenticode, and modify the hashes, the signatures, and the corresponding certificate chain. Then it would validate as the signatures are verified with the public key of the signer and its trust is determined with a chain of trust. But again, manipulating the chain would probably result in not a very trustworthy signature, but a valid one.

@hugmyndakassi
Copy link

hugmyndakassi commented Apr 13, 2022

But again, manipulating the chain would probably result in not a very trustworthy signature, but a valid one.

Indeed. And I think this data point of being a valid or invalid signature alone is a valuable piece of information. And without validating the signature, any further details about the certificate can't be taken serious.

I agree in all other points. Does #1657 #1623 offer this kind of validation, @HoundThe ?

@HoundThe
Copy link
Contributor

I am not familiar with the PR you linked, maybe you meant #1623? That would offer this kind of validation.

@hugmyndakassi
Copy link

Thanks, yes I meant #1623. Sorry. Great to hear.

@plusvic
Copy link
Member

plusvic commented Aug 10, 2024

Closing because this was implemented long ago.

@plusvic plusvic closed this as completed Aug 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants