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 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
)
)
}
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
)
}
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 ?? ?? ?? |
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
)
)
}
- PE Format - Win32 apps | Microsoft Learn
- A Warm Welcome to ASN.1 and DER - Let's Encrypt
- PE module — yara 4.3.0 documentation
- CyberChef's "Parse ASN.1 hex string" operation
If you find anything to correct or improve in these notes, feel free to comment below or contact me on Twitter.