On Mon, 2022-07-04 at 14:04 +0200, Ansgar wrote:
> On Sun, 19 Jun 2022 12:59:55 +0200 Ben Hutchings wrote:
> > > I'm now looking at whether the missing bytes are recoverable (e.g. are
> > > they always zeroes).
> > [...]
> > 
> > I wrote a script to try all possible byte values for 2 bytes before or
> > after the short signature.  For this particular file, none of them
> > producd a valid signature.  So the short signatures seem to be
> > corrupted in a more complex way.
> 
> The "OCTET STRING" containing the actual signature is shorter for the
> seemingly corrupted signatures:
[...]

Yes I know, and my script uses a library to manipulate the ASN.1
structure when adding bytes.  I'm attaching the script, so you can
check the logic.

Ben.

-- 
Ben Hutchings
Any smoothly functioning technology is indistinguishable
from a rigged demo.
#!/usr/bin/python3

# Fix broken detached signatures

import os.path
import sys

import asn1crypto.algos
import asn1crypto.cms
import asn1crypto.core

from M2Crypto import BIO, SMIME, X509


# Signature algorithm should be RSA
SIG_ALGO_OID = asn1crypto.core.ObjectIdentifier('1.2.840.113549.1.1.1')


# Signature length should match key length (2048 bits)
SIG_LEN = 2048 // 8


def make_smime_context(cert):
    # We don't verify against a CA, just one specific certificate
    smime = SMIME.SMIME()
    signer_key = X509.X509_Stack()
    signer_key.push(X509.load_cert(cert))
    smime.set_x509_stack(signer_key)
    smime.set_x509_store(X509.X509_Store())
    return smime


def verify_payload(smime, sig, data):
    smime.verify(
        SMIME.load_pkcs7_bio_der(BIO.MemoryBuffer(sig.dump(force=True))),
        BIO.MemoryBuffer(data),
        flags=(SMIME.PKCS7_BINARY | SMIME.PKCS7_DETACHED
               | SMIME.PKCS7_NOVERIFY))


def brute_force_sig_2bytes(smime, sig, sig_os, data):
    orig_raw_sig = bytes(sig_os)
    for byte1 in range(256):
        for byte2 in range(256):
            sig_os.set(bytes((byte1, byte2)) + orig_raw_sig)
            try:
                verify_payload(smime, sig, data)
            except Exception:
                pass
            else:
                print(f'prepended {byte1}, {byte2} to start of sig')
                return True

            sig_os.set(bytes((byte1,)) + orig_raw_sig + bytes((byte2,)))
            try:
                verify_payload(smime, sig, data)
            except Exception:
                pass
            else:
                print(f'added {byte1}, {byte2} at start and end of sig resp.')
                return True

            sig_os.set(orig_raw_sig + bytes((byte1, byte2)))
            try:
                verify_payload(smime, sig, data)
            except Exception:
                pass
            else:
                print(f'appended {byte1}, {byte2} to end of sig')
                return True

    return False


def fix_detached_sig(smime, sig, bin_name):
    # The ContentInfo should be a SEQUENCE with signed data at index 1
    if len(sig) < 2 or not isinstance(sig[1], asn1crypto.cms.SignedData):
        return 'no signed data found', False
    sd = sig[1]

    # The SignedData should be a SEQUENCE with signer infos at index 5
    if len(sd) < 6 or not isinstance(sd[5], asn1crypto.cms.SignerInfos):
        return 'no signer infos found', False
    infos = sd[5]

    # The SignerInfos should be a SET with 1 item
    if len(infos) != 1:
        return f'found { len(infos) } signer infos; expected 1', False
    info = infos[0]

    # The SignerInfo should be a SEQUENCE with the signature algorithm
    # at index 4 and signature at index 5
    if (len(info) < 6
        or not isinstance(info[4], asn1crypto.algos.SignedDigestAlgorithm)
        or len(info[4]) < 1
        or not isinstance(info[5], asn1crypto.core.OctetString)):
        return 'expected fields not found in signer info', False

    # Check the signature algorithm and length (see bug #1012741)
    if info[4][0] != SIG_ALGO_OID:
        return f'unexpected signature algorithm { info[4][0].dotted }', False
    actual_sig_len = len(bytes(info[5]))

    with open(bin_name, 'rb') as f:
        data = f.read()

    if (actual_sig_len == SIG_LEN - 2
        and brute_force_sig_2bytes(smime, sig, info[5], bin_name)):
        return (f'signature length is { actual_sig_len } bytes;'
                f' expected { SIG_LEN }; filled in missing 2 bytes',
                True)
    
    if actual_sig_len != SIG_LEN:
        return (f'signature length is { actual_sig_len } bytes;'
                f' expected { SIG_LEN }',
                False)

    verify_payload(smime, sig, data)
    return None, False


def load_detached_sig(name):
    with open(name, 'rb') as f:
        return asn1crypto.cms.ContentInfo.load(f.read())


def save_detached_sig(sig, name):
    with open(name, 'wb') as f:
        f.write(sig.dump())


def main(sig_dir, bin_dir, cert):
    err_count = 0

    smime = make_smime_context(cert)

    for dirpath, dirnames, filenames in os.walk(sig_dir):
        for name in filenames:
            sig_name = os.path.join(dirpath, name)

            # We can only fix module signatures, because hashes on
            # PE executables need to be calculated differently
            if not sig_name.endswith('.ko.sig'):
                print(f'{sig_name}: ignoring, no .ko.sig extension')
                continue

            bin_name = os.path.join(
                bin_dir, os.path.relpath(sig_name[:-4], sig_dir))

            try:
                sig = load_detached_sig(sig_name)
            except Exception as e:
                err, fixed = str(e), False
            else:
                err, fixed = fix_detached_sig(smime, sig, bin_name)
            if err:
                print(f'{name}: {err}', file=sys.stderr)
                if fixed:
                    save_detached_sig(sig, sig_name)
                else:
                    err_count += 1

    return err_count


if __name__ == '__main__':
    sys.exit(main(*sys.argv[1:]) != 0)

Attachment: signature.asc
Description: This is a digitally signed message part

Reply via email to