弱編碼:程序之間的溝通語言安全嗎?(強(qiáng)編碼和弱編碼)
你好,我是王昊天。
進(jìn)入了加密失敗這個(gè)大篇章,我們的第一個(gè)話題就是——弱編碼。
如果你想了解什么是編碼,那么不妨想象一下雙十一購物的場(chǎng)景吧。
我們通過電商平臺(tái)購買了許多零食、家居用品以及二次元手辦,一時(shí)下單一時(shí)爽,一直下單一直爽,于是全國人民都在買買買。這個(gè)時(shí)候電商平臺(tái)的難題來了,各式各樣的商品要如何送到每個(gè)人手里呢?總不能每一種商品打造一條運(yùn)輸線路。
于是快遞出現(xiàn)了,通過對(duì)不同類型的商品進(jìn)行方形硬紙盒的封裝,既保護(hù)了商品在運(yùn)輸中的完整性,又保證了傳輸?shù)谋憬菪浴?/p>
這就是編碼的典型場(chǎng)景,在服務(wù)端與客戶端傳輸數(shù)據(jù)的過程中,我們無法確認(rèn)傳輸?shù)膬?nèi)容中是否包含傳輸協(xié)議不支持的內(nèi)容,因此在數(shù)據(jù)傳輸之前我們希望通過編碼的方式將傳輸數(shù)據(jù)進(jìn)行規(guī)范化。
這里一定要注意,編碼是不具備保密性的。就像快遞小哥只是不想知道包裝里面是什么東西,如果他想知道的話,應(yīng)該是一件不難事。
編碼
我們來看看維基百科是如何定義編碼的:
編碼是信息從一種形式或格式轉(zhuǎn)換為另一種形式的過程;解碼則是編碼的逆過程。
作為一名優(yōu)雅的開發(fā)工程師,或者是一名“大黑客”,掌握多種編碼特征都是非常重要的,這一講,我就來帶你進(jìn)入編碼的世界遨游一番。
字符編碼
字符編碼是把字符集中的字符映射為指定集合中的某一個(gè)對(duì)象,以便文本在計(jì)算機(jī)中存儲(chǔ)或者在網(wǎng)絡(luò)之間傳遞。在計(jì)算機(jī)發(fā)展的早期,ASCII這樣的字符集是字符編碼的標(biāo)準(zhǔn)形式,但是這些字符集有著很大的局限性,比如只適用于英文場(chǎng)景等,于是人們開發(fā)了許多方法來擴(kuò)展它們,編碼的類型也逐步豐富:
● 早期標(biāo)準(zhǔn):ASCII、EBCDIC
● 西歐標(biāo)準(zhǔn):ISO-8859-1、ISO-8859-5、ISO-8859-6、ISO-8859-7、ISO-8859-11、ISO-8859-15等
● DOS字符集:CP437、CP737、CP850等
● Windows字符集:Windows-1250、Windows-1251、Windows-1252等
● 中文:GB2312、GBK等
● Unicode:Unicode、UTF-7、UTF-8、UTF-16、UTF-32等
這些字符集有各自的誕生意義和應(yīng)用場(chǎng)景,在我們?nèi)粘9ぷ髦袝?huì)經(jīng)常遇到其中的某一些,這里我們選取幾個(gè)有代表性的字符集來深入研究:
ASCII
ASCII(American Standard Code for Information Interchange,美國信息交換標(biāo)準(zhǔn)代碼)是最常用的編碼,來表示字母、數(shù)字以及常用符號(hào)。如果你正在使用Mac或者Linux類型系統(tǒng),可以直接使用如下命令來查看所有的ASCII字符:
> man ascii
ASCII(7) BSD Miscellaneous Information Manual ASCII(7)
NAME
ascii — octal, hexadecimal and decimal ASCII character sets
DESCRIPTION
The octal set:
000 nul 001 soh 002 stx 003 etx 004 eot 005 enq 006 ack 007 bel
010 bs 011 ht 012 nl 013 vt 014 np 015 cr 016 so 017 si
020 dle 021 dc1 022 dc2 023 dc3 024 dc4 025 nak 026 syn 027 etb
030 can 031 em 032 sub 033 esc 034 fs 035 gs 036 rs 037 us
040 sp 041 ! 042 " 043 # 044 $ 045 % 046 & 047 '
050 ( 051 ) 052 * 053 054 , 055 – 056 . 057 /
060 0 061 1 062 2 063 3 064 4 065 5 066 6 067 7
…
ASCII的一個(gè)字符占8位(bit),第一位總是0,這種情況下能夠支持2的7次方也就是128個(gè)字符,其中00100000~01111110之間都是可打印字符。
GB 2312 & GBK
對(duì)于中文來說,漢字博大精深,區(qū)區(qū)128個(gè)字符肯定是不能夠滿足我們的需求的,于是就誕生了中文編碼。考慮到8位編碼是遠(yuǎn)遠(yuǎn)不夠的,并且需要與ASCII編碼兼容,GB2312編碼方法應(yīng)運(yùn)而生,它具有以下特征:
1. 使用兩個(gè)8位來進(jìn)行編碼;
2. 0~127編號(hào)的字符使用ASCII標(biāo)準(zhǔn)編碼;
3. 兩個(gè)大于127的字符連在一起時(shí)表示一個(gè)漢字,前一個(gè)稱為高字節(jié),后一個(gè)稱為低字節(jié)。
我們通常所說的全角字符就是雙字節(jié)字符,而單字節(jié)字符就是半角字符。但后來發(fā)現(xiàn)GB2312的編碼仍然不具備表示所有漢字的能力,于是我們就對(duì)上述第3個(gè)條件進(jìn)行了優(yōu)化,誕生了GBK編碼,這里K表示“擴(kuò)展”。優(yōu)化后第三點(diǎn)特征表示為:
3. 允許低字節(jié)使用0~127的字符,僅憑借高字節(jié)判斷是否為中文。
GB2312編碼示例:
你好hello123
xC4xE3xBAxC3x68x65x6Cx6Cx6Fx31x32x33
常見的GBK編碼:
你好hello123
xC4xE3xBAxC3x68x65x6Cx6Cx6Fx31x32x33
● Unicode & UTF-8
對(duì)于全球各國的文字來說,ASCII的字符集已經(jīng)不能滿足使用了,對(duì)于這個(gè)問題ISO提出了一個(gè)囊括全球所有文字的終極解決方案:Unicode。它最初規(guī)定所有的字符都是用兩個(gè)字節(jié)來表示,這個(gè)版本就是UTF-16;但是后面發(fā)現(xiàn)仍然不夠使用,于是擴(kuò)展到四個(gè)字節(jié),這個(gè)版本就是UTF-32。目前最新的Unicode已經(jīng)支持了emoji表情,讓我們的文字語言更加豐富且生動(dòng)。
但是所有的字符都使用Unicode來存儲(chǔ)是否會(huì)增大存儲(chǔ)成本呢?畢竟ASCII單字符只占用1個(gè)字節(jié),GBK也僅僅只占用2個(gè)字節(jié),如果全部使用UTF-32來表示,就意味著至少2倍存儲(chǔ)空間的膨脹,這時(shí)另一個(gè)新的編碼算法的出現(xiàn)解決了這個(gè)問題,并成為了在coding過程中廣泛使用的編碼類型——UTF-8。
UTF-8是一種變長編碼,比如對(duì)于ASCII碼它就用1個(gè)字節(jié)表示,面對(duì)其他類型的編碼就在前面加一個(gè)高位字節(jié)。通過這種方式,它在普遍英文coding但是攜帶中文注釋的環(huán)境中就顯得非常適合了。
Unicode編碼示例:
你好hello123
x00004F60x0000597Dx00000068x00000065x0000006Cx0000006Cx0000006Fx00000031x00000032x00000033
TF-8編碼示例:
你好hello123
xE4BDA0xE5A5BDx68x65x6Cx6Cx6Fx31x32x33
程序編碼
URL 編碼
URL編碼又稱百分號(hào)編碼,因?yàn)樗木幋a特征是以%開頭,是不是很形象?它主要用于統(tǒng)一資源定位符(URL)的編碼,也適用于統(tǒng)一資源標(biāo)識(shí)符(URI)的編碼。URI所允許的字符主要分為保留字符和未保留字符兩類:保留字符主要是那些具有特殊含義的字符,如! * &等;未保留字符,主要指不具備特殊含義的字符,如A B C等。
如果一個(gè)保留字符在上下文中是有意義的,并且需要在URI中按照內(nèi)容格式進(jìn)行展示,那么該字符就要使用百分號(hào)編碼。百分號(hào)編碼首先會(huì)把字符的ASCII值表示為兩個(gè)16進(jìn)制的數(shù)字,然后在其前面放置轉(zhuǎn)義字符%;對(duì)于非ASCII字符則先轉(zhuǎn)換為UTF-8字節(jié)序,然后再放置轉(zhuǎn)義字符%。
UTF-8格式百分號(hào)編碼示例:
你好hello123
你好hello123
Base64 編碼
Base64是一種用64個(gè)字符來表示二進(jìn)制數(shù)據(jù)的方法。由于 64 = 2 ^ 6,因此每6位可映射到一個(gè)可打印字符,又由于每6位等于四分之三字節(jié),因此可以簡單理解為每四分之三字節(jié)映射到一個(gè)新的字節(jié),這樣也就很容易能計(jì)算出base64的編碼膨脹率。Base64通常用于表示、傳輸以及存儲(chǔ)二進(jìn)制數(shù)據(jù)。
簡單思考一下Base64的規(guī)則,會(huì)發(fā)現(xiàn)一個(gè)有趣的事情:如果要編碼的字節(jié)數(shù)不能被3整除,那么就會(huì)無法進(jìn)行Base64編碼。所以完整的Base64編碼規(guī)則是先使用“0”將不足的字節(jié)數(shù)在末尾補(bǔ)足,使其能夠被3整除,然后再進(jìn)行Base64的編碼。增加的字節(jié)數(shù)在末尾用等同數(shù)量的“=”進(jìn)行標(biāo)記。
base64編碼示例:
你好hello123
5L2g5aW9aGVsbG8xMjM=
編碼 v.s. 加密
通過對(duì)編碼的一些討論,我們已經(jīng)了解到編碼的一些特性,這里我們將編碼與我們上節(jié)課學(xué)過的加密做一下簡單的對(duì)比,看看它們有什么相同和不同。
● 編碼與加密都是可逆運(yùn)算:
通過對(duì)編碼數(shù)據(jù)進(jìn)行解碼即可恢復(fù)原始數(shù)據(jù);對(duì)加密數(shù)據(jù)解密我們同樣可以獲得原始數(shù)據(jù)。
● 編碼只需要1個(gè)輸入,而加密需要2個(gè)輸入:
選定編碼函數(shù)之后,我們只需要選擇待編碼數(shù)據(jù)即可;而對(duì)于加密函數(shù),除了待加密數(shù)據(jù)以外,我們還需要選擇加密密鑰。
● 編碼的目的是方便數(shù)據(jù)交互,加密的目的是保護(hù)數(shù)據(jù)交互:
通過編碼可以將數(shù)據(jù)在不同協(xié)議系統(tǒng)之間進(jìn)行流轉(zhuǎn),目的在于可用性;通過加密可以將數(shù)據(jù)安全地傳輸,目的在于機(jī)密性。
編碼 v.s. 轉(zhuǎn)義
通常,轉(zhuǎn)義是很容易與編碼混淆的概念。因?yàn)榕c加密相比,轉(zhuǎn)義同時(shí)具備只需要一個(gè)輸入,可逆運(yùn)算兩個(gè)條件。但是轉(zhuǎn)義與編碼的使用場(chǎng)景是不同的,即它們的“目的”不同。
與編碼便于數(shù)據(jù)交互的目的不同,轉(zhuǎn)義通常有兩個(gè)目的:
1. 編碼一個(gè)語句上的實(shí)體,比如設(shè)備命令或者無法被打印字符直接表示的特殊數(shù)據(jù);
2. 作為特殊字符引用,主要用于表示無法在當(dāng)前上下文中以可打印形態(tài)錄入的字符,比如回車符。
轉(zhuǎn)義字符開頭的字符序列被叫做轉(zhuǎn)義序列,通常一個(gè)轉(zhuǎn)義字符并沒有它自己的意思,因此轉(zhuǎn)義序列一般具有2個(gè)或更多字符。
通過判斷二者的目的,我們可以很容易對(duì)編碼和轉(zhuǎn)義進(jìn)行區(qū)分。
案例實(shí)戰(zhàn)
了解了編碼的基礎(chǔ)知識(shí),接下來我們一起來研究幾個(gè)與編碼相關(guān)的安全問題。這幾個(gè)實(shí)戰(zhàn)案例都已經(jīng)搭建在MiTuan,搜索【編碼漏洞合集】就可以直接使用。
寬字節(jié)注入
啟動(dòng)靶機(jī)之后,我們可以直接看到一個(gè)支持HTTP GET請(qǐng)求的頁面,頁面上告訴了我們這個(gè)示例漏洞內(nèi)部的代碼邏輯:程序內(nèi)部通過addslashes函數(shù),對(duì)用戶GET請(qǐng)求中的str參數(shù)進(jìn)行處理,然后拼接到SQL語句中,同時(shí)頁面上也將打印實(shí)際執(zhí)行的SQL語句,方便我們對(duì)漏洞利用過程進(jìn)行調(diào)試。
那么接下來我們就開始嘗試?yán)眠@個(gè)潛在的SQL注入漏洞。
第一步是尋找注入點(diǎn)。由于這個(gè)頁面僅支持str這一個(gè)參數(shù)的輸入,因此我們可以判斷注入點(diǎn)應(yīng)該就在這里。我們可以先嘗試一些常規(guī)的注入方式來看一下頁面的處理結(jié)果。比如,通過嘗試1 1'這兩種不同的輸入,我們發(fā)現(xiàn)經(jīng)過addslashes函數(shù)的處理,SQL語句并沒有被閉合,這種情況下我們是不能執(zhí)行注入的。
雖然1'這個(gè)參數(shù)并沒有達(dá)到讓SQL語句閉合的目標(biāo),但是這一次SQL語句的構(gòu)造可以給我們一些新的啟發(fā):
select * from user where user='1''
通過這個(gè)完整的SQL語句,我們可以發(fā)現(xiàn) 1 與 是連續(xù)字符,這種情況下如果將 1 修改為特殊字符,使其能夠通過編碼組合與 組成新的字符,我們就能實(shí)現(xiàn)編碼繞過。
第二步就是實(shí)踐我們的想法,找出一個(gè)能與 組成新的字符的特殊字符。
通過編碼工具,可以得知 的GBK編碼是 x5C ,經(jīng)過剛剛的學(xué)習(xí)我們知道了GBK編碼中漢字編碼的特征,所以我們只需要選取一個(gè)合適的高位字節(jié)即可。比如,這里我選擇了 xC4 ,通過編碼工具我們可以知道 xC4x5C 是漢字 腬 ,因此拼接完成之后的完整內(nèi)容 xC4x5Cx26x23x33x39x3B 即可滿足要求。
通過這些操作,我們將 1 替換為 ? 即可實(shí)現(xiàn)第一步中我們的編碼繞過設(shè)想。
第三步很簡單,將 ? 作為參數(shù)輸入GET請(qǐng)求即可。要注意GET請(qǐng)求中的str參數(shù)需要應(yīng)用URL編碼格式,而想要得到GB2312的URL編碼,只需在前面增加“%”符號(hào)即可。因此將 ? 與 ' 一起拼接,得到的完整參數(shù)是 ?\’ 。
將我們構(gòu)造的完整參數(shù)輸入瀏覽器地址欄進(jìn)行訪問,可以得到頁面的輸出:
select * from user where user='腬''
接下來可以進(jìn)一步增加其他SQL控制字符進(jìn)行注入動(dòng)作:
str=?\’#
select * from user where user='腬'#'
CVE-2021-42574
這是一個(gè)由劍橋大學(xué)的研究人員發(fā)現(xiàn)的漏洞,它由編碼問題引起,常見于供應(yīng)鏈污染類型漏洞。在介紹漏洞原理之前我們先來和它進(jìn)行一個(gè)親密接觸:
#include <stdio.h>
#include <stdbool.h>
int main() {
bool isAdmin = false;
/* begin admins only */ if (isAdmin) {
printf("You are an admin.n");
/* end admins only */ }
return 0;
}
上述C代碼邏輯十分簡單,核心邏輯是判定isAdmin的bool類型并執(zhí)行相應(yīng)動(dòng)作。按照isAdmin的初始化數(shù)值,函數(shù)應(yīng)該直接進(jìn)入return邏輯,不產(chǎn)生任何輸出。這里我們直接運(yùn)行:
$> clang program.c && ./a.out
You are an admin.
神奇的事情出現(xiàn)了,盡管isAdmin的值為False,程序仍然執(zhí)行了if判斷分支內(nèi)部的函數(shù)。
聰明的你知道這是為什么嗎?
其實(shí)奧秘就在“控制字符”上。通過使用Unicode控制字符,我們可以將編碼的順序進(jìn)行視覺效果上的反轉(zhuǎn)。比如上面的示例代碼,其真實(shí)代碼如下:
#include <stdio.h>
#include <stdbool.h>
int main() {
bool isAdmin = false;
/*RLO } LRIif (isAdmin)PDI LRI begin admins only */
printf("You are an admin.n");
/* end admins only RLO { LRI*/
return 0;
}
可以看到在真實(shí)的代碼中,if 語句完全被注釋符號(hào)包裹,根本不存在真實(shí)判斷邏輯。
那么為什么Unicode要設(shè)置這么惡意的“欺騙性”字符呢?
其實(shí)并非Unicode有惡意,這里我們回顧一下Unicode誕生的原因——囊括全球文字的終極編碼方案。人類社會(huì)的文化是非常豐富的,以語言文字為例,既有像漢字這樣按照從左到右順序讀寫的文字,也有像阿拉伯語這樣從右到左讀寫的文字,因此為了滿足這種文字應(yīng)用場(chǎng)景,Unicode提供了影響閱讀順序的控制字符。
由于近些年供應(yīng)鏈污染攻擊盛行,一旦黑客入侵軟件廠商代碼庫或者污染了具有廣泛應(yīng)用的開源項(xiàng)目,就會(huì)造成巨大的安全威脅。
總結(jié)
這節(jié)課我們學(xué)習(xí)了加密失敗的另一種安全風(fēng)險(xiǎn)形式——弱編碼。
事實(shí)上關(guān)于編碼的安全問題很多,主要是由于對(duì)編碼和加密的算法理解有誤所致,弱編碼僅僅是一個(gè)淺層問題的縮影。通過了解編碼的本質(zhì)——信息格式的轉(zhuǎn)換,就可以區(qū)分開編碼與加密,進(jìn)而就可以選擇合適的使用場(chǎng)景。
從弱編碼這一淺層安全問題入手,這節(jié)課我們進(jìn)一步解讀了一些主流的編碼標(biāo)準(zhǔn),讓我們可以快速識(shí)別數(shù)據(jù)所屬的編碼類別:像ASCII占位1個(gè)字節(jié),共8bit,能夠描述128個(gè)字符,適用于英文場(chǎng)景;GB2312與GBK占位2個(gè)字節(jié),共16bit,用于中文場(chǎng)景,GBK是GB2312的擴(kuò)展;Unicode與UTF-8則更為宏大,用于描述全球各國的文字,并且UTF-8具有變長的特征。
在了解了字符編碼的基礎(chǔ)上,我們進(jìn)一步探討了常見的程序編碼:像URL編碼,其特征是以%開頭,因此又稱百分號(hào)編碼,其編碼結(jié)果與GBK和UTF-8的原始編碼是非常相似的;而Base64編碼,其特征是編碼結(jié)果均為可打印字符,并且編碼結(jié)果末尾可能存在=符號(hào),主要適用場(chǎng)景是二進(jìn)制數(shù)據(jù)的傳遞;再進(jìn)一步擴(kuò)展的話,其他Base編碼也有相似之處。
與編碼相關(guān)的更多深層次安全問題,是與編碼轉(zhuǎn)換以及轉(zhuǎn)義字符處理相關(guān)的,因此在實(shí)戰(zhàn)案例部分我選擇了2個(gè)漏洞帶你深入探究編碼安全問題:
1. 寬字節(jié)注入問題,其發(fā)生的根源在于數(shù)據(jù)與命令的結(jié)合,但直接導(dǎo)火索是字符處理函數(shù)考慮不全,對(duì)于編碼轉(zhuǎn)換場(chǎng)景未經(jīng)過嚴(yán)密的處理,產(chǎn)生了編碼繞過的后果;
2. Unicode字符序列問題,以CVE-2021-42574為例,其發(fā)生的根源是IDE在渲染Unicode編碼過程中進(jìn)行了控制字符解析,造成了開發(fā)人員理解代碼錯(cuò)誤引入后門或其他安全威脅。
通過這節(jié)課的學(xué)習(xí),我們可以發(fā)現(xiàn)編碼看似是非程序開發(fā)問題,但是涉及的知識(shí)和原理非常廣泛,同時(shí)引入的安全問題由于其邏輯晦澀也不易被發(fā)現(xiàn)。因此在coding過程中,深刻理解編碼的作用以及程序內(nèi)部執(zhí)行過程的編碼邏輯十分重要,考慮到編碼引入的安全問題相對(duì)隱蔽,我們也可以考慮在項(xiàng)目中引入優(yōu)秀的SAST工具協(xié)助發(fā)現(xiàn)和定位編碼層的安全問題。
思考題
除了這一講中我們提到了兩種編碼漏洞,還有一種同形字符編碼漏洞,CVE-ID是CVE-2021-42694,你可以自己完成漏洞追蹤及分析嗎?
歡迎在評(píng)論區(qū)留下你的思考,我們下節(jié)課再見。
內(nèi)容來源:《Web漏洞挖掘?qū)崙?zhàn)》(https://time.geekbang.org/column/article/474244?utm_source=zmt&utm_medium=zmt&utm_term=zmt)