Closed Bug 2030102 Opened 5 months ago Closed 4 months ago

Heap-use-after-free of sslSessionID in [@ ssl_FreeLockedSID] via dangling ss->sec.ci.sid left by ssl_BeginClientHandshake on OOM

Categories

(NSS :: Libraries, defect, P3)

Tracking

(nss 3.125, firefox-esr115 wontfix, firefox-esr140 affected, firefox151 wontfix, firefox152 wontfix, firefox153 fixed)

RESOLVED FIXED
Tracking Status
nss --- 3.125
firefox-esr115 --- wontfix
firefox-esr140 --- affected
firefox151 --- wontfix
firefox152 --- wontfix
firefox153 --- fixed

People

(Reporter: bugmon, Assigned: jschanck)

Details

(5 keywords, Whiteboard: [adv-main153+r])

Attachments

(8 files)

In NSS libssl, ssl_BeginClientHandshake() (security/nss/lib/ssl/sslcon.c) mishandles an externally-cached session ID when its TLS version no longer falls within the socket's enabled range. When a client has installed a resumption token via the public SSL_SetResumptionToken() API, ss->sec.ci.sid holds the only reference (refcount 1, cached == in_external_cache). ssl_BeginClientHandshake() aliases that pointer into a local without adding a reference, and on version mismatch calls ssl_FreeSID() at line 167, which destroys and frees the heap object — but ss->sec.ci.sid is never cleared.

Immediately afterwards the function calls ssl3_NewSessionID(). If that allocation fails (PORT_ZAlloc returning NULL under memory pressure), control jumps to the loser: label and the function returns SECFailure with ss->sec.ci.sid still pointing into freed heap. When the application subsequently closes the socket (or calls SSL_ResetHandshake), ssl_DestroySecurityInfo() → ssl_ResetSecurityInfo() calls ssl_FreeSID(sec->ci.sid) on the dangling pointer, reading and decrementing sid->references in freed memory. If the freed slot has been reallocated, this becomes an arbitrary-object decrement followed by a potential second ssl_DestroySID/PORT_ZFree — i.e., a double-free of an attacker-influenceable 416-byte chunk.

The bug is reachable through documented public NSS client APIs in a plausible sequence (set external resumption token → narrow SSL_VersionRangeSet → SSL_ForceHandshake → PR_Close). It is gated on a single small-allocation failure during the handshake, which an attacker can only influence indirectly (e.g., by driving the client toward heap exhaustion). The fix is trivial: set ss->sec.ci.sid = NULL immediately after the ssl_FreeSID() at sslcon.c:167.

Build Info

Affected Code

File: security/nss/lib/ssl/sslcon.c, line 151-179

/* If there's an sid set from an external cache, use it. */
if (ss->sec.ci.sid && ss->sec.ci.sid->cached == in_external_cache) {
    sid = ss->sec.ci.sid;          /* alias only; refcount stays 1 */
    SSL_TRC(3, ("%d: SSL[%d]: using external token", SSL_GETPID(), ss->fd));
} else if (!ss->opt.noCache) {
    sid = ssl_LookupSID(ssl_Time(ss), &ss->sec.ci.peer,
                        ss->sec.ci.port, ss->peerID, ss->url);
}

if (sid) {
    if (sid->version >= ss->vrange.min && sid->version <= ss->vrange.max) {
        PORT_Assert(!ss->sec.localCert);
        ss->sec.localCert = CERT_DupCertificate(sid->localCert);
    } else {
        ssl_UncacheSessionID(ss);
        ssl_FreeSID(sid);          /* refcount 1->0 -> ssl_DestroySID frees heap.
                                    * BUG: ss->sec.ci.sid is NOT cleared and now dangles. */
        sid = NULL;
    }
}
if (!sid) {
    sid = ssl3_NewSessionID(ss, PR_FALSE);
    if (!sid) {
        goto loser;                /* returns SECFailure with ss->sec.ci.sid dangling */
    }
    sid->u.ssl3.keys.resumable = PR_FALSE;
}
ss->sec.ci.sid = sid;              /* only reached on success */

File: security/nss/lib/ssl/sslsecur.c, line 665-668

/* cleanup the ci */
if (sec->ci.sid != NULL) {
    ssl_FreeSID(sec->ci.sid);   /* UAF: sec->ci.sid points to freed sslSessionID */
}

File: security/nss/lib/ssl/sslnonce.c, line 216-222

static void
ssl_FreeLockedSID(sslSessionID *sid)
{
    PORT_Assert(sid->references >= 1);   /* <-- ASAN: READ of size 4 in freed heap */
    if (--sid->references == 0) {
        ssl_DestroySID(sid, PR_TRUE);     /* potential double-free */
    }
}

File: security/nss/lib/ssl/sslsock.c, line 4613-4643

sid = ssl3_NewSessionID(ss, PR_FALSE);   /* refcount = 1 */
...
/* Use the sid->cached as marker that this is from an external cache and
 * we don't have to look up anything in the NSS internal cache. */
sid->cached = in_external_cache;
sid->lastAccessTime = ssl_Time(ss);

ss->sec.ci.sid = sid;                    /* sole owner; no extra ref */

SSLExp_SetResumptionToken stores a refcount-1 sslSessionID with cached==in_external_cache into ss->sec.ci.sid. ssl_BeginClientHandshake aliases it without adding a reference; on version-range mismatch it frees the only reference (line 167) but leaves ss->sec.ci.sid pointing at freed memory. The subsequent ssl3_NewSessionID failure path (goto loser) returns without overwriting ss->sec.ci.sid, so ssl_ResetSecurityInfo later dereferences and re-frees the dangling pointer.

Exploit Chain

  1. Application creates an NSS TLS client socket and installs an externally-cached resumption token via SSL_SetResumptionToken(); this sets ss->sec.ci.sid (refcount 1, cached=in_external_cache) with the token's negotiated TLS version (e.g., TLS 1.2).
  2. Application narrows the socket's enabled versions via SSL_VersionRangeSet() so that the stored sid->version is outside [vrange.min, vrange.max] (e.g., restricts to TLS 1.3 only after a configuration update).
  3. Application initiates the handshake (SSL_ForceHandshake / first read/write). ssl_Do1stHandshake → ssl_BeginClientHandshake takes the in_external_cache branch, detects the version mismatch, and calls ssl_FreeSID(sid) at sslcon.c:167. The 416-byte sslSessionID is destroyed and freed; ss->sec.ci.sid is left dangling.
  4. Under memory pressure, the immediately following ssl3_NewSessionID() → PORT_ZAlloc(sizeof(sslSessionID)) fails and returns NULL. ssl_BeginClientHandshake jumps to loser: and returns SECFailure without touching ss->sec.ci.sid.
  5. (Optional) Attacker-influenced heap activity reallocates the freed 416-byte slot with controlled contents.
  6. Application closes the socket (PR_Close) or calls SSL_ResetHandshake. ssl_DestroySecurityInfo → ssl_ResetSecurityInfo calls ssl_FreeSID(sec->ci.sid) on the dangling pointer.
  7. ssl_FreeLockedSID reads and decrements sid->references in freed/reallocated memory (use-after-free). If the field reads as 1, ssl_DestroySID runs again on the reclaimed object → double-free / arbitrary-pointer frees of fields inside the reclaimed chunk, providing a heap-corruption primitive.

Steps to Reproduce

  1. Apply the test-only allocation-fault helper to gtests/ssl_gtest/libssl_internals.c (SSLInt_FailNextZAllocOfSize / SSLInt_SizeofSessionID and a one-shot PORT_ZAlloc_Util shadow) and add gtests/ssl_gtest/ssl_sslcon_unittest.cc with TlsConnectTest.BeginClientHandshakeExternalSidVersionMismatchUAF; register both in ssl_gtest.gyp and manifest.mn.
  2. Build NSS with ASAN: cd /firefox/security/nss && ./build.sh --asan (or use the clauditor build_nss helper with firefox_dir=/firefox).
  3. Run: /firefox/security/dist/Debug/bin/ssl_gtest --gtest_filter=TlsConnectTest.BeginClientHandshakeExternalSidVersionMismatchUAF -d /firefox/security/tests_results/security/localhost/ssl_gtests
  4. Observe AddressSanitizer: heap-use-after-free in ssl_FreeLockedSID (sslnonce.c:219), freed at ssl_BeginClientHandshake (sslcon.c:167), allocated at SSLExp_SetResumptionToken (sslsock.c:4613).

Security Impact

  • Severity: Moderate
  • Attacker capability: Heap use-after-free of a 416-byte sslSessionID followed by a refcount decrement and potential double-free / ssl_DestroySID on a reclaimed object. If an attacker can groom the freed slot before PR_Close, this yields a write-what-where-lite (decrement at fixed offset) and a second free of attacker-chosen sub-pointers, which can be leveraged toward memory corruption / code execution in the embedding process.
  • Preconditions: Embedding application must use the NSS external resumption-token client API (SSL_SetResumptionToken) and subsequently narrow SSL_VersionRangeSet so the stored token's version is excluded before the handshake; AND a single ~416-byte heap allocation (PORT_ZAlloc in ssl3_NewSessionID) must fail at that moment. The allocation failure is not directly attacker-controlled, only indirectly influenceable via memory pressure, hence OOM-gated.

ASAN Report

==25463==ERROR: AddressSanitizer: heap-use-after-free on address 0x5140010f484c at pc 0x5b610aeed673 bp 0x7ffd3dd2cf90 sp 0x7ffd3dd2cf80
READ of size 4 at 0x5140010f484c thread T0
    #0 0x5b610aeed672 in ssl_FreeLockedSID ../../lib/ssl/sslnonce.c:219
    #1 0x5b610aeed72e in ssl_FreeSID ../../lib/ssl/sslnonce.c:236
    #2 0x5b610aef9919 in ssl_ResetSecurityInfo ../../lib/ssl/sslsecur.c:667
    #3 0x5b610aef99d9 in ssl_DestroySecurityInfo ../../lib/ssl/sslsecur.c:683
    #4 0x5b610af0a064 in ssl_DestroySocketContents ../../lib/ssl/sslsock.c:478
    #5 0x5b610af0a4a7 in ssl_FreeSocket ../../lib/ssl/sslsock.c:535
    #6 0x5b610aee6a16 in ssl_DefClose ../../lib/ssl/ssldef.c:221
    #7 0x5b610aef9fd3 in ssl_SecureClose ../../lib/ssl/sslsecur.c:748
    #8 0x5b610af1617a in ssl_Close ../../lib/ssl/sslsock.c:3229
    #9 0x7eae42c84043 in PR_Close ../../../../pr/src/io/priometh.c:95
    ...
    #20 0x5b610aa92af3 in nss_test::TlsConnectTest_BeginClientHandshakeExternalSidVersionMismatchUAF_Test::TestBody() ../../gtests/ssl_gtest/ssl_sslcon_unittest.cc:88

0x5140010f484c is located 12 bytes inside of 416-byte region [0x5140010f4840,0x5140010f49e0)
freed by thread T0 here:
    #0 0x7eae431564d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x7eae42ca2839 in PR_Free ../../../../pr/src/malloc/prmem.c:449
    #2 0x7eae43004e87 in PORT_ZFree_Util ../../lib/util/secport.c:182
    #3 0x5b610aeed624 in ssl_DestroySID ../../lib/ssl/sslnonce.c:205
    #4 0x5b610aeed703 in ssl_FreeLockedSID ../../lib/ssl/sslnonce.c:221
    #5 0x5b610aeed72e in ssl_FreeSID ../../lib/ssl/sslnonce.c:236
    #6 0x5b610af8356f in ssl_BeginClientHandshake ../../lib/ssl/sslcon.c:167
    #7 0x5b610aef692d in ssl_Do1stHandshake ../../lib/ssl/sslsecur.c:43
    #8 0x5b610aef88e9 in SSL_ForceHandshake ../../lib/ssl/sslsecur.c:433
    #9 0x5b610aa9289c in nss_test::TlsConnectTest_BeginClientHandshakeExternalSidVersionMismatchUAF_Test::TestBody() ../../gtests/ssl_gtest/ssl_sslcon_unittest.cc:82

previously allocated by thread T0 here:
    #0 0x7eae43157340 in calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:77
    #1 0x7eae42ca2773 in PR_Calloc ../../../../pr/src/malloc/prmem.c:434
    #2 0x5b610a33c76f in PORT_ZAlloc_Util ../../gtests/ssl_gtest/libssl_internals.c:644
    #3 0x5b610aea67db in ssl3_NewSessionID ../../lib/ssl/ssl3con.c:8644
    #4 0x5b610af1ceb3 in SSLExp_SetResumptionToken ../../lib/ssl/sslsock.c:4613
    #5 0x5b610aa92135 in nss_test::TlsConnectTest_BeginClientHandshakeExternalSidVersionMismatchUAF_Test::TestBody() ../../gtests/ssl_gtest/ssl_sslcon_unittest.cc:66

SUMMARY: AddressSanitizer: heap-use-after-free ../../lib/ssl/sslnonce.c:219 in ssl_FreeLockedSID
==25463==ABORTING
Attached file ssl_sslcon_unittest.cc —
Attached patch manifest.mn.diff — — Splinter Review
Attached patch fix.patch — — Splinter Review
Attached file crash_stack.txt —
Group: core-security → crypto-core-security
Severity: -- → S3
Status: UNCONFIRMED → NEW
Ever confirmed: true
Priority: -- → P3
Attached file (secure) —
Assignee: nobody → jschanck
Status: NEW → ASSIGNED

Pushed by jschanck@mozilla.com:
https://hg.mozilla.org/projects/nss/rev/a2b4ae220df6
avoid leaving a dangling ss->sec.ci.sid on allocation failure. r=nss-reviewers,keeler

Status: ASSIGNED → RESOLVED
Closed: 4 months ago
Resolution: --- → FIXED
Group: crypto-core-security → core-security-release
status-nss: --- → 3.125
Whiteboard: [adv-main153+r]
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: