Skip to content

Instantly share code, notes, and snippets.

@gregclermont
Last active April 2, 2023 09:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gregclermont/32b5e3d09d78662b97e6b0a5885208d8 to your computer and use it in GitHub Desktop.
Save gregclermont/32b5e3d09d78662b97e6b0a5885208d8 to your computer and use it in GitHub Desktop.
import "pe"
rule ms13_098 {
condition:
pe.is_dll() and filesize < 10MB and pe.data_directories[
pe.IMAGE_DIRECTORY_ENTRY_SECURITY].size > 0x8000
and (
(uint16be(pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_SECURITY].virtual_address+8) == 0x3082
and uint16be(pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_SECURITY].virtual_address+10) <
pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_SECURITY].size - 2000) or
(uint16be(pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_SECURITY].virtual_address+8) == 0x3083
and (65536 * uint8(pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_SECURITY].virtual_address+10)
+ uint16be(pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_SECURITY].virtual_address+11)) <
pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_SECURITY].size - 2000)
)
}

How the rule works

In a Twitter conversation about the use of SigFlip in the 3CX supply-chain attack, @med0x2e mentionned that a YARA rule existed to detect files modified using this technique (MS13-098 / CVE-2013-3900).

As I didn't understand well how the rule worked, I took some time to explore the underlying mechanisms, and learned a ton along the way.

The rule was created and shared by Adrien B (@Int2e_):

This #YARA rule detects malware abusing MS13-098. It looks for at least 2000 bytes in PE signature padding. This was used by APT actors lately to hide some of their payloads in legit signed DLLs... by default Windows reports these as valid... There are a few false positives :)

— Adrien B (@Int2e_) November 23, 2020

The Attribute Certificate Table

The rule makes several references to pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_SECURITY].
This corresponds to the Attribute Certificate Table of the PE file, which contains the Authenticode signature.

If we simplify the rule by abbreviating this expression as SIGNATURE, it looks like this:

// SIGNATURE = pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_SECURITY]
rule {
    condition:
        pe.is_dll()
    and filesize < 10MB
    and SIGNATURE.size > 0x8000
    and (
            (
                uint16be(SIGNATURE.virtual_address+8) == 0x3082
            and uint16be(SIGNATURE.virtual_address+10) < SIGNATURE.size - 2000
            )
         or (
                uint16be(SIGNATURE.virtual_address+8) == 0x3083
            and (65536 * uint8(SIGNATURE.virtual_address+10) + uint16be(SIGNATURE.virtual_address+11)) < SIGNATURE.size - 2000
            )
        )
}

Structure of a certificate entry

We can now see that the rule handles two cases, depending on the value of uint16be(SIGNATURE.virtual_address+8).
The specification details that each certificate entry contains the following fields:

Offset Size Field Description
0 4 dwLength Specifies the length of the attribute certificate entry.
4 2 wRevision Contains the certificate version number.
6 2 wCertificateType Specifies the type of content in bCertificate.
8 - bCertificate Contains a certificate, such as an Authenticode signature.

This indicates that SIGNATURE.virtual_address+8 corresponds to the bCertificate field, that contains the certificate itself. In the case of an Authenticode signature, this field specifically follows a structure called SignedData.

We will abbreviate this in the rule by SIGNEDDATA_ADDR:

// SIGNATURE       = pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_SECURITY]
// SIGNEDDATA_ADDR = SIGNATURE.virtual_address+8
rule {
    condition:
        // ...
            (
                uint16be(SIGNEDDATA_ADDR) == 0x3082
            and uint16be(SIGNEDDATA_ADDR + 2) < SIGNATURE.size - 2000
            )
         or (
                uint16be(SIGNEDDATA_ADDR) == 0x3083
            and (65536 * uint8(SIGNEDDATA_ADDR + 2) + uint16be(SIGNEDDATA_ADDR + 3)) < SIGNATURE.size - 2000
            )
}

PKCS #7 and ASN.1

The Authenticode SignedData structure is encoded using the PKCS #7 format, which itself relies on the ASN.1 standard.

ASN.1 is a language used to define data structures. The SignedData type is specified like this:

SignedData ::= SEQUENCE {
    version CMSVersion,
    digestAlgorithms DigestAlgorithmIdentifiers,
    encapContentInfo EncapsulatedContentInfo,
    certificates [0] IMPLICIT CertificateSet OPTIONAL,
    crls [1] IMPLICIT RevocationInfoChoices OPTIONAL,
    signerInfos SignerInfos
}

ASN.1 structures are serialized using a type-length-value encoding.

type length value
13 05 68 65 6c 6c 6f

The first byte encodes the type. In this case, 0x13 corresponds to the type PrintableString.
The second byte encodes the length of the value. In this case, 5 bytes.
The value in this example is thus decoded as the PrintableString 68 65 6c 6c 6f, which corresponds to the string hello!

In this example, the length is encoded on a single byte. That means that it could be at most 0xFF = 255. What if the value longer than that?

This encoding has a way to write the length field over several bytes:

  • If the highest bit of the length byte is 0, then the 7 remaining bits contain the length of the value.
  • However, if the highest bit is 1, then the 7 remaining bits indicate the number of bytes used to represent the actual length of the value.

Here are some examples:

length binary encoding hexadecimal
1 00000001 01
127 01111111 7F
128 10000001 10000000 81 80
255 10000001 11111111 81 FF
256 10000002 00000001 00000000 82 01 00
65535 10000002 11111111 11111111 82 FF FF
65536 10000003 00000001 00000000 00000000 83 01 00 00

Now, we can go back to the SignedData structure. In the PKCS #7 format, it is defined as a SEQUENCE.
The type byte for a SEQUENCE is 0x30. The type-length serialization for SEQUENCEs of various length will be as follows:

SEQUENCE length type-length encoding
1-127 30 ??
128-255 30 81 ??
256-65535 30 82 ?? ??
65536-16777215 30 83 ?? ?? ??

SignedData size

We now have enough to understand the rest of the YARA rule.

uint16be(SIGNEDDATA_ADDR) is a way to read the first two bytes of SignedData. This means that the two cases of the rule depend of the size of SignedData:

  • the first case (0x3082) is when it is between 256 and 65,535 bytes (size encoded on 2 bytes)
  • the second case (0x3083) is when it is between 65,536 and 16,777,215 bytes (size encoded on 3 bytes)

uint16be(SIGNEDDATA_ADDR + 2) then skips the first two bytes to read the actual encoded size.
We will abbreviate this expression as SIGNEDDATA_SIZE_2B in the rule.

(65536 * uint8(SIGNEDDATA_ADDR + 2) + uint16be(SIGNEDDATA_ADDR + 3)) does two reads: the first byte, then the last two. The two values are then combined to produce the total encoded size.
We will abbreviate this expression as SIGNEDDATA_SIZE_3B in the rule.

// SIGNATURE          = pe.data_directories[pe.IMAGE_DIRECTORY_ENTRY_SECURITY]
// SIGNEDDATA_ADDR    = SIGNATURE.virtual_address+8
// SIGNEDDATA_SIZE_2B = uint16be(SIGNEDDATA_ADDR + 2)
// SIGNEDDATA_SIZE_3B = (65536 * uint8(SIGNEDDATA_ADDR + 2) + uint16be(SIGNEDDATA_ADDR + 3))
rule {
    condition:
        pe.is_dll()
    and filesize < 10MB
    and SIGNATURE.size > 0x8000
    and (
            (   // the length of SignedData is encoded on 2 bytes
                uint16be(SIGNEDDATA_ADDR) == 0x3082
                // there is more than 2000 bytes of padding after SignedData
            and SIGNEDDATA_SIZE_2B < SIGNATURE.size - 2000
            )
         or (   // the length of SignedData is encoded on 3 bytes
                uint16be(SIGNEDDATA_ADDR) == 0x3083
                // there is more than 2000 bytes of padding after SignedData
            and SIGNEDDATA_SIZE_3B < SIGNATURE.size - 2000
            )
        )
}

References


If you find anything to correct or improve in these notes, feel free to comment below or contact me on Twitter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment