感谢hugtech 提出的问题、代码以及使用 HTTP 协议的工作。也非常感谢Remy Lebeau 告知我们有关 SSL 和 TLS 握手的信息。我的研究从他们那里起飞。
在我的例子中,发送原始 HTTP GET 命令导致 WinSock send(...) 以零失败。想象一下,我什至不允许发送那些未加密的东西。使用从站点复制的 ClientHello 后,我得到 www.youtube.com 给我发回一个 ServerHello。我不断地调整、编译我的代码并进行测试;我希望 Youtube 不会感到沮丧并要求提供验证码。
然后我就能够成功地修改 ClientHello。我最后简单地取出了扩展,因为它们是可选的,而且我不会支持它们。并修改了长度变量以适应调整。
// Client Hello I copied from the site
//uint8_t clientHello[] = { 0x16, 0x03, 0x01, 0x00, 0xa5, 0x01, 0x00, 0x00, 0xa1, 0x03, 0x03, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00, 0x00, 0x20, 0xcc, 0xa8, 0xcc, 0xa9, 0xc0, 0x2f, 0xc0, 0x30, 0xc0, 0x2b, 0xc0, 0x2c, 0xc0, 0x13, 0xc0, 0x09, 0xc0, 0x14, 0xc0, 0x0a, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0xc0, 0x12, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x18, 0x00, 0x16, 0x00, 0x00, 0x13, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x75, 0x6c, 0x66, 0x68, 0x65, 0x69, 0x6d, 0x2e, 0x6e, 0x65, 0x74, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x01, 0x04, 0x03, 0x05, 0x01, 0x05, 0x03, 0x06, 0x01, 0x06, 0x03, 0x02, 0x01, 0x02, 0x03, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x12, 0x00, 0x00, };
// modified client hello
// unnecessary extension code has been erased,
// just as how stack overflow moderators will erase
// my unnecessary wordings and sentencing.
uint8_t clientHello[] =
{
//Record Header
0x16, //type: handshake
0x03, 0x01, //TLS version:
0x00, 75, //HandShake Length
//Handshake Header
0x01, //type: client hello
0x00, 0x00, 71, //handshake data len
// Client Version
0x03, 0x03, // TLS 1.2 //actual TLS version client uses
// Client Random Garbage
// Here I see 32 bytes being used; but I don't know if 32 bytes is mandatory.
// But if it is not mandatory how would one know when the random garbage ends?
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
//Session ID
// This ID is used to logon back from a previous session.
// This eliminates certain computations as per what I read.
// Here we use zero to say this is a fresh session with the server.
0x00,
// Cypher Suites
// A list of Cypher Suite used to do the encryption.
// Cypher Suites are represented by two bytes.
// Example 0xCCA8 is TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 Cypher Suite.
0x00, 0x20, // count of bytes
0xcc, 0xa8, 0xcc, 0xa9, 0xc0, 0x2f, 0xc0, 0x30, 0xc0, 0x2b, 0xc0, 0x2c, 0xc0, 0x13, 0xc0, 0x09,
0xc0, 0x14, 0xc0, 0x0a, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0xc0, 0x12, 0x00, 0x0a,
// Compression Methods
// A list of compression methods.
// Here we have only one compression method which is zero for no compression.
0x01, //bytes of compression methods
0x00,
// After this we have one long list of Extension information which
// I have no interest in at all.
};
size_t clientHelloLen = sizeof(clientHello);
待续...
服务器你好
我已经阅读了 Joshua Davis 的 350 页的书,实现 SSL_TLS,结果发现 TLS 握手比任何人都解释的要复杂得多。我设法为www.youtube.com 准备了我自己的ClientHello 消息并取回了一个有效的ServerHello 和ServerCertificate(s)。但是当我用www.example.org 和www.apachefriends.org 尝试这个时,我没有收到任何消息。然后,我尝试了从https://tls.ulfheim.net/ 复制的原始 ClientHello,并取回了格式正确的 ServerHello 和 ServerCertificate,我可以使用我的自定义例程进行打印。问题是从服务器获得响应需要大约一分钟;我注意到证书看起来很可疑。它由同一个实体发行和发行。它还指出这是一个假证书控制。
然后我使用 OpenSSL 运行以下命令。 openssl s_client -connect "www.apachefriends.org:https"。我立即收到了服务器的回复!这意味着该站点正在拒绝我的自定义 ClientHello 消息。这意味着我浪费了我的周末来学习 Joshua Davis 的书,而应该阅读一本展示如何使用 OpenSSL 的书。
更新:
我终于可以开始工作了,但是 HTTPS、TLS/SSL 握手实在是太复杂了。我很生气,我研究了 ClientHello、ServerHello、ServerCertificate 的详细信息,但根本没有用。所有这些都被 OpenSSL 抽象出来了。并且出于某种原因,网站拒绝接受我的自定义 ClientHello,即使我完全按照 Joshua Davis 展示的那样做。我也对 OpenSSL 抽象出 Linux 和 Win32 SOCKETS 感到难过;所以学习这些细节只是为了学术。
以下是我使用 OpenSSL 从服务器获取文件的代码。请注意,在下载文件时,服务器通常会将您重定向到实际拥有该文件的另一台服务器。在这里,我对重定向进行了硬编码。
#include <Jav/test.h> // rep(...)
// My custom library for handling files
// You can substitute this with std::fstream, FILE*, or
// any other file library
#include <Jav/file/file.h>
// Custom exception class, Jav::Error
// derived from std::exception
#include <Jav/error/error.h>
//Trying to get _fileno to be defined
// as it is needed by openssl/applink.c
#undef __STRICT_ANSI__
// Undefing strict ansi failed so I simply
// declared _fileno
extern "C"{
_CRTIMP int __cdecl __MINGW_NOTHROW _fileno (FILE*);
}
# include <openssl/bio.h>
# include <openssl/ssl.h>
# include <openssl/err.h>
// Yep we actually include a source file.
// Other wise openssl gives "no app link" error during runtime.
// I tried adding applink.c to src instead of including it
// but got linker errors.
# include <openssl/applink.c>
void init_openssl_library()
{
SSL_library_init();
SSL_load_error_strings();
OPENSSL_config(NULL);
}
void on_openssl_fail(int line)
{
ERR_print_errors_fp(stderr);
throw Jav::Error("problem at line %i\n",line);
}
#define HOST_REDIRECTED "redirectedexamle.org"
#define RESOURCE "/exampleresource.txt"
int main()
{
SSL_CTX *ctx = NULL;
BIO *web = NULL, *out = NULL;
SSL *ssl = NULL;
init_openssl_library();
const SSL_METHOD *method = SSLv23_method();
if(method == NULL) on_openssl_fail(__LINE__);
ctx = SSL_CTX_new(method);
if(ctx == NULL) on_openssl_fail(__LINE__);
// Cannot fail???
//SSL_CTX_set_verify(ctx,SSL_VERIFY_PEER,verify_callback);
//SSL_CTX_set_verify_depth(ctx,4);
const long flags = SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3|SSL_OP_NO_COMPRESSION;
SSL_CTX_set_options(ctx,flags);
web = BIO_new_ssl_connect(ctx);
if(web == NULL) on_openssl_fail(__LINE__);
if(BIO_set_conn_hostname(web,HOST":https") != 1)
on_openssl_fail(__LINE__);
BIO_get_ssl(web,&ssl);
if(ssl == NULL) on_openssl_fail(__LINE__);
const char *PREFERRED_CIPHERS = "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4";
if(SSL_set_cipher_list(ssl,PREFERRED_CIPHERS) != 1)
on_openssl_fail(__LINE__);
if(SSL_set_tlsext_host_name(ssl,HOST) != 1)
on_openssl_fail(__LINE__);
//out = BIO_new_fp(stdout,BIO_NOCLOSE);
//if(out == NULL) on_openssl_fail(__LINE__);
if(BIO_do_connect(web) != 1) on_openssl_fail(__LINE__);
if(BIO_do_handshake(web) != 1) on_openssl_fail(__LINE__);
X509 *cert = SSL_get_peer_certificate(ssl);
if(cert == NULL) on_openssl_fail(__LINE__);
X509_free(cert);
//if(SSL_get_verify_result(ssl) != X509_V_OK) on_openssl_fail(__LINE__);
int len = 10000;
Jav::cstring buf(len);
auto cmd = "GET "RESOURCE" HTTP/1.1\r\n"
"Host: "HOST"\r\n"
"Connection: close\r\n\r\n";
if(BIO_puts(web, cmd) <= 0)
{
rep("bio write failed");
if(! BIO_should_retry(web))
{
rep("bio should retry");
}
else return 1;
}
int total = 0;
Jav::wFile file = Jav::createNewFile<Jav::wFile>("download.txt");
while(true)
{
int numRead = BIO_read(web, buf, len);
total += numRead;
if(numRead == 0)
{
rep("No more data read");
if(total) break;
else return 1;
}
else if(numRead < 0)
{
rep("bio read failed");
if(! BIO_should_retry(web))
{
rep("bio should retry");
int numRead = BIO_read(out, buf, len);
rep("num read",numRead);
on_openssl_fail(__LINE__);
}
else return 1;
}
else rep("num read",numRead);
file.write(buf,numRead);
}
//Jav::wFile file = Jav::createFile<Jav::wFile>("output.txt");
//file.write(buf,numRead);
BIO_free(out);
BIO_free(web);
SSL_CTX_free(ctx);
}