Skip to content

Use-after-free and type confusion in newPySSLSocket() when SSL_new() fails #144833

Description

@raminfp

Bug report

Bug description:

Modules/_ssl.c:920-921 When SSL_new() returns NULL, Py_DECREF(self) frees the PySSLSocket object, then get_state_ctx(self) dereferences the freed pointer with a wrong type cast (PySSLSocket* PySSLContext*).

Bug Location

Modules/_ssl.c, function newPySSLSocket(), lines 919-922:

if (self->ssl == NULL) {
    Py_DECREF(self);                                                // frees self 
    _setSSLError(get_state_ctx(self), NULL, 0, __FILE__, __LINE__); // UAF
    return NULL;
}

Root Cause

Two bugs in one line:

  1. UAF: Py_DECREF(self) drops refcount to 0, PySSL_dealloc() and PyObject_GC_Del(self) frees memory. Next line reads freed memory.

  2. Type Confusion: get_state_ctx() is defined as (((PySSLContext *)(c))->state) (_ssl.h:52). self is PySSLSocket*, not PySSLContext*. The state field offset in PySSLContext exceeds PySSLSocket size, out-of-bounds read.

Trigger Condition

SSL_new() returns NULL . Reachable from Python via SSLContext.wrap_bio() or SSLContext.wrap_socket().

Patch

--- a/Modules/_ssl.c
+++ b/Modules/_ssl.c
@@ -917,8 +917,8 @@
     self->ssl = SSL_new(ctx);
     PySSL_END_ALLOW_THREADS(sslctx)
     if (self->ssl == NULL) {
+        _setSSLError(get_state_ctx(sslctx), NULL, 0, __FILE__, __LINE__);
         Py_DECREF(self);
-        _setSSLError(get_state_ctx(self), NULL, 0, __FILE__, __LINE__);
         return NULL;
     }

Changes: self to sslctx, error call moved before Py_DECREF.

Reproduction

Build CPython with ASAN

./configure --with-address-sanitizer --with-pydebug
make -j$(nproc)

Hook: force SSL_new() to return NULL (ssl_new_hook.c)

#define _GNU_SOURCE
#include <stdio.h>
typedef struct ssl_st SSL;
typedef struct ssl_ctx_st SSL_CTX;

SSL *SSL_new(SSL_CTX *ctx)
{
    fprintf(stderr, "[HOOK] SSL_new(%p) returning NULL\n", (void *)ctx);
    return NULL;
}
gcc -shared -fPIC -o ssl_new_hook.so ssl_new_hook.c

Trigger (trigger_uaf.py)

import ssl
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
incoming = ssl.MemoryBIO()
outgoing = ssl.MemoryBIO()
ctx.wrap_bio(incoming, outgoing, server_side=False)

Run

ASAN_LIB=$(gcc -print-file-name=libasan.so)
LD_PRELOAD="$ASAN_LIB ./ssl_new_hook.so" ./python trigger_uaf.py

ASAN Output (before fix)

❯ LD_PRELOAD="/usr/lib/gcc/x86_64-linux-gnu/14/libasan.so ./poc/ssl_new_hook.so" ./python ./poc/trigger_uaf.py
[*] Calling ctx.wrap_bio() - triggers newPySSLSocket() - SSL_new() will fail
[HOOK] SSL_new(0x51c000012880) called returning NULL to trigger error path
=================================================================
==88693==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50b00008d698 at pc 0x77b215896350 bp 0x7ffdf8af79b0 sp 0x7ffdf8af79a0
READ of size 8 at 0x50b00008d698 thread T0
    #0 0x77b21589634f in newPySSLSocket Modules/_ssl.c:921
    #1 0x77b21589672b in _ssl__SSLContext__wrap_bio_impl Modules/_ssl.c:4962
    #2 0x77b215896c83 in _ssl__SSLContext__wrap_bio Modules/clinic/_ssl.c.h:2134
    #3 0x616a3deb9bf1 in method_vectorcall_FASTCALL_KEYWORDS Objects/descrobject.c:421
    #4 0x616a3de99cb6 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:136
    #5 0x616a3de99da9 in PyObject_Vectorcall Objects/call.c:327
    #6 0x616a3e116672 in _Py_VectorCallInstrumentation_StackRefSteal Python/ceval.c:762
    #7 0x616a3e12cfc9 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:3193
    #8 0x616a3e15d85c in _PyEval_EvalFrame Include/internal/pycore_ceval.h:118
    #9 0x616a3e15dbc2 in _PyEval_Vector Python/ceval.c:2125
    #10 0x616a3e15de78 in PyEval_EvalCode Python/ceval.c:673
    #11 0x616a3e261171 in run_eval_code_obj Python/pythonrun.c:1366
    #12 0x616a3e2614b7 in run_mod Python/pythonrun.c:1469
    #13 0x616a3e2623ec in pyrun_file Python/pythonrun.c:1294
    #14 0x616a3e265222 in _PyRun_SimpleFileObject Python/pythonrun.c:518
    #15 0x616a3e2654ce in _PyRun_AnyFileObject Python/pythonrun.c:81
    #16 0x616a3e2ba3f6 in pymain_run_file_obj Modules/main.c:410
    #17 0x616a3e2ba663 in pymain_run_file Modules/main.c:429
    #18 0x616a3e2bbe61 in pymain_run_python Modules/main.c:691
    #19 0x616a3e2bc4f7 in Py_RunMain Modules/main.c:772
    #20 0x616a3e2bc6e3 in pymain_main Modules/main.c:802
    #21 0x616a3e2bca68 in Py_BytesMain Modules/main.c:826
    #22 0x616a3dd24675 in main Programs/python.c:15
    #23 0x77b21782a3b7 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #24 0x77b21782a47a in __libc_start_main_impl ../csu/libc-start.c:360
    #25 0x616a3dd245a4 in _start (/home/raminfp/Projects/cpython/python+0x2ec5a4) (BuildId: 0396070aaf722d21b278a88393f68f9832f122d1)

0x50b00008d698 is located 16 bytes after 104-byte region [0x50b00008d620,0x50b00008d688)
freed by thread T0 here:
    #0 0x77b217cfc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x616a3df65f10 in _PyMem_RawFree Objects/obmalloc.c:93
    #2 0x616a3df682ca in _PyMem_DebugRawFree Objects/obmalloc.c:3067
    #3 0x616a3df6830b in _PyMem_DebugFree Objects/obmalloc.c:3212
    #4 0x616a3df90698 in PyObject_Free Objects/obmalloc.c:1634
    #5 0x616a3e1d0ca7 in PyObject_GC_Del Python/gc.c:2460
    #6 0x77b215886942 in PySSL_dealloc Modules/_ssl.c:2463
    #7 0x616a3df5c50d in _Py_Dealloc Objects/object.c:3270
    #8 0x77b21587f9ba in Py_DECREF Include/refcount.h:403
    #9 0x77b21589630e in newPySSLSocket Modules/_ssl.c:920
    #10 0x77b21589672b in _ssl__SSLContext__wrap_bio_impl Modules/_ssl.c:4962
    #11 0x77b215896c83 in _ssl__SSLContext__wrap_bio Modules/clinic/_ssl.c.h:2134
    #12 0x616a3deb9bf1 in method_vectorcall_FASTCALL_KEYWORDS Objects/descrobject.c:421
    #13 0x616a3de99cb6 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:136
    #14 0x616a3de99da9 in PyObject_Vectorcall Objects/call.c:327
    #15 0x616a3e116672 in _Py_VectorCallInstrumentation_StackRefSteal Python/ceval.c:762
    #16 0x616a3e12cfc9 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:3193
    #17 0x616a3e15d85c in _PyEval_EvalFrame Include/internal/pycore_ceval.h:118
    #18 0x616a3e15dbc2 in _PyEval_Vector Python/ceval.c:2125
    #19 0x616a3e15de78 in PyEval_EvalCode Python/ceval.c:673
    #20 0x616a3e261171 in run_eval_code_obj Python/pythonrun.c:1366
    #21 0x616a3e2614b7 in run_mod Python/pythonrun.c:1469
    #22 0x616a3e2623ec in pyrun_file Python/pythonrun.c:1294
    #23 0x616a3e265222 in _PyRun_SimpleFileObject Python/pythonrun.c:518
    #24 0x616a3e2654ce in _PyRun_AnyFileObject Python/pythonrun.c:81
    #25 0x616a3e2ba3f6 in pymain_run_file_obj Modules/main.c:410
    #26 0x616a3e2ba663 in pymain_run_file Modules/main.c:429
    #27 0x616a3e2bbe61 in pymain_run_python Modules/main.c:691
    #28 0x616a3e2bc4f7 in Py_RunMain Modules/main.c:772
    #29 0x616a3e2bc6e3 in pymain_main Modules/main.c:802

previously allocated by thread T0 here:
    #0 0x77b217cfd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x616a3df66827 in _PyMem_RawMalloc Objects/obmalloc.c:65
    #2 0x616a3df65bf8 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2999
    #3 0x616a3df65c60 in _PyMem_DebugRawMalloc Objects/obmalloc.c:3032
    #4 0x616a3df674e8 in _PyMem_DebugMalloc Objects/obmalloc.c:3197
    #5 0x616a3df90554 in PyObject_Malloc Objects/obmalloc.c:1605
    #6 0x616a3e1d075a in _PyObject_MallocWithType Include/internal/pycore_object_alloc.h:46
    #7 0x616a3e1d075a in gc_alloc Python/gc.c:2352
    #8 0x616a3e1d08b0 in _PyObject_GC_New Python/gc.c:2372
    #9 0x77b215895e8a in newPySSLSocket Modules/_ssl.c:901
    #10 0x77b21589672b in _ssl__SSLContext__wrap_bio_impl Modules/_ssl.c:4962
    #11 0x77b215896c83 in _ssl__SSLContext__wrap_bio Modules/clinic/_ssl.c.h:2134
    #12 0x616a3deb9bf1 in method_vectorcall_FASTCALL_KEYWORDS Objects/descrobject.c:421
    #13 0x616a3de99cb6 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:136
    #14 0x616a3de99da9 in PyObject_Vectorcall Objects/call.c:327
    #15 0x616a3e116672 in _Py_VectorCallInstrumentation_StackRefSteal Python/ceval.c:762
    #16 0x616a3e12cfc9 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:3193
    #17 0x616a3e15d85c in _PyEval_EvalFrame Include/internal/pycore_ceval.h:118
    #18 0x616a3e15dbc2 in _PyEval_Vector Python/ceval.c:2125
    #19 0x616a3e15de78 in PyEval_EvalCode Python/ceval.c:673
    #20 0x616a3e261171 in run_eval_code_obj Python/pythonrun.c:1366
    #21 0x616a3e2614b7 in run_mod Python/pythonrun.c:1469
    #22 0x616a3e2623ec in pyrun_file Python/pythonrun.c:1294
    #23 0x616a3e265222 in _PyRun_SimpleFileObject Python/pythonrun.c:518
    #24 0x616a3e2654ce in _PyRun_AnyFileObject Python/pythonrun.c:81
    #25 0x616a3e2ba3f6 in pymain_run_file_obj Modules/main.c:410
    #26 0x616a3e2ba663 in pymain_run_file Modules/main.c:429
    #27 0x616a3e2bbe61 in pymain_run_python Modules/main.c:691
    #28 0x616a3e2bc4f7 in Py_RunMain Modules/main.c:772
    #29 0x616a3e2bc6e3 in pymain_main Modules/main.c:802
    #30 0x616a3e2bca68 in Py_BytesMain Modules/main.c:826

SUMMARY: AddressSanitizer: heap-buffer-overflow Modules/_ssl.c:921 in newPySSLSocket
Shadow bytes around the buggy address:
  0x50b00008d400: fa fa 00 00 00 00 00 00 00 00 00 00 00 00 00 fa
  0x50b00008d480: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
  0x50b00008d500: 00 00 00 00 00 fa fa fa fa fa fa fa fa fa fd fd
  0x50b00008d580: fd fd fd fd fd fd fd fd fd fd fd fd fa fa fa fa
  0x50b00008d600: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fd
=>0x50b00008d680: fd fa fa[fa]fa fa fa fa fa fa fa fa fa fa fa fa
  0x50b00008d700: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50b00008d780: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50b00008d800: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50b00008d880: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50b00008d900: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==88693==ABORTING


After fix

ssl.SSLError raised normally.

[HOOK] SSL_new(0x51c000012880) called returning NULL to trigger error path
Traceback (most recent call last):
  File "/home/raminfp/Projects/cpython/poc/trigger_uaf.py", line 27, in <module>
    sslobj = ctx.wrap_bio(incoming, outgoing, server_side=False)
  File "/home/raminfp/Projects/cpython/Lib/ssl.py", line 468, in wrap_bio
    return self.sslobject_class._create(
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        incoming, outgoing, server_side=server_side,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        server_hostname=self._encode_hostname(server_hostname),
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        session=session, context=self,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/home/raminfp/Projects/cpython/Lib/ssl.py", line 816, in _create
    sslobj = context._wrap_bio(
        incoming, outgoing, server_side=server_side,
        server_hostname=server_hostname,
        owner=self, session=session
    )
ssl.SSLError: unknown error (0x0) (_ssl.c:920)

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions