2019独角兽企业重金招聘Python工程师标准>>>
为了在oneproxy-monitor中实现sql server的前后端登录分离,经过一段时间的研究终于把sql server的登录搞定了。目前可以做到前端通过一个密码连接到中间件oneproxy-for-sqlserver(oneproxy-monitor的sqlserver版本)。oneproxy-for-sqlserver再通过数据库密码登录到后端sql server数据库,再实现前后端数据的直接转发功能。
sql server登录过程的数据包交互情况如下图所示,登录过程中从大体上来说指涉及到两个请求包和两个响应数据包。其中prelogin数据包是为了本次连接把客户端的环境信息发送到服务端,这个数据包中包括:客户端的版本信息,是否要求加密等8类数据并且以0xff来结束,在0xff结束后在放置前面各种类型的具体数据内容。可以参见MSDN。prelogin的响应包是对应类型的响应情况。这一步比较简单,可以直接进行解包和回复响应包。
目前我研究的对象是sql server 2016,从sql server的官方文档可以知道,不管是否配置使用ssl还是不使用ssl,在登录的过程中都会 使用ssl来保护login7的数据包,防止中间人攻击。在login7中包含了用户的用户名,密码,以及前端应用信息,tds版本,客户端能够接收的包大小,客户端进程ID, 连接ID等。可见这个数据包是登录过程中关键的数据包。只要搞定这个数据包登录过程就能够搞定了。
要解析出login7包中的内容,就需要在中间件中实现ssl的服务端环境。通过ssl来对数据进行解密和加密login7数据包。要实现ssl端的服务端环境,则需要解决两个问题:
1. ssl服务端的证书问题
2. 解决ssl的加密数据包中增加和去掉sqlserver的包头的问题
解决ssl服务端的证书问题
开始以为sqlserver会在主机上面生成证书,并且保存到指定的文件中。于是在主机上面搜索sql server的证书,结果...,然后没有结果了。于是阅读msdn上面的文章,才了解到sql server当用户设置不使用ssl加密时,会生成自签名证书。但是没有说到是否生成证书文件,在stackoverflow上面了解到是没有生成证书文件,只是在sql server启动时内存中生成证书信息,把证书信息保存在内存中的。
于是通过阅读openssl的源代码了解内存中证书的使用方法,以及使用openssl api生成证书的实现方式。其中openssl api生成证书的实现可以参见开源软件ssl-cert-generator-lib。在通过如下两个函数把证书和私钥载入到ssl上下文中。
int SSL_CTX_use_PrivateKey(SSL_CTX *ctx, EVP_PKEY *pkey);
int SSL_CTX_use_certificate(SSL_CTX *ctx, X509 *x);
需要注意的是不能直接使用ssl-cert-generator-lib中返回的public_key_pem和private_key_pem来设置,否则会得到wrong tag的错误。
解决ssl加密包头的问题
通过阅读msdn的文档可以知道,并不是把login7数据包直接通过ssl来发送即可,还需要在加密后的数据包中增加一个sql server包的头信息。通过阅读openssl发数据的代码(如下所示),可以知道在发送的数据会先调用回调函数,只要设置这个回调函数就可以在发送加密数据包之前增加sql server包的头信息。
int BIO_read(BIO *b, void *out, int outl)
{
int i;
long (*cb) (BIO *, int, const char *, int, long, long);
if ((b == NULL) || (b->method == NULL) || (b->method->bread == NULL)) {
BIOerr(BIO_F_BIO_READ, BIO_R_UNSUPPORTED_METHOD);
return (-2);
}
cb = b->callback;
if ((cb != NULL) &&
((i = (int)cb(b, BIO_CB_READ, out, outl, 0L, 1L)) <= 0))
return (i);
if (!b->init) {
BIOerr(BIO_F_BIO_READ, BIO_R_UNINITIALIZED);
return (-2);
}
i = b->method->bread(b, out, outl);
if (i > 0)
b->num_read += (unsigned long)i;
if (cb != NULL)
i = (int)cb(b, BIO_CB_READ | BIO_CB_RETURN, out, outl, 0L, (long)i);
return (i);
}
从上面代码中能够了解到只要实现BIO结构中的callback即可。而这个callback正是SSL结构中的rbio指针的callback。并且openssl提供了如下的函数来设置这个回调函数:
void BIO_set_callback(BIO *b,
long (*callback) (struct bio_st *, int, const char *,
int, long, long));
现在主要来讲解回调函数的定义情况,下面是oneproxy-monitor中针对sqlserver的回调函数指针的定义。
typedef long (*handle_data_callback) (struct bio_st *bio, int oper, const char *data, int dataLen, long argl, long ret);
需要注意的是oper的值,其中oper的可以取值:BIO_CB_WRITE,BIO_CB_READ,BIO_CB_RETURN等,当发送数据的时候oper将是BIO_CB_WRITE,发送完毕后oper的值为BIO_CB_WRITE|BIO_CB_RETURN.当开始读取数据时oper是BIO_CB_READ,读取完毕后BIO_CB_RETURN.
下面来说一个有趣的事情(见下面ssl结构的部分代码):先说明我使用的是openssl-1.0.2e版本,从这个结构中可以了解到ssl结构中提供了BIO *rbio, BIO* wbio以及BIO* bbio.我开始以为openssl是通过bbio来进行读写数据的,于是通过BIO_set_callback来设置bbio的回调函数,结果直接core了。通过阅读代码才知道原来这个啃爹的尽然为NULL,并没有设置。这个让我很不能理解openssl为啥这么干,这不是明显的站着资源不干活嘛。
struct ssl_st {
/*
* protocol version (one of SSL2_VERSION, SSL3_VERSION, TLS1_VERSION,
* DTLS1_VERSION)
*/
int version;
/* SSL_ST_CONNECT or SSL_ST_ACCEPT */
int type;
/* SSLv3 */
const SSL_METHOD *method;
/*
* There are 2 BIO's even though they are normally both the same. This
* is so data can be read and written to different handlers
*/
# ifndef OPENSSL_NO_BIO
/* used by SSL_read */
BIO *rbio;
/* used by SSL_write */
BIO *wbio;
/* used during session-id reuse to concatenate messages */
BIO *bbio;
# else
/* used by SSL_read */
char *rbio;
/* used by SSL_write */
char *wbio;
char *bbio;
# endif
第二个有趣的事情是:由于sql server在读和写的时候都需要对包头进行操作。读的时候需要去掉sql server的额外包头,在写的时候要增加额外包头。于是看了BIO_set_callback的函数后秒懂,不就是设置两个回调函数嘛?于是匆匆写了一个写的回调函数,一个读的回调函数,并且通过BIO_set_callback函数设置到对应的BIO上面,结果测试发现先设置的回调函数总是失效。这个坑爹的,连个回调函数都设置不成功了。把自己的代码阅读了n遍发现没有错误呀,最后再次阅读openssl的代码,发现rbio和wbio居然是同一个结构。在通过BIO_set_callback设置rbio的回调函数的同时也就是把wbio的回调函数给设置了,反之也是。这个地方又体现了openssl坑爹的一面。居然设置wbio的回调函数把rbio的回调函数也设置了。这个怎么看怎么怪。为啥不把bbio也指向rbio和wbio的结构 呢,这样通过设置bbio的回调函数来设置读和写的回调函数不是更加合理么?
更多信息,请关注oneproxy-monitor。