由蘋果的低級Bug想到的
2014 年 2 月 22 日,在這個“這么二”的日子里,蘋果公司推送了 iOS 7.0.6(版本號 11B651)修復了 SSL 連接驗證的一個 bug。官方網頁在這里:http://support.apple.com/kb/HT6147,網頁中如下描述:
Impact: An attacker with a privileged network position may capture or modify data in sessions protected by SSL/TLS
Description: Secure Transport failed to validate the authenticity of the connection. This issue was addressed by restoring missing validation steps.
</blockquote>也就是說,這個 bug 會引起中間人攻擊,bug 的描述中說,這個問題是因為 miss 了對連接認證的合法性檢查的步驟。
這里多說一句,一旦網上發生任何的和 SSL/TL 相關的 bug 或安全問題,不管是做為用戶,還是做為程序員的你,你一定要高度重視起來。因為這個網絡通信的加密協議被廣泛的應用在很多很多最最需要安全的地方,如果 SSL/TLS 有問題的話,意味著這個世界的計算機安全體系的崩潰。
Bug 的代碼原因
Adam Langley 的《Apple’s SSL/TLS bug 》的博文暴出了這個 bug 的細節。(在蘋果的開源網站上,通過查看蘋果的和 SSL/TLS 有關的代碼變更,我們可以在文件 sslKeyExchange.c 中找到下面的代碼)
static OSStatus SSLVerifySignedServerKeyExchange (SSLContext ctx, bool isRsa, SSLBuffer signedParams, uint8_t signature, UInt16 signatureLen) { OSStatus err; ...if ((err = SSLHashSHA1.update (&hashCtx, &serverRandom)) != 0) goto fail; if ((err = SSLHashSHA1.update (&hashCtx, &signedParams)) != 0) goto fail; goto fail; if ((err = SSLHashSHA1.final (&hashCtx, &hashOut)) != 0) goto fail; err = sslRawVerify (ctx, ctx->peerPubKey, dataToSign, /* plaintext */ dataToSignLen, /* plaintext length */ signature, signatureLen); if(err) { sslErrorLog ("SSLDecodeSignedServerKeyExchange: sslRawVerify "
"returned %d\n", (int) err); goto fail; }
fail: SSLFreeBuffer (&signedHashes); SSLFreeBuffer (&hashCtx); return err; }</pre>
注意,我高亮的地方,也就是那里有兩個 goto fail; 因為 if 語句沒有加大括號,所以,只有第一個 goto 是屬于 if 的,而第二個 goto 則是永遠都會被執行到的(注:這里不是 Python 是C語言,縮進不代表這個語句屬于同一個語句塊)。也就是說,就算是前面的 if 檢查都失敗了(err == 0),也會 goto fail。我們可以看到 fail 標簽中釋放完內存后就會 return err;
你想一下,這段程序在 SSLHashSHA1.update () 返回成功,也就是返回 0 的時候會發生什么樣的事?是的,真正干活的 sslRawVerify ()被 bypass 了。而且這個函數 SSLVerifySignedServerKeyExchange () 還返回了0,也就是成功了!尼瑪!你可能想到酷殼網上之前《一個空格引發的慘劇》的文章。都是低級 bug。
這個低級 bug 在這個周末在網上被炒翻了天,你可以上 Twiter 上看看#gotofail 的標簽的盛況。Goto Fail 必然會成為歷史上的一個經典事件。
如果你喜歡 XKCD,你一定會想到這個漫畫:
注意:這個 bug 不會影響 TLS 1.2 版本,因為 1.2 版本不會用這個函數,走的是另一套機制。但是別忘了 client 端是可以選擇版本的。
如果你想測試一下你的瀏覽器是否會有問題,你可以上一下當天就上線的 https://gotofail.com 網站
一些思考
下面是我對這個問題的一些思考。
0)關于編譯報警
有人在說蘋果的這個代碼中的 goto 語句會產生死代碼——dead code,也就是永遠都不會執行到的代碼,C/C++的編程器是會報警的。但,實際上,dead code 在默認上的不會報警的。即使你加上-Wall,GCC 4.8.2 或 Clang 3.3 都不會報警,包括 Visual Studio 2012 在默認的報警級別也不會(默認是/W3 級,需要上升到/W4 級以上,但是升級到/W4 上,你的工程可能會有N多的 Warning,你不一定能看得過來)。gcc 和 Clang 有一個參數叫:-Wunreachable-code,是可以對這種情況報警的,但即沒有被包括在-Wall 里。原因是,這個參數有很多的問題,因為編譯器的優化代碼的行為,這個參數并不能對每種情況都準確地報告。另請注意,GCC 的新版本中剔除了這個參數。當然,其它一些靜態的代碼檢查工具也可以檢查這個低級的問題。
另外,是不是用 IDE 的代碼自動化格式工具也可以幫上一點忙呢?至少可以把那個縮進變成讓人一看就覺得有問題。
1)關于 Code Merge 和 Code Review
你可以通過這里的代碼比較看到這個 bug 的 diff,也可以到這里看看(631 行)。
diff -urN <(curl -s http://opensource.apple.com/source/Security/Security-55179.13/libsecurity_ssl/lib/sslKeyExchange.c\?txt) \ <(curl -s http://opensource.apple.com/source/Security/Security-55471/libsecurity_ssl/lib/sslKeyExchange.c\?txt) \
</blockquote>通過 code diff 你可以看到,蘋果公司是在重構代碼——為很多函數去掉了 ctx 的參數。
所以,我們可以猜測,兩個 goto fail 語句,可能是因為對 code 在不同 branch 上做 merge 發生的。版本工具 merge 代碼的時候,經常性的會出現這樣的問題。如果代碼的 diff 很多,這個問題會很容易就沒有注意到。就算有 code review,這個有問題的代碼也很難被找出來的。如果你來 review 下面的 diff,你會注意到這個錯誤嗎?
也就是說,在重構分支上的代碼是對的,但是在分支 merge 的時候,被 merge 工具搞亂了。所以說,我們在做 code merge 的時候,一定要小心小心再小心,不能完全相信 merge 工具。
2)關于測試
很明顯,這個 bug 很難被 code review 發現。對于重構代碼和代碼 merge 里眾多的 diff,是很難被 review 的。
當然,“事后諸葛亮”的人們總是很容易地說這個問題可以被測試發現,但是實際情況是這樣的嗎?
這個問題也很難被功能測試發現,因為這個函數在是在網絡握手里很深的地方,功能測試不一定能覆蓋得那么深,你要寫這樣的 case,必需對 TLS 的協議棧非常熟悉,熟悉到對他所有的參數都很熟悉,并能寫出針對每一個參數以及這些參數的組合做一堆 test case,這個事情也是一件很復雜的事。要寫出所有的 case 本身就是一件很難很難的事情。關于這個叫 SSLVerifySignedServerKeyExchange ()函數的細節,你可以看看相關的 ServerKeyExchange RFC 文檔。
如果只看這個問題的話,你會說對這個函數做的 Unit Test 可以發現這個問題,是的。但是,別忘了 SSL/TLS 這么多年了,這些基礎函數都應該是很穩定的了, 在事前,我們可能不會想到要去為這些穩定了多少年的函數寫幾個 Unit Test。
只要有足夠多的時間,我們是可以對所有的功能點,所有的函數都做 UT,也可以去追求做代碼覆蓋和分支覆蓋一樣。但有一點我們卻永遠無法做到,那就是——窮舉所有的負面案例。所以,對于測試來說,我們不能走極端,需要更聰明的測試。就像我在《我們需要專職的 QA》文章里的說過的——測試比 coding 難度大多了,測試這個工作只有高級的開發人員才做得好。我從來不相信不寫代碼的人能做好測試。
這里,我并不是說通過測試來發現這個問題的可能性不大,我想說的是,測試很重要,單測更重要。但是,我們無法面面俱到。在我們沒有關注到的地方,總會發生愚蠢的錯誤。
P.S.,在各大網站對這個事的討論中,我們可以看到 OS X 下的 curl 命令居然可以接受一個沒有驗證過的 IP 地址的 https 的請求,雖然現在還沒有人知道這事的原因,但是,這可能是沒有在測試中查到的一個原因。
3)關于編碼風格
對于程序員來說,在C語言中,省掉語句大括號是一件非常不明智的事情。如我們強制使用語句塊括號,那么,這兩個 goto fail 都會在一個 if 的語句塊里,而且也容易維護并且易讀。(另外,通過這個 bug,我們可以感受到,像 Python 那樣,用縮進來表示語句塊,的確是挺好的一件事)
也有人說,如果你硬要用只有單條語句,且不用語句塊括號,那么,這就是一條語句,應該放在同一行上。如下所示:
if (check_something) do_something ();
但是這樣一來,你在單步調試代碼的時候,就有點不爽了,當你 step over 的時候,你完全不知道 if 的條件是真還是假。所以,還是分多行,加上大括號會好一些。
相似的問題,我很十多年前也犯過,而且那次我出的問題也比較大,導致了用戶的數據出錯。那次就是維護別人的代碼,別人的代碼就是沒有 if 的語句塊括號,就像蘋果的代碼那樣。我想在 return z 之前調用一個函數,結果就杯具了:
if ( ...... ) return x;if ( ...... ) return y;if ( ...... ) foo (); return z;這個錯誤一不小心就犯了,因為人的大腦會相當然地認為縮進的都是一個語句塊里的。但是如果原來的代碼都加上了大括號,然后把縮進做正常,那么對后面維護的人會是一個非常好的事情。就不會犯我這個低級錯誤了。就像下面的代碼一樣,雖然寫起來有點羅嗦,但利人利己。
if ( ...... ){ return x; }if ( ...... ){ return y; }if ( ...... ){ return z; }與此類似的代碼風格還有如下,你覺得哪個更容易閱讀呢?
- if (!p) 和 if (p == NULL)
- if (p) 和 if (p != NULL)
- if (!bflag) 和 if (bflag == false)
- if ( CheckSomthing () ) 和 if ( CheckSomething () == true )
所以說,代碼不是炫酷的地方是給別人讀的。
另外,我在想,為什么蘋果的這段代碼不寫成下面這樣的形式?你看,下面這種情況不也很干凈嗎?
if ( (err = ReadyHash (&SSLHashSHA1, &hashCtx)) != 0 ) || (err = SSLHashSHA1.update (&hashCtx, &clientRandom)) != 0) || (err = SSLHashSHA1.update (&hashCtx, &serverRandom) != 0) || (err = SSLHashSHA1.update (&hashCtx, &signedParams) != 0) || (err = SSLHashSHA1.final (&hashCtx, &hashOut)) != 0)) { goto fail; }其實,還可以做一些代碼上的優化,比如,把 fail 標簽里的那些東西寫成一個宏,這樣就可以去掉 goto 語句了。
4)關于 goto 語句
關于 goto 語句,1968 年,Edsger Dijkstra 投了一篇文章到 Communications of the ACM。原本的標題是《A Case Against the Goto Statement》。CACM 編輯 Niklaus Wirth 靈感來了,把標題改為我們熟知的 《Go To Statement Considered Harmful》Dijkstra 寫的內容也是其一貫的犀利語氣,文中說:“幾年前我就觀察到,一個程序員的品質是其程序中 goto 語句的密度成反比的”,他還說,“后來我發現了為什么 goto 語句的使用有這么嚴重的后果,并相信所有高級語言都應該把 goto 廢除掉。” (花絮:因為,這篇文章的出現,計算學界開始用’ X considered harmful ’當文章標題的風潮,直到有人終于受不了為止)
為什么 goto 語句不好呢?Dijkstra 說,一個變量代表什么意義要看其上下文。一個程序用N
記錄房間里的人數,在大部分時候,N
代表的是“目前房間里的人”。但在觀察到又有一個人進房間后、把N
遞增的指令前的這段程序區塊中,N
的值代表的是“目前房間里的人數加一”。因此,要正確詮釋程序的狀態,必須知道程序執行的歷史,或著說,知道現在“算到哪”了。
怎么談“算到哪了”?如果是一直線執行下來的程序,我們只要指到那條語句,說“就是這里”,就可以了。如果是有循環程序,我們可能得說:“現在在循環的這個地方,循環已經執行了第
i
次”。如果是在函數中,我們可能得說:“現在執行到函數p
的這一點;p
剛剛被q調用
,調用點在一個循環中,這個循環已經執行了i
次”。如果有 goto
語句了
呢?那就麻煩了。因為電腦在執行某個指令前,可能是從程序中許許多多 goto其中之一跳過來的。要談某變量的性質也幾乎變得不可能了。這就是為什么 goto 語句問題。
Dijkstra 的這篇文章對后面很多程序員有非常深的影響,包括我在內,都覺得 Goto 語句能不用就不用,雖然,我在十年前的《編程修養》(這篇文章已經嚴重過時,某些條目已經漏洞百出)中的第 23 條也說過,我只認為在 goto 語句只有一種情況可以使用,就是蘋果這個 bug 里的用法。但是我也同意 Dijkstra,goto 語句能不用就不用了。在更為高級的 C++ 中,使用 RAII 技術,這樣的 goto 語句已經沒有什么存在的意義了。
Dijkstra 這篇文章后來成為結構化程式論戰最有名的文章之一。長達 19 年之后,Frank Rubin 投了一篇文章到 CACM,標題為《‘
Go To Considered Harmful’ Considered Harmful 》Rubin 說,「雖然 Dijkstra 的說法既太學術又缺乏說服力」,卻似乎烙到每個程序員的心里了。這樣,當有人說“用 goto 語句來解這題可能會比較好”會被嚴重鄙視。于是 Rubin 出了一道這樣的題:令
X
為N * N
的整數陣列。如果X
的第i
行全都是零,請輸出i
。如果不只一行,輸出最小的i
.Rubin 找了一些慣用 goto 和不用 goto 的程序員來解題,發現用 goto 的程序又快又清楚。而不用 goto 通常花了更多的時間,寫出很復雜的解答。你覺得呢? 另外,你會怎么寫這題的程序呢?
(花絮:以后幾個月的 CACM 熱鬧死了。編輯收到許多回應,兩個月后刊出了其中五篇。文章也包括了《“‘GOTO Considered Harmful’ Considered Harmful” Considered Harmful? 》)
對于我而言,goto 語句的弊遠遠大于利,在 99% 的情況下,我是站在反 goto 這邊的。
(花絮:這段時間,我在開發 Nginx 的模塊,因為以前沒有做過,而且 Nginx 的開發文檔也不好,所以就得讀一些別人的源代碼。當我看了一個某同學開發的 nginx redis 的模塊里的這段代碼 ngx_http_redis2_reply.c 看 到里面飛沙走石的 goto 語句,我崩潰了!雖然,有網友指出這是代碼自動生成工具生成出來的(這樣的代碼放在源碼庫里跟放個可執行文件有什么差別?),但是不能不說我看到這樣的代 碼的時候,就像我在某個餐館看到了他那骯臟的廚房,無論你做菜的技藝有多高超,做的菜做得有多好看多好吃,我都惡心得一點也不想吃了)
總結
你看,我們不能完全消滅問題,但是,我們可以用下面幾個手段來減少問題:
1)盡量在編譯上發生錯誤,而不是在運行時。
2)代碼是讓人讀的,順便讓機器運行。不要怕麻煩,好的代碼風格,易讀的代碼會減少很多問題。
3)Code Review 是一件很嚴肅的事情,但 Code Reivew 的前提條件是代碼的可讀性一定要很好。
4)測試是一件很重要也是很難的事情,尤其是開發人員要非常重視。
5)不要走飛線,用飛線來解決問題是可恥的!所以,用 goto 語句來組織代碼的時代過去了,你可以有很多種方式不用 goto 也可以把代碼組織得很好。
最后,我在淘寶過去的一年里,經歷過一些 P1/P2 故障,尤其是去年的8-9 月份故障頻發的月份,我發現其中有 70% 的 P1/P2 故障,就是因為沒有 code review,沒有做好測試,大量地用飛線來解決問題,歸根結底就是只重業務結果,對技術沒有應有的嚴謹的態度和敬畏之心。
正如蘋果的這個“goto fail”事件所暗喻的,如果你對技術沒有應有的嚴謹和敬畏之心,你一定會——
Go To Fail !!!
在這里嘮叨這么多,與大家共勉!
來自: coolshell.cn<span id="shareA4" class="fl"> </span>
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!