TLSの勉強
ネットワークの通信はほとんど暗号化されています。
ブラウザが勝手に対応してくれているために何を行っているのかわかりません。
そこで今回はそのやり取りを見てみようと思います。
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)
なんかいろいろデータを送っています。
あー多すぎる。
こんどやろ。
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でやりとりしてとかさっぱりわかりません。
解説がありましたが、やっぱ難しそうです。
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のGETとhttpsの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を作成する)
ここら辺を参考にしながら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(); }
コメント
0 件のコメント :
コメントを投稿