Подтвердить что ты не робот

Получение адреса собственного базового класса с помощью ctypes

Я хочу иметь возможность передать сертификат в библиотеку ssl Python, не требуя временного файла. Кажется, что модуль ssl Python не может этого сделать.

Чтобы обойти эту проблему, я хочу получить базовую структуру SSL_CTX, хранящуюся в классе ssl._ssl._SSLContext, из собственного _ssl модуля. Используя ctypes, я мог бы вручную вызвать соответствующие функции SSL_CTX_* из libssl с этим контекстом. Как это сделать на C показано здесь, и я сделал бы то же самое через ctypes.

К сожалению, я застрял в точке, где мне удалось подключиться к функции load_verify_locations из ssl._ssl._SSLContext, но, похоже, не удалось получить правильный адрес памяти экземпляра структуры ssl._ssl._SSLContext. Вся функция load_verify_locations видит родительский объект ssl.SSLContext.

Мой вопрос: как я могу получить из экземпляра объекта ssl.SSLContext в память собственного базового класса ssl._ssl._SSLContext? Если бы у меня было это, я мог бы легко получить доступ к его члену ctx.

Вот мой код. Кредиты на то, как monkeypatch на родном модуле Python перейдите в проект запрещенных фруктов Линкольна Клэрета

Py_ssize_t = hasattr(ctypes.pythonapi, 'Py_InitModule4_64') and ctypes.c_int64 or ctypes.c_int

class PyObject(ctypes.Structure):
    pass

PyObject._fields_ = [
    ('ob_refcnt', Py_ssize_t),
    ('ob_type', ctypes.POINTER(PyObject)),
]

class SlotsProxy(PyObject):
    _fields_ = [('dict', ctypes.POINTER(PyObject))]

class PySSLContext(ctypes.Structure):
    pass

PySSLContext._fields_ = [
        ('ob_refcnt', Py_ssize_t),
        ('ob_type', ctypes.POINTER(PySSLContext)),
        ('ctx', ctypes.c_void_p),
        ]

name = ssl._ssl._SSLContext.__name__
target = ssl._ssl._SSLContext.__dict__
proxy_dict = SlotsProxy.from_address(id(target))
namespace = {}
ctypes.pythonapi.PyDict_SetItem(
        ctypes.py_object(namespace),
        ctypes.py_object(name),
        proxy_dict.dict,
)
patchable = namespace[name]

old_value = patchable["load_verify_locations"]

libssl = ctypes.cdll.LoadLibrary("libssl.so.1.0.0")
libssl.SSL_CTX_set_verify.argtypes = (ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p)
libssl.SSL_CTX_get_verify_mode.argtypes = (ctypes.c_void_p,)

def load_verify_locations(self, cafile, capath, cadata):
    print(self)
    print(self.verify_mode)
    addr = PySSLContext.from_address(id(self)).ctx
    libssl.SSL_CTX_set_verify(addr, 1337, None)
    print(libssl.SSL_CTX_get_verify_mode(addr))
    print(self.verify_mode)
    return old_value(self, cafile, capath, cadata)

patchable["load_verify_locations"] = load_verify_locations

context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)

Вывод:

<ssl.SSLContext object at 0x7f4b81304ba8>
2
1337
2

Это говорит о том, что независимо от того, что я изменяю, это не контекст ssl, о котором знает Python, а какая-то другая случайная ячейка памяти.

Чтобы опробовать код сверху, вам нужно запустить https-сервер. Создайте самоподписанный сертификат SSL, используя:

$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost' -nodes

И запустите сервер, используя следующий код:

import http.server, http.server
import ssl
httpd = http.server.HTTPServer(('localhost', 4443), http.server.SimpleHTTPRequestHandler)
httpd.socket = ssl.wrap_socket (httpd.socket, certfile='cert.pem', keyfile='key.pem', server_side=True)
httpd.serve_forever()

И затем добавьте следующую строку в конец моего примера выше:

urllib.request.urlopen("https://localhost:4443", context=context)
4b9b3361

Ответ 1

Фактический SSLContext ответ ожидается, предположение уже неверно.

См. https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_verify_locations

Там третий аргумент, cadata

Объект cadata, если он присутствует, является либо строкой ASCII одного или больше PEM-закодированных сертификатов или байтового объекта DER-кодированного сертификаты.

По-видимому, случай с Python 3.4

Получение базового контекста PyObject

Этот простой, ssl.SSLContext наследует от _ssl._SSLContext, который в модели данных Python означает, что есть только один объект на одном адресе памяти.

Следовательно, ssl.SSLContext().load_verify_locations(...) действительно вызовет:

ctx = \
ssl.SSLContext.__new__(<type ssl.SSLContext>, ...)  # which calls
    self = _ssl._SSLContext.__new__(<type ssl.SSLContext>, ...)  # which calls
        <type ssl.SSLContext>->tp_alloc()  # which understands inheritance
        self->ctx = SSL_CTX_new(...)  # _ssl fields
    self.set_ciphers(...)  # ssl fields
    return self

_ssl._SSLContext.load_verify_locations(ctx, ...)`.

Реализация C получит объект, казалось бы, неправильный тип, но это ОК, потому что все ожидаемые поля есть, поскольку он был выделен общим type->tp_alloc, а поля сначала заполнялись _ssl._SSLContext, а затем ssl.SSLContext.

Здесь демонстрация (утомительные подробности опущены):

# _parent.c
typedef struct {
  PyObject_HEAD
} PyParent;

static PyObject* parent_new(PyTypeObject* type, PyObject* args,
                            PyObject* kwargs) {
  PyParent* self = (PyParent*)type->tp_alloc(type, 0);
  printf("Created parent %ld\n", (long)self);
  return (PyObject*)self;
}

# child.py
class Child(_parent.Parent):
    def foo(self):
        print(id(self))

c1 = Child()
print("Created child:", id(c1))

# prints:
Created parent 139990593076080
Created child: 139990593076080

Получение базового контекста OpenSSL

typedef struct {
    PyObject_HEAD
    SSL_CTX *ctx;
    <details skipped>
} PySSLContext;

Таким образом, ctx находится на известном смещении, которое равно:

PyObject_HEAD
This is a macro which expands to the declarations of the fields of the PyObject type; it is used when declaring new types which represent objects without a varying length. The specific fields it expands to depend on the definition of Py_TRACE_REFS. By default, that macro is not defined, and PyObject_HEAD expands to:

Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;

When Py_TRACE_REFS is defined, it expands to:

PyObject *_ob_next, *_ob_prev;
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;

Таким образом, в производственной (не-отладочной) сборке и принимая во внимание естественное выравнивание, PySSLContext становится:

struct {
    void*;
    void*;
    SSL_CTX *ctx;
    ...
}

Таким образом:

_ctx = _ssl._SSLContext(2)
c_ctx = ctypes.cast(id(_ctx), ctypes.POINTER(ctypes.c_void_p))
c_ctx[:3]
[1, 140486908969728, 94916219331584]
# refcnt,      type,          C ctx

Объединяя все это

import ssl
import socket
import ctypes
import pytest


def contact_github(cafile=""):
    ctx = ssl.SSLContext()
    ctx.verify_mode = ssl.VerifyMode.CERT_REQUIRED

    # ctx.load_verify_locations(cafile, "empty", None) done via ctypes
    ssl_ctx = ctypes.cast(id(ctx), ctypes.POINTER(ctypes.c_void_p))[2]
    cssl = ctypes.CDLL("/usr/lib/x86_64-linux-gnu/libssl.so.1.1")
    cssl.SSL_CTX_load_verify_locations.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
    assert cssl.SSL_CTX_load_verify_locations(ssl_ctx, cafile.encode("utf-8"), b"empty")

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(("github.com", 443))

    ss = ctx.wrap_socket(s)
    ss.send(b"GET / HTTP/1.0\n\n")
    print(ss.recv(1024))


def test_wrong_cert():
    with pytest.raises(ssl.SSLError):
        contact_github(cafile="bad-cert.pem")


def test_correct_cert():
    contact_github(cafile="good-cert.pem")