使用 ctypes 检索本机基址 class 的地址
Retrieving address of native base class with ctypes
我希望能够将证书传递给 Python 的 ssl 库而不需要临时文件。似乎 Python ssl 模块 cannot do that.
为了解决这个问题,我想从本机 _ssl
模块中检索存储在 ssl._ssl._SSLContext
class 中的基础 SSL_CTX
结构。使用 ctypes,我可以使用该上下文从 libssl 手动调用相应的 SSL_CTX_*
函数。 here 显示了如何在 C 中执行此操作,我将通过 ctypes 执行相同的操作。
不幸的是,我卡在了我设法从 ssl._ssl._SSLContext
连接到 load_verify_locations
函数的地步,但似乎无法获得 ssl._ssl._SSLContext
实例的正确内存地址=17=] 结构。所有 load_verify_locations
函数看到的都是父 ssl.SSLContext
对象。
我的问题是,如何从 ssl.SSLContext
对象的实例到本机基础 class ssl._ssl._SSLContext
的内存?如果我有那个,我可以轻松访问它的 ctx
成员。
到目前为止,这是我的代码。有关如何对本机 Python 模块进行 monkeypatch 的积分,请转到 the forbidden fruit project by Lincoln Clarete
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
这表明,无论我要更改什么,都不是 Python 知道的 ssl 上下文,而是其他一些随机内存位置。
要尝试上面的代码,您必须 运行 一个 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)
实际 SSLContext
答案即将到来,假设不再正确。
见https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_verify_locations
还有第三个参数,cadata
The cadata object, if present, is either an ASCII string of one or
more PEM-encoded certificates or a bytes-like object of DER-encoded
certificates.
显然是这样,因为 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
分配的,并且字段首先由 [=20= 填充] 然后 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")
我希望能够将证书传递给 Python 的 ssl 库而不需要临时文件。似乎 Python ssl 模块 cannot do that.
为了解决这个问题,我想从本机 _ssl
模块中检索存储在 ssl._ssl._SSLContext
class 中的基础 SSL_CTX
结构。使用 ctypes,我可以使用该上下文从 libssl 手动调用相应的 SSL_CTX_*
函数。 here 显示了如何在 C 中执行此操作,我将通过 ctypes 执行相同的操作。
不幸的是,我卡在了我设法从 ssl._ssl._SSLContext
连接到 load_verify_locations
函数的地步,但似乎无法获得 ssl._ssl._SSLContext
实例的正确内存地址=17=] 结构。所有 load_verify_locations
函数看到的都是父 ssl.SSLContext
对象。
我的问题是,如何从 ssl.SSLContext
对象的实例到本机基础 class ssl._ssl._SSLContext
的内存?如果我有那个,我可以轻松访问它的 ctx
成员。
到目前为止,这是我的代码。有关如何对本机 Python 模块进行 monkeypatch 的积分,请转到 the forbidden fruit project by Lincoln Clarete
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
这表明,无论我要更改什么,都不是 Python 知道的 ssl 上下文,而是其他一些随机内存位置。
要尝试上面的代码,您必须 运行 一个 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)
实际 SSLContext
答案即将到来,假设不再正确。
见https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_verify_locations
还有第三个参数,cadata
The cadata object, if present, is either an ASCII string of one or more PEM-encoded certificates or a bytes-like object of DER-encoded certificates.
显然是这样,因为 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
分配的,并且字段首先由 [=20= 填充] 然后 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")