本文將簡要介紹Jscript 9 (Charka)引擎處理數字、對象、函數操作的一些內容。
IE9開始,微軟使用了全新的JavaScript引擎—工程代號Charka版本9的JavaScript引擎。而IE8中它的版本還是5,也就是說5到9之間其實沒有什么其他版本了,既然版本號跳躍如此之大,那Charka(下均稱Jscript 9)到底有什么改進呢?
首先,與Jscript 5不同的是,JScript9引入了一個新的JIT,由此指令的執行變得更加線性化,不用像之前一樣跳來跳去的執行各個分支,源代碼經過解析器生成AST,然后經過字節碼生成器產生字節碼并由其JIT解釋成機器碼并執行。也達到了之前類似Malzilla Firefox所稱Javascript執行性能已經達到原生C++編譯出的代碼的水平。
本文中我們將關注:
大改之后的JScript,由于引入了字節碼執行方式,相對于之前的解釋型執行,必然會為攻擊者提供一些新的思路—ROP。
字節碼的編譯過程中,如果像編譯普通EXE一樣產生字節碼,必然會遇到如下的問題:
在微軟的層層保護之下,如果字節碼不設防,那么從字節碼中進行ROP將會是一個十分輕松愉悅的過程。由攻擊者控制的JS代碼(和生成的字節碼)中產生Gadget將會十分簡單,這個可能給UAF類型的漏洞的利用常嚴重的影響,會大幅降低攻擊者攻擊的難度。
因此,在JScript9的JIT引擎中,引入了大量的安全概念1:
(1)代碼是默認開啟DEP的;
(2)代碼對齊隨機化,字節碼存放在各個新申請的內存中,“頂格”放可能會被預測地址,因此在對齊方面預留了這樣的一手;
(3)隨機NOP插入,也是同樣防止攻擊者預測指令地址的,在調試JIT生成的字節碼中,你也許會發現有很多莫名其妙的指令,例如mov ecx,ecx, lea ecx, [ecx],不用理會,這些都是它插入的NOP;
(4)常量加密存儲,你會發現DWORD值在字節碼中并不是按照原樣存放的,而是xor了一個隨機的值存放,并在使用它的時候再次xor解開;
(5)以及JIT內存分配的上限,以及頁的隨機化。
讓我們看一下最為直觀的(4),在閱讀NDSS15的一篇論文時,我關注到了作者所述“Charka中小于2字節的常量都是XOR一個值并存放起來,并在使用時解開”。但是在之后的調試中我發現這個描述是不正確的。看起來也是作者的筆誤。
經過調試,可知整數的存取整理如下:
32位IE中,
(1)2 byte以上32位整數常量全部以(n * 2 + 1) xor (some value)過的形式存在內存中;xor key隨機,需要一段代碼解開才能繼續使用;
(2)n > 0x7fffffff的值無法* 2 + 1放在EXX寄存器中了,這個時候會轉入SSE寄存器中(xmmX)來處理;
(3)2 byte以下常量將以n * 2 + 1的方式保存,并在需要使用時將其右移一位再使用。
以下是具體的例子:
2 byte以下常量將以n * 2 + 1的方式保存,并在需要使用時將其右移一位再使用。
正常來說,32位整數,包括負數在Chakra中均采用有符號數來處理,那-1即0xffffffff,讓我們通過實例來確定是否存在這個情況。
考慮如下JS代碼: var a = 1; var b = -1; a--; b++;
掛上調試器,棧中看到這種解釋不出來名字的,跟在Js::JavascriptFunction::CallFunction
的,就是編譯出來的字節碼:
該地址所在的內存頁的Allocation Base基本就是函數開頭,觀察該函數。
首先可以看到大量的NOP插入,如箭頭處,感覺沒有什么大作用的語句會讓想從這里拿到ROP的片段的攻擊者大為頭疼。
繼續往下,可以看到在Chakra(11.0.9600.17689)中,這兩個數字被解釋為:
1被存儲為了3 ,也即 1 * 2 + 1 (左移一位加一)。 -1 被存儲為了0xffffffff, -1 * 2 + 1 = -1 (同上)。
而由于編譯執行,所以編譯器自然地做了優化,將a++ 的值2按 * 2 + 1編碼后5 和 b— 的值 -2 ,編碼后-2 * 2 + 1 = -3 (0xfffffffd)提前計算好寫入了字節碼中,如上圖,這個和流行的編譯器的優化邏輯如出一轍,只不過并沒有像是編譯器那樣那么徹底,有點像Release開啟了O1 O2優化之后的感覺。但是明顯效率高于Debug版的。
讓我們看一下同樣代碼在VS2008 Release開啟O2和Debug 默認以及Chakra的結果:
代碼在Debug Vs2008生成如下操作:
可以看到就是逐句翻譯,真·機翻的感覺。
Release 開啟O2之后的效果,這里已經把加加減減的值算好了,這個和Chakra的動作一樣。
代碼比較簡單,所以開啟O1也是一個效果,所以用腳趾頭也可以得出結論,Chakra的字節碼編譯過程中確實有優化。
不過不同于VS,如果你直接寫個變量加加減減最后哪兒也不用他,開啟OX優化的時候,這個變量根本就不會編譯進EXE,而Chakra則稍有不同,即使沒用,還是編譯進來了,只不過做了最大可能的優化處理。
如果全優化了,F12你怎么調試呢。
這個區間內的數字比較特殊,采用了異或的方式存儲。異或的隨機密鑰在生成字節碼時由編譯器確定。
考慮如下JS代碼:
var a = 0x32132132; var b = -0x12312312;
a++; b--;
調試可知,這里的數字被“加密”了:
以
062a003f bad8359075 mov edx,offset USP10!pScriptProperties+0xd0 (759035d8)
062a0044 81f2bd77b611 xor edx,11B677BDh
為例 (a = 0x32132132;)
0:008> g
Breakpoint 0 hit
eax=9ef4b531 ebx=03f2bad0 ecx=03419720 edx=db9db9db esi=03f2b8bc edi=062a0000
eip=062a003f esp=03f2b8a0 ebp=03f2b8a8 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
062a003f bad8359075 mov edx,offset USP10!pScriptProperties+0xd0 (759035d8)
0:008> p
eax=9ef4b531 ebx=03f2bad0 ecx=03419720 edx=759035d8 esi=03f2b8bc edi=062a0000
eip=062a0044 esp=03f2b8a0 ebp=03f2b8a8 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
062a0044 81f2bd77b611 xor edx,11B677BDh
0:008>
eax=9ef4b531 ebx=03f2bad0 ecx=03419720 edx=64264265 esi=03f2b8bc edi=062a0000
eip=062a004a esp=03f2b8a0 ebp=03f2b8a8 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
062a004a 899108060000 mov dword ptr [ecx+608h],edx ds:002b:03419d28=67422664
將edx右移一位,得到0x32132132即a的值。
負數的處理也是一樣,以第二句為例:
062a0056 ba170c6687 mov edx,87660C17h
062a005b 81f2cab5fb5c xor edx,5CFBB5CAh
0:008>
eax=9ef4b531 ebx=03f2bad0 ecx=03419720 edx=64264265 esi=03f2b8bc edi=062a0000
eip=062a0056 esp=03f2b8a0 ebp=03f2b8a8 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
062a0056 ba170c6687 mov edx,87660C17h
0:008>
eax=9ef4b531 ebx=03f2bad0 ecx=03419720 edx=87660c17 esi=03f2b8bc edi=062a0000
eip=062a005b esp=03f2b8a0 ebp=03f2b8a8 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
062a005b 81f2cab5fb5c xor edx,5CFBB5CAh
0:008>
eax=9ef4b531 ebx=03f2bad0 ecx=03419720 edx=db9db9dd esi=03f2b8bc edi=062a0000
eip=062a0061 esp=03f2b8a0 ebp=03f2b8a8 iopl=0 nv up ei ng nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000286
062a0061 89910c060000 mov dword ptr [ecx+60Ch],edx ds:002b:03419d2c=dbb99ddb
得到edx后,將其右移一位得到0xEDCEDCEE
(記得符號位不要也一起右移了)。正是var b = -0x12312312
;。
下方a++運算結果和2byte的例子一樣,我們就簡單看一個
062a006d ba30059380 mov edx,80930530h
062a0072 81f25747b5e4 xor edx,0E4B54757h
異或后:64264267 右移一位:32132133 (32132132 + 1)
作為32位有符號數,大于4byte的就不好弄了,讓我們看看
var a=0x90909090;
var b=0x99999999;
a++;b++;
的結果:
針對它的加法,JS已經選擇用SSE2的函數了:
07030080 68b8b31803 push 318B3B8h
07030085 53 push ebx
07030086 e88539e46b call jscript9!Js::SSE2::JavascriptMath::Increment_Full (72e73a10)
0703008b 8bf8 mov edi,eax
0703008d e9b8ffffff jmp 0703004a
它們的值被提前算好存儲起來了:
063b0067 47 inc edi
063b0068 8b1504612c03 mov edx,dword ptr ds:[32C6104h]
063b006e 81fa804f2c03 cmp edx,32C4F80h
063b0074 0f8562000000 jne 063b00dc
063b007a 8b1508612c03 mov edx,dword ptr ds:[32C6108h]
063b0080 8bff mov edi,edi
063b0082 899a08060000 mov dword ptr [edx+608h],ebx
063b0088 8b1508612c03 mov edx,dword ptr ds:[32C6108h]
063b008e 89b20c060000 mov dword ptr [edx+60Ch],esi
063b0094 8b1508612c03 mov edx,dword ptr ds:[32C6108h]
063b009a 898208060000 mov dword ptr [edx+608h],eax
063b00a0 8b1508612c03 mov edx,dword ptr ds:[32C6108h]
063b00a6 898a0c060000 mov dword ptr [edx+60Ch],ecx
ebx、esi等其實是存儲著mmword 的指針,但這看起來并不直觀,所以我們需要有更直觀的方式:
var a=0x90909090;
var b=0x99999999;
b = a+b;
068f0026 8d6424f4 lea esp,[esp-0Ch]
068f002a 57 push edi
068f002b 56 push esi
068f002c 53 push ebx
068f002d bbf0805503 mov ebx,35580F0h
068f0032 be00815503 mov esi,3558100h
068f0037 33ff xor edi,edi
068f0039 f20f10056c490003 movsd xmm0,mmword ptr ds:[300496Ch]
068f0041 f20f100db4f96e00 movsd xmm1,mmword ptr ds:[6EF9B4h]
068f0049 f20f58c1 addsd xmm0,xmm1
068f004d 8b0538b8fb02 mov eax,dword ptr ds:[2FBB838h]
068f0053 8d4810 lea ecx,[eax+10h]
068f0056 3b0d34b8fb02 cmp ecx,dword ptr ds:[2FBB834h]
068f005c 0f875b000000 ja 068f00bd
這回就看的很清楚了
這正是我們的第一個數,0x90909090;
第二個數也不例外,
可見,使用xmm的時候,Chakra并沒有給它們做任何的混淆,事實上也沒有什么太大的必要混淆這類數字了,因此做法可以接受。
VIII.2 對象的操作與結構
作為一個面向對象的語言,JScript中寬松的定義使得對象操作簡單但是又“復雜” ,考慮如下代碼。
var a = new Object;
a.X = 3; a.Y = 4;
此時a是一個含有X、Y兩個Number成員的Object,它的編譯方式是怎樣呢?
請一定要記得上一節的內容,3和4在內存里面是7和9。
只有這樣,我們才能愉快地找到后兩句的位置,閱讀代碼可知這個對象的內存分布是:
+0: jscript9!Js::ArrayBufferParent::`vftable'
左邊是Object,右邊是成員的兩個Number,在Windbg中可以跟蹤得到:
0:007> dd [edi+8]
0b69a030 00000007 00000009 030eb040 030eb040
0b69a040 00000000 00000000 00000000 00000000
這樣當然是不夠的,讓我們再看看將這個對象(或者更形象的說“Property Bag”)復制后再擴展一個成員(Slot),代碼如下
#!c
function xObj(x,y)
{
this.X = x;
this.Y = y;
}
var a=new xObj(3,4);
var b=new xObj(3,4);
b.Z = 5;
首先,找到五個賦值語句的位置:
第一句的3,4
第二句的3,4
到此為止,這兩個對象都是一樣的,
直到這里,可以看出來它給b的第三個元素(應為Z)賦值5,這里a、b的情況我們在控制臺輸入一下。
a:
b:
可以看出來a、b除了成員變量之外別無二致,這正是Chakra中為了對象的快速存取而制作的多態特性:
VIII.3 函數的調用
最后,讓我們回到這個問題上,想在一章之內說完Chakra絕對無異于癡人說夢,所以這里挑一個常見的地方來描述:在Chakra中的函數調用是如何的?
首先是基本的函數,這個和JScript5差不多,一些具體功能的操作符(或者在JS中稱為Token),例如new Object這樣無參數的new操作符,對應的函數是Js::JavascriptOperators::NewJavascriptObjectNoArg
。
而有些簡單的內容被編譯器直接計算值后顯示,例如Math.sqrt(5)在這里直接存為了2.23606xxxxx
,而不會再現場調用函數來運算:
其他的一些無法優化的函數調用還是會照常調用,還是類似JScript5一樣看名字即可知道是做啥的,例如取隨機數的函數即為Js::SSE2::JavascriptMath::Random:
通過符號很容易看到各個函數的名稱和參數類型等:
而用戶自定義的函數、循環體等通常在字節碼中不會作為內聯函數存在,例如下列偽代碼,它們的調用有點像是:
在生成字節碼的時候它的范圍通常是:
顏色代表立場,呸,顏色代表不同的函數塊,上述函數在調用時最可能的棧像是:
Chakra的內容介紹到此結束,限于個人水平以及篇幅內容,先介紹的就是這么多,其實基本調試方法和調試之前的代碼沒啥兩樣,這里的調試很容易就能夠舉一反三。
自Chakra開始,IE的腳本執行速度比先前提升了十多倍(微軟自己的SunSpider跑分),原圖我找不著了,但是在微軟的某個文章里面還是看到了小圖……如下,IE9面世不久后的測試得分和當時的Chrome21的腳本跑分不相上下。可惜悲劇的它出來的時候大家在用IE6,IE11出來的時候大家還是在用IE6,就是卡的不行啊怎么辦,趕緊升級吧:)。
1 http://files.accuvant.com/web/files/images/webbrowserresearch_v1_0.pdf
2 http://users.ics.forth.gr/~elathan/papers/ndss15.pdf
3 https://blog.indutny.com/5.allocating-numbers