作って学ぶ 「Https Man in The Middle Proxy」 in Go
ᕕ( ᐛ )ᕗ こんにちわ、しいたけです。
webのhttps化が推進される昨今ですね?
https通信は経路上での通信内容が盗聴・改竄されるのを防ぐことができますが、開発用途でhttps通信の内容を確認したい場合が稀にあります。
そのような場合は mitmproxy などを導入すればよいのですが、せっかくなので実際にこのようなProxyをGoで実装してみて、 中間者攻撃(Man-in-The-Middle Attack)がどのような手法でhttps通信を盗聴・改竄するのか確かめてみました。
実際に書いたProxyのコードはこちらです
https proxy と HTTP CONNECT tunneling
まず、通常のhttps Proxyの動作を確認してみましょう。
httpsでは、ProxyはクライアントからのCONNECTメソッドを受信すると、クライアントに代わって対象ホストとのTCPコネクションを確立し、以降はクライアントと対象ホストのTCP通信を転送します。クライアント-ホスト間のTLS接続のhandshakeもproxyを経由して行わます。
この方式により、Proxyを経由しつつクライアント-ホスト間でTLSセッションが確立され、Proxyを経由しつつも経路上では暗号化された通信が可能となります。
図にするとこんな感じです
実装
では具体的な実装を見てましょう。
まずはおなじみ ServeHTTP
です。クライアントから CONNECT
メソッドが送信されたら、通信を転送する relayHTTPSRequest
を呼び出します
https://github.com/yuroyoro/mitm_proxy_sample/blob/master/main.go#L34
func (proxy *MiTMProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // CONNECT メソッドが来たら if r.Method == http.MethodConnect { if proxy.mitm { proxy.mitmRequest(w, r) // Man in The Middleする } else { proxy.relayHTTPSRequest(w, r) // Tunnelingで通信を転送する } return } proxy.transportHTTPRequest(w, r) }
実際に通信を転送しているコードは以下のとおりです。処理の流れはコメントを読んでもらえばわかると思いますが、やっていることは単純です。
CONNECT
メソッドで指定されたホストにTCP接続を張って、クライアントからの通信をそのまま流し込むだけです
https://github.com/yuroyoro/mitm_proxy_sample/blob/master/https.go#L12
func (proxy *MiTMProxy) relayHTTPSRequest(w http.ResponseWriter, r *http.Request) { proxy.info("relayHTTPSRequest : %s %s", r.Method, r.URL.String()) // CONNECT先のHostへTCPコネクションを張る dest, err := net.Dial("tcp", r.Host) if err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) return } // http.Hicjacker を利用してクライアントとの生のTCPコネクションを取り出す conn := hijackConnect(w) // クライアントには200 OKを返す。これでクライアントはこのTCP接続にHTTPリクエストを送ってくる conn.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) proxy.info("relayHTTPSRequest : start relaying tcp packets %s %s", r.Method, r.URL.String()) // クライアント-対象Host間のTCP通信をそのまま中継する go transfer(dest, conn) go transfer(conn, dest) } func transfer(dest io.WriteCloser, source io.ReadCloser) { defer dest.Close() defer source.Close() io.Copy(dest, source) }
Goには http.Hijacker という便利なインターフェースがあり、 http.ResponseWriter
から生のクライアントとのTCP接続を取り出すことができます。
これを利用して、TCP通信の転送を行っています
func hijackConnect(w http.ResponseWriter) net.Conn { hj, ok := w.(http.Hijacker) if !ok { panic("httpserver does not support hijacking") } conn, _, err := hj.Hijack() if err != nil { panic("Cannot hijack connection " + err.Error()) } return conn }
実際はtimeoutなどを考慮した実装をすべきなのですが、これだけでも動きます。
Man in The Middel Proxyの仕組み
では、本題の中間者攻撃を行うProxyについてです。
通常のhttps proxyでは、クライアントからの CONNECT
メソッドを契機に、対象HostとのTCP通信を中継していました。
Proxyを流れる通信内容はTLSによって暗号化されており、内容を盗聴・改竄することはできません。
しかし、対象Hostとクライアント間のTLS handshakeもProxyを経由するので、この段階でクライアントからのTLS handshakeを、対象ホストになりすましてProxyが行うとどうなるでしょうか?
つまり、Proxyは対象ホストのサーバー証明書をその場で生成して署名し、クライアントに提示します。
もちろん、Proxyが署名したサーバー証明書は信頼できないCAのものとしてブラウザには警告が出ますが、そのままユーザーが続行することでTLS handshakeが成功します。
クライアントは確立したTLS接続を対象ホストとのものだと思いこんで、Proxyがクライアントに送り込んだニセのサーバー証明書の公開鍵で通信を暗号化するので、Proxyはその内容を復号することができます。
あとは、復号したリクエストをそのまま対象のホストに転送すれば、httpsにも関わらずProxyは通信内容を把握しつつ、対象ホストとの通信を取り持つことができてしまいます。
これで中間者攻撃が成立しますʕ ゚皿゚ ʔ 。
図にすると以下の流れとなります
通常、このような攻撃はブラウザが警告を出すために成立しません。
まず、ユーザーが明示的にブラウザにProxyを指定する必要がありますし(port forwardを利用した透過Proxyはその限りではない)、Proxyが署名に使用するルート証明書(または中間証明書)がTrust Chainにないからです。
逆に言えば、信頼できないルート証明書をシステムにインストールしてしまうと、このような攻撃が成立する余地が生まれてしまいます。
実際に、一部のセキュリティアプライアンスやアンチウィルスソフトウェアは、このような手法でhttps通信の内容をチェックしています。
Avastを入れた状態でブラウザで証明書チェーンを確認すると、 「Avast trusted CA」という謎の認証局が出現するのはこのためです( ;゚皿゚)ノシΣ フィンギィィーーッ!!!
以前、LenovoのPCにプリインストールされたアドウェア「Superfish」がルート証明書をシステムにインストールした上に、全PCで共通のCA秘密鍵を使っていたことで大問題になりましたね。
Dellでも似たようなことがあったみたいです( ꒪⌓꒪)
LenovoのPC全機種にプレロードされているアドウェアが実は恐ろしいマルウェアだった! | TechCrunch Japan DellのPCに不審なルート証明書、LenovoのSuperfishと同じ問題か - ITmedia エンタープライズ
実装
それでは具体的な実装の解説を行います。処理の流れは以下のとおりです。
- CONNECTメソッドのリクエストから、http.Hijackerを使って生のTCPコネクションを取り出す
- クライアントには200 okを返す
- 接続先ホストの証明書を、予め用意してあるroot証明書でサインして生成する
- 生成した証明書でクライアントとtls接続を確立する (root証明書が登録されていないとブラウザで警告が出る)
- goroutine起こして、クライアントとのtls接続からhttp requestを読み込む
- 受けたhttp requestをそのまま接続先hostに送信する
- 接続先hostからのhttp responseを、クライアントtls接続に書き込む
- EOFが来るまで 5-7繰り返し
https://github.com/yuroyoro/mitm_proxy_sample/blob/master/https.go#L57
func (proxy *MiTMProxy) mitmRequest(w http.ResponseWriter, r *http.Request) { // http.Hicjacker を利用してクライアントとの生のTCPコネクションを取り出す conn := hijackConnect(w) // クライアントに200 OKを返しておく conn.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) // 以降の処理はgoroutine上で行う go proxy.transportHTTPSRequest(w, r, conn) } func (proxy *MiTMProxy) transportHTTPSRequest(w http.ResponseWriter, r *http.Request, conn net.Conn) { proxy.info("transportHTTPSRequest : %s %s", r.Method, r.URL.String()) // 対象ホストのニセのサーバー証明書を生成して署名する host := r.Host tlsConfig, err := proxy.generateTLSConfig(host) if err != nil { if _, err := conn.Write([]byte("HTTP/1.0 500 Internal Server Error\r\n\r\n")); err != nil { proxy.error("Failed to write response : %v", err) } conn.Close() } // クライアントとのTCP接続上で、ニセのサーバー証明書を利用してTLS接続を待ち受ける tlsConn := tls.Server(conn, tlsConfig) if err := tlsConn.Handshake(); err != nil { proxy.error("Cannot handshake client %v %v", r.Host, err) return } defer tlsConn.Close() proxy.info("transportHTTPSRequest : established tls connection") // ニセの証明書で確立したTLS接続上でクライアントからのリクエストを読み込む tlsIn := bufio.NewReader(tlsConn) for !isEOF(tlsIn) { req, err := http.ReadRequest(tlsIn) // http.Requestオブジェクトとして通信を読み込む if err != nil { if err == io.EOF { proxy.error("EOF detected when read request from client: %v %v", r.Host, err) } else { proxy.error("Cannot read request from client: %v %v", r.Host, err) } return } proxy.info("transportHTTPSRequest : read request : %s %s", req.Method, req.URL.String()) // 転送用にURLやヘッダーなどを設定 req.URL.Scheme = "https" req.URL.Host = r.Host req.RequestURI = req.URL.String() req.RemoteAddr = r.RemoteAddr dumpRequest(req) removeProxyHeaders(req) // http.RoundTripper で受信したリクエストを対象ホストに転送し、レスポンスを受け取る resp, err := proxy.transport.RoundTrip(req) if err != nil { proxy.error("error read response %v %v", r.URL.Host, err.Error()) if resp == nil { http.Error(w, err.Error(), 500) return } } proxy.info("transportHTTPSRequest : transport request: %s", resp.Status) dumpResponse(resp) // レスポンスをクライントへのTLS接続に書き込む resp.Write(tlsConn) } proxy.info("transportHTTPSRequest : finished ") }
ポイントは、 リクエストを受けるとニセのサーバー証明書をその場で生成して、その証明書と http.Hijacker
で取り出したクライントのTCPで tls.Server
を用いてTLS接続をなりすますことです(生成した証明書はキャッシュします)。
証明書の生成は長くなるのでここには載せませんが、 こちら を見てもらえばと思います。
クライントとのTLS接続を乗っ取れば、あとはその接続上でHttpリクエストを読み込み、対象ホストに転送すればOKです。Goでは、 http.RoundTripper
を利用すれば http.Request
をそのまま転送できるので便利です。その際に、リクエスト・レスポンスの内容をdumpしています。
悪意があれば、この段階で改竄も可能でしょう。
まとめ
以上が、Man-in-The-Middle Attackを行う簡単なProxyの実装です。この攻撃が成功する条件としては、
の2点です。特に2点目はTLSの根幹をなす部分で、それゆえにルート証明書の管理は厳格に行う必要があり、GoogleはSymantecの証明書を無効にし、LenoveやDellは責められるべきなのです。Avastもちょっとどうかと思います。
実際に手を動かして実装してみると、Proxyの実装で注意スべき点や、TLSと認証局の仕組みとか色々と学びがあり、よかったとおもいました( ꒪⌓꒪)
ディスク使用量をFlameGraphで可視化する
こんにちわ。しいたけです。今日はディスク使用量をFlameGraphにするツールの話です。
FlameGraphについては、 Flame Graphs や GolangでFlame Graphを描く | SOTA を読んでもらうのが手っ取り早いのですが、ようはプロファイル結果を可視化する方法です。縦軸が呼び出しの階層に、横軸がサンプル数や実行時間などに対応しており、どの関数が支配的かを直感的に見ることができる優れたグラフですよ。
で、このFlameGraph、別にプロファイル結果だけではなく、ツリー構造で各ノードが量を持つ場合に、枝毎の累積量を可視化するのに利用できます。プロファイル以外に、ツリー構造でノードが量を持つ例として、ディレクトリ階層毎のディスク使用量が考えられます。
というわけで、指定ディレクトリ以下のディスク使用量をFlameGraph化するツールを書きました。
GitHub - yuroyoro/du-flamegraph: visualize disk usage as flamegraph
こんな感じのグラフが出力されます
goで書かれており、使い方は、 `go get -u yuroyoro/du-flamegraph` でインストールできます。
このツールは、 FlameGraphの描画に `flamegraph.pl` というスクリプトが必要であり、これは GitHub - brendangregg/FlameGraph: Stack trace visualizer にあります。
これを git cloneなどで手元に入れて、 $PATHに追加するか、 `--flamegraph-script` で位置を指定するかしてやれば、FlameGraph がsvgとして出力されます。
NAME: du-flamegraph - visualize disk usage as flamegraph USAGE: du-flamegraph [global options] [FILE] VERSION: 0.0.0 GLOBAL OPTIONS: --width value, -w value width of image (default 1200) (default: 1200) --height value, -h value height of each frame (default 16) (default: 16) --flamegraph-script value path of flamegraph.pl. if not given, find the script from $PATH --out value distination path of grenerated flamegraph. default is ./du-flamegraph.svg (default: "./du-flamegraph.svg") --verbose show verbose log --version, -v print the version
FlameGraph、色々と応用がききそうですね。
「commit-m: GitHubコミットメッセージの文例が検索できるサービス」がとても便利だったのでcliから使えるコマンド書いた
http://commit-m.minamijoyo.com/:titele という有名OSSのコミットメッセージを検索できるサービスがあって、英語のコミットメッセージを書くときに「あれ? これどういう風に書けばいいんダー」ってときに例文を検索できて捗る。
が、自分の場合はコミットメッセージ書くときはvim
とか git commit -m
とかからなのでCLIで検索できたらより捗るかと思ってGolangで書いた。
APIとかは無いようなのでクロールしてる。 GoQuery
使えばこの手のクローラーが一瞬でかけるのでよさがある。
go get github.com/yuroyoro/gommit-m
で入れた後に gommit-m keyword [page]
で検索できる。
GolangでSIMDプログラミング
以前から気になっていたSIMDプログラミングをGoでやってみた。
Single Instruction Multiple Data (SIMD) 演算とは1回の命令で複数のデータを同時に処理する演算です.近年の CPU には SIMD 演算を行うことができる SIMD 演算器が搭載されており,Intel 社の CPU ならば Streaming SIMD Extensions (SSE) を用いることで SIMD 演算を行うことが可能です.SSE は CPU に搭載されている 128bit レジスタを用いて演算を行うため単精度データならば4つ,倍精度データならば2つずつ演算を行うことができます.また,近年 SSE 後継の SIMD 拡張命令として Intel Advanced Vector eXtentions (AVX) が登場しました.AVX は第2世代 Intel Core i シリーズのプロセッサ (Sandy Bridge) から使用することが可能であり,演算幅が SSE の2倍の 256bit となっています.つまり,単精度データならば8つ,倍精度データならば4つずつ演算を行うことが可能です.
http://kawa0810.hateblo.jp/entry/20120303/1330797281
といっても、GoのコンパイラがSIMDを使ったバイナリを吐くかというとそうではないので、アセンブラなりCなりでSIMDを使うように書いてcgoから使う、という形になる。今後も、GoがSIMD対応することは無さそう(Google グループ)
アセンブラを書くのはつらい(というか書けない)ので、gcc組み込みのSIMD intrinsicsをcgoから使う。intrinsics関数を利用するとcからSSE/AVXを利用できる。今回はSandy Bridgeから利用できるAVXを使って、32bitのfloatの加算をやってみた。
注意点
cgoからintrinsicsを使う際には、注意しなければならないことがある。intrinsicsでSIMD演算を行う場合、
という流れになる。
AVXだと32bit * 8 = 256bitを一度にレジスタにロードして、結果を取り出すことになる。
ここで、メモリとレジスタのやりとりにおいては、対象アドレスが32byteにAlignされている必要がある。
32byte Alignとは、ポインタのアドレスが32で割り切れるようになっている、という意味。
intrinsicsでは、floatの配列を__m256という型にキャストすれば、レジスタとのやりとりをよしなにやってくれるようだが、このときのfloat配列のアドレスは、上述のように32byte alignedである必要がある。
さて、Goからintrinsics関数を利用するときに、このfloat配列のアドレスをどうするのか、という問題が発生する。
対策は2つ。
- C側でfloat配列を確保する
- GoでSliceを確保し、Sliceの先頭アドレスを渡す
C側で確保する場合は、話は簡単で、_mm_malloc関数を使うと指定bitでalignして確保できる。他にもgcc4.7からalignを指定する機能もある。ただし、Cで確保した配列をGoのSliceとして扱うには、ちょっとした変換が必要である。また、C側で確保したメモリなのでGoのGC管理下にない。解放のタイミングはプログラマが責任を持つ必要がある。ちょっと、つらい。
GoのSliceを利用する場合、確保されたアドレスが32byte alignedである保証はない。したがって、intrinsicsで非alignedでも利用できる命令(_mm256_loadu_ps)を用いる。この場合は、それなりのオーバーヘッドが発生する。
ベンチマーク結果
Cでメモリ確保する版(BenchmarkAvxAdd)、GoのSliceを渡す版(BenchmarkNonAlignedAvxAdd)、Goのforループで加算する版(BenchmarkGoAdd)でベンチマークを取ってみた。
BenchmarkAvxAdd | 3000000 | 415 ns/op |
BenchmarkNonAlignedAvxAdd | 1000000 | 1143 ns/op |
BenchmarkGoAdd | 1000000 | 2059 ns/op |
32byteにalignした場合が最も速くて、Goのループに比べて約5倍高速。32byte alignedで無い場合は、alignedに比べて2倍強遅くなっている。
ベンチマーク結果をみると、かなりの高速化が期待できる。
Goでwaifu2xのような画像処理を高速に書く場合は、一部の演算をSIMD化する、という最適化はありなのかも知れない。
参考
Intel AVX を使用して SIMD 演算を試してみる - kawa0810 のブログ
メモリアライメントを揃えずに SIMD する方法 - kawa0810 のブログ
SSEとAVXで高次元ベクトルの内積計算を高速化してみた | さかな前線
introdunction to SIMD programming - primitive: blog
組み込み関数(intrinsic)によるSIMD入門
gccでintrinsicsでSSEでベクタライズする時の簡易的なまとめ - ぬうぱんの備忘録
コンパイラー最適化入門: 第1回 SIMD 命令とプロセッサーの関係 | iSUS
Google グループ
Intel Intrinsics Guide
x86/x64 SIMD命令一覧表 (SSE~AVX2)
"err"という文字列をHighlightしておくとGolangのコードリーディングが捗る
vimの人はこんな感じで
autocmd FileType go :highlight goErr cterm=bold ctermfg=214 autocmd FileType go :match goErr /\<err\>/
golang勉強会で「cgoやってみた」という話をしてきた
Go lang勉強会 - connpassで発表してきた。
今までにblogに書いたcgoの話。
以下が資料とサンプルコード。
久しぶりに人前で発表した気がする。
当日はdemoもやるとなると時間が足りなくなるだろうという予想通りになり、ブランクを感じた。
あと、ネタ成分が不足気味だったのでリハビリしなければならない。
他の人の発表も面白かった。特にライセンスの話など。皆さんお疲れ様でした。
こんな勉強会なら、また参加したいものです。
ʕ ゚皿゚ ʔ GolangのASTを可視化するツールを作った
はじめてのGo Runtime。
ということで、GoのAST(抽象構文木)を可視化するツールを書いた。
yuroyoro/goast-viewer · GitHub
goast.yuroyoro.net にデモがある。
go/astパッケージを使うと、GoのソースコードからAST(抽象構文木)を得ることができる。
あとはこれをAngulerJSとか使ってみて可視化してみただけ。
ソースコードをアップロードするか、入力してparseボタンを押すと、右側にASTが展開される。マウスオーバーするとASTのnodeに該当するコードが選択状態になる。
以下の手順でインストールできるます
$ go get -d github.com/yuroyoro/goast-viewer $ cd $GOPATH/src/github.com/yuroyoro/goast-viewer $ make install
GoよりAngulerJSの方が難しかったʕ ゚皿゚ ʔ