ネットワークの通信はほとんど暗号化されています。

ブラウザが勝手に対応してくれているために何を行っているのかわかりません。

そこで今回はそのやり取りを見てみようと思います。

wiresharkを使う

ブラウザとサーバーはパケットというデータの単位でやり取りしており、それはwiresharkというソフトで見れるためそれでみます。

しかしブラウザはいろんな処理とかして複雑で意味わかんないので、とりあえずcurlというコンソールからネットにアクセスできるコマンドのやり取りを見てみます。

curlでgoogleにアクセスする

cmdに「curl www.google.com」と入力すると、よく見るgoogleの検索画面のhtmlが送られてきます。

このときのやり取りをwiresharkでみると

最初の3つがSYN,SYN/ACK,ACKというTCP接続を確立するハンドシェイクです。

これで接続した後4つ目にGETでhtmlをサーバーに要求して、そのあとに続くパケットでgoogleの検索画面のhtmlが送られています。

このやり取りではTLSが使われておらず暗号化されていません。

wiresharkの追跡>TCPストリームから内容を見ると

GET / HTTP/1.1

Host: www.google.com

User-Agent: curl/7.79.1

Accept: */*


HTTP/1.1 200 OK

Date: Sun, 30 Jan 2022 11:37:46 GMT

Expires: -1

Cache-Control: private, max-age=0

Content-Type: text/html; charset=ISO-8859-1

P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info."

Server: gws

X-XSS-Protection: 0

X-Frame-Options: SAMEORIGIN

Set-Cookie: 

Accept-Ranges: none

Vary: Accept-Encoding

Transfer-Encoding: chunked

以下にhtmlのデータが続く

赤いのがクライアント側のcurlが送ったデータです。

中身はこんな感じで内容がそのまま見れることがわかります。

しかしここから検索して何かしらのサイトを開こうとするとTLSが必要となります。


curlでyoutubeにアクセスする

curl www.youtube.comと入力すると、HTTP/1.1 301 Moved Permanentlyというデータが送られてきてアクセスできません。

これはhttps://が抜けているからです。

そのためTLSを使わないといけないことがわかります。

curl https://www.youtube.comと入力すると

最初の3つでTCPを確立し、その後GETではなくClient Helloとなっています。

そしてServer Hello, Client Key Exchangeなどを通じた後、やっと暗号化されたApplication Dataのやり取りが始まっています。


Client Hello

client helloをwiresharkで見てみると、

    TLSv1.2 Record Layer: Handshake Protocol: Client Hello

        Content Type: Handshake (22)

        Version: TLS 1.2 (0x0303)

        Length: 196

        Handshake Protocol: Client Hello

            Handshake Type: Client Hello (1)

            Length: 192

            Version: TLS 1.2 (0x0303)

            Random:

            Session ID Length: 0

            Cipher Suites Length: 42

            Cipher Suites (21 suites)

            Compression Methods Length: 1

            Compression Methods (1 method)

            Extensions Length: 109

            Extension: server_name (len=20)

            Extension: status_request (len=5)

            Extension: supported_groups (len=8)

            Extension: ec_point_formats (len=2)

            Extension: signature_algorithms (len=26)

            Extension: session_ticket (len=0)

            Extension: application_layer_protocol_negotiation (len=11)

            Extension: extended_master_secret (len=0)

            Extension: renegotiation_info (len=1)

なんかいろいろデータを送っています。

あー多すぎる。

httpからhttpsになった瞬間データ量がめっちゃ増えてる。

こんどやろ。


Client Helloの仕様を確認しよう。

この仕様はRFCというものに書いてあるらしい。

RFCとはIETFとかいう団体が作っているやつである。

The Transport Layer Security (TLS) Protocol Version 1.3

このTLSのRFCのClient Helloのところを見てみる。

uint16 ProtocolVersion;
      opaque Random[32];

      uint8 CipherSuite[2];    /* Cryptographic suite selector */

      struct {
          ProtocolVersion legacy_version = 0x0303;    /* TLS v1.2 */
          Random random;
          opaque legacy_session_id<0..32>;
          CipherSuite cipher_suites<2..2^16-2>;
          opaque legacy_compression_methods<1..2^8-1>;
          Extension extensions<8..2^16-1>;
      } ClientHello;

っと、危ない。

TLS1.2と1.3はなんか結構違うらしい。

SSL/TLS(SSL3.0~TLS1.2)のハンドシェイクを復習する

ここにそう書いてあった。

上のやり取りではTLS1.2を使っているようだったのでTLS1.2の方を見てみる。

The Transport Layer Security (TLS) Protocol Version 1.2

struct {
             uint32 gmt_unix_time;
             opaque random_bytes[28];
         } Random;
uint8 CipherSuite[2];    /* Cryptographic suite selector */
enum { null(0), (255) } CompressionMethod;

struct {
          ProtocolVersion client_version;
          Random random;
          SessionID session_id;
          CipherSuite cipher_suites<2..2^16-2>;
          CompressionMethod compression_methods<1..2^8-1>;
          select (extensions_present) {
              case false:
                  struct {};
              case true:
                  Extension extensions<0..2^16-1>;
          };
      } ClientHello;

確かに1.2と1.3で少し違うっぽい?

<>はなんだ?可変長配列?

とりあえずさっきのyoutubeに送った中身を当てはめてみよう。

struct {
          ProtocolVersion client_version=0x303;
          Random random={
                 gmt_unix_time=0x620ba72b;
                 random_bytes[28]=0x8a6e879d26a8bf253f04016e5bba4da2dc07aa9ab814167fb2fe6ea8;
                        };
          SessionID session_id=0x00;
          CipherSuite cipher_suites<2..2^16-2>={
                 length=0x002a;
                 Cipher_Suite=0xc02c;//TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
                 Cipher_Suite=0xc02b;//TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
                 Cipher_Suite=0xc030;//TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
                 ・・・
                                                }
          CompressionMethod compression_methods<1..2^8-1>={
                 length=0x01;
                 method=0x00;
                                                          }
          select (extensions_present) {
              case false:
                  struct {};
              case true:
                  Extension extensions<0..2^16-1>={
                 extension_length=0x0069;
                 server_name
                   type=0x0000;
                    length=0x0010;
                    server_name_list_length=0x000e;
                    server_name_type=0x00;//host_name
                    server_name_length=0x000b;
                    server_name=0x796f75747562652e636f6d;//youtube.com
                  status_request
                    ・・・
                                                  };
          };
      } ClientHello;

なんかbloggerに載せると死ぬほど見にくくなったけどまあいいや。

長い部分は省略している。

特にextensionは長い。。

暗号化方式はECDSAでAES256bitでSHA384を使う方式がCURLでは一番優先度が高いみたいですね。

なんかchromeだとchacha20を使ってるとか聞いたんですけど、QUICというプロトコルでTLS1.3が含まれていて、UDPでやりとりしてとかさっぱりわかりません。

QUICをゆっくり解説(1):QUICが標準化されました

QUICをゆっくり解説(3):QUICパケットの構造

QUICをゆっくり解説(4):ハンドシェイク

解説がありましたが、やっぱ難しそうです。

Server Hello

次はserver helloを見てみます。

まずserverhelloの構造を見てみると

 struct {
          ProtocolVersion server_version;
          Random random;
          SessionID session_id;
          CipherSuite cipher_suite;
          CompressionMethod compression_method;
          select (extensions_present) {
              case false:
                  struct {};
              case true:
                  Extension extensions<0..2^16-1>;
          };
      } ServerHello;

実際のパケットを見てみると、

Transport Layer Security

    TLSv1.2 Record Layer: Handshake Protocol: Server Hello

        Content Type: Handshake (22)

        Version: TLS 1.2 (0x0303)

        Length: 78

        Handshake Protocol: Server Hello

            Handshake Type: Server Hello (2)

            Length: 74

            Version: TLS 1.2 (0x0303)

            Random:

            Session ID Length: 0

            Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)

            Compression Method: null (0)

            Extensions Length: 34

            Extension: extended_master_secret (len=0)

            Extension: renegotiation_info (len=1)

            Extension: ec_point_formats (len=2)

            Extension: session_ticket (len=0)

            Extension: application_layer_protocol_negotiation (len=11)

さっきのclient helloの2番目のcipher suiteを用いていることがわかりました。

TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256を理解できればTLSを実装できそうです。

curlのソースコードを見る

curlのソースコードを見て、実装方法を勉強する。

githubを見てみると、死ぬほどファイルがあってどれを見ればいいかわからない。

いろいろてきとーに見ていると、httpのGEThttpsのGETのサンプルコードらしきものがあった。

どちらも

curl_easy_init()

curl_easy_setopt()

curl_easy_perform()

curl_easy_cleanup()

こんな流れで動いていた。

動作のメインはcurl_easy_perform()にありそうなため、これを見てみる。

検索すると、easy.cにあった。

中でeasy_perform()を呼んでおり、同じファイル内にあった。

result = events ? easy_events(multi) : easy_transfer(multi);

で、eventsはcurl_easy_performでFALSEを渡しているためeasy_transfer()が実行される。

easy_transfer()では何個か関数を呼んでいるが、たぶんcurl_multi_info_read()が重要っぽい気がする。

multi.cにあり、見てみたがメッセージの処理をしているみたいで重要ではなかった。。

easy_transfer()にあったcurl_multi_perform()を追ってみる。

そこからさらにmulti_runsingle()をみるとめっちゃ長いコードがあった。

長すぎて何を見ればいいか分からなかったが、Curl_connect()やCurl_http_connect()という関数があった。

とりあえずCurl_http_connect()を見てみる。

http.cにあり、Curl_proxy_connect()、CONNECT_FIRSTSOCKET_PROXY_SSL()、https_connecting()などがあった。

https_connecting()を追うと、Curl_ssl_connect_nonblocking()が呼ばれている。

vtls.cにあり、connect_nonblocking()などを呼んでいたが、ただ構造体をセットしているだけみたい。

Curl_http_connect()まで戻ってCurl_proxy_connect()を追うと、http_proxy.cにあった。

https_proxy_connect()を呼んでおり、またCurl_ssl_connect_nonblocking()を呼んでいた。

今度はその中のssl_connect_init_proxy()を追ってが、memset()でメモリを確保しているだけっぽい。

うーぬ。

multi_runsingle()に戻るか。

今度はCurl_connect()を追うと、url.cにあった。

その中のcreate_conn()とか追ってみたけどよくわからん。

オープンソースのコードすら読めない。。

winsock以外にもc言語でソケット通信する方法があるのかなとか思ったんだけどどうなんだろうか。


x64dbgでwindows/system32にあったcurl.exeを開いてみると、シンボルにws2_32.dllがあった。

やっぱりwinsockを使ってるっぽい。

そうか、winsock使っているか調べたいならwinsockの関数で検索すればよかったのか。

WSAStartupで検索すると、


やっぱり普通にwinsock使ってた。

あと関係ないけどpythonのスクレイピングもws2_32.dllがロードされていて、connectにBP置いたら止まったし、やっぱりwinsockが一番下にあるのか。


今度はwinsockの関数を中心にもう少しcurlのソースコードを調べてみる。

winsockの流れを見てみると、

WSAStartup() 4

gethostbyname() 21

socket() 228

getservbyname() 4

connect() 368

send() 335

recv() 137

closesocket() 28

WSACleanup() 3

大体こんな感じのはず。

右の数字は検索した時のヒット数。

この数字が少ないものを見てみる。

WSAStartup()

externalsocket.cのmain()

system_win32.cのCurl_win32_init()

util.cのwin32_init()

やっぱり初期化処理が多い。

WSACleanup()も同様だろう。

getservbyname()

ヒットしたのは全部コメントだった。


結局、TLSのハンドシェイクをどう実装しているのかcurlのソースコードを読んで理解しようとしたが、curlが多機能でコードが多すぎて理解できなかった。


winsockでコピペしたハンドシェイクを送る

TLSの実装方法がわからないので、今度は先ほどwiresharkで得たclient helloのバイナリをwinsockでyoutubeに送ってみようと思います。


で、送ってみたらBad Requestでした。

おそらく昔のwinsockのコードを参考にしたため、IPv6に対応していなかったことが原因だと思われます。

IPv4のアドレスにClient Helloを送ってもだめっぽいので、IPv6のアドレスに送ってみます。

いつもはEasyIDECという苦しんで覚えるc言語というサイトが出しているc言語開発環境を使っているのですが、winsockはvisualstudioの方がいろいろやりやすいのでこっちを使っています。

#define _WINSOCK_DEPRECATED_NO_WARNINGSを外してvsでwinsockのコードを打つと古い関数を教えてくれます。


 C4996: 'inet_addr': Use inet_pton() or InetPton() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings

C4996: 'gethostbyname': Use getaddrinfo() or GetAddrInfoW() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings

C4996: 'gethostbyaddr': Use getnameinfo() or GetNameInfoW() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings

C4996: 'inet_ntoa': Use inet_ntop() or InetNtop() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings


こんな感じのエラーですね。

これを直せばIPv6にも対応すると思うのでやってみます。

getaddrinfo(getaddrinfoを使ってsockaddrを作成する)

GETADDRINFO

ここら辺を参考にしながらIPv6に対応するコードに直してみました。

すると、

TLSのClient Helloとして扱ってもらうことに成功しました!

なんか全然うまくできなくて、opensslとか素直に使えばいいじゃんとか、python使えばいいし、というかchrome使えばよくない?とか内心思っていました。

しかし、遂に、client helloの第一歩を踏み出せた気がします!

まあcurlのパケットをそのまま送っただけなので、これからそのパケットを自分なりに編集したり、serverhelloの解析からkey exchangeなどの世界を実際にいじって確かめてみていこうと思います。


一応そのコードを載せておきます。

引っかかった部分としては、getaddrinfo()の使い方、send()で送るバイト数、ポート番号が80ではなく443なことです。


#define _CRT_SECURE_NO_WARNINGS
//#define _WINSOCK_DEPRECATED_NO_WARNINGS
#pragma comment(lib,"ws2_32.lib")
#include <stdio.h>
#include <winsock2.h>
#include <WS2tcpip.h>

void setTCP();

int main(int argc, char** argv)
{
	//拡張しようとsetTCP()にまとめたけどまだなにもできてないやつ
	setTCP();
	return 0;
}

void setTCP()
{
	WORD wVersionRequested = MAKEWORD(2, 2);
	WSADATA wsaData;
	int nRet;

	char lpServerName[100], lpFileName[100];
	puts("serverName filePath");
	scanf("%s %s", lpServerName, lpFileName);

	nRet = WSAStartup(wVersionRequested, &wsaData);

	int err;
	struct addrinfo hints, * res;
	SOCKET	Socket;
	//http
	//char port[] = "80";
	//https
	char port[] = "443";

	memset(&hints, 0, sizeof(hints));
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_family = AF_INET6;
	err = getaddrinfo(lpServerName,port,&hints,&res);
	if (err) {
		printf("err getaddrinfo %d\n",err);
		return;
	}

	Socket = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
	if (Socket == INVALID_SOCKET) {
		puts("err socket");
		return;
	}

	nRet = connect(Socket, res->ai_addr, res->ai_addrlen);
	if (nRet != 0)puts("err connect");
	char szBuffer[1024];
	//GET
	/*
	sprintf(szBuffer, "GET %s\n", lpFileName);
	nRet = send(Socket, szBuffer, strlen(szBuffer), 0);
	*/
	//client hello
	FILE* fp;
	fp= fopen("C:\\Users\\monst\\Desktop\\client_hello", "rb");
	nRet=fread(szBuffer, 1, 1024, fp);
	nRet = send(Socket, szBuffer, nRet, 0);
	if (nRet == -1)puts("err send");
	fclose(fp);

	while (1)
	{
		nRet = recv(Socket, szBuffer, sizeof(szBuffer), 0);
		//fprintf(stderr, "\nrecv() returned %d bytes", nRet);

		if (nRet == 0)
			break;
		fwrite(szBuffer, nRet, 1, stdout);
	}
	closesocket(Socket);
	freeaddrinfo(res);
	WSACleanup();
}



参考

winsockでTCPを使う

C#でAES暗号化アルゴリズムを外部ライブラリに一切頼らず完全実装してみた

C言語によるHttpRequestの送信

SSL 上の TCP 通信を行うサーバとクライアントの実装方法