一,、前言 首先觀察如下語句: alert(1) 這是Edge瀏覽器上一條有效的JavaScript代碼,,如何實(shí)現(xiàn)這一點(diǎn)呢? 當(dāng)Twitter將推文的字符個(gè)數(shù)限制從140增加到280時(shí),,當(dāng)時(shí)我想試一下哪些unicode字符可以在這種限制條件下使用,,這應(yīng)該是非常有趣的一件事情。我發(fā)了一則推文,,中間包含一些有趣的字符,,可以導(dǎo)致Twitter出現(xiàn)渲染錯(cuò)誤,這種字符就是所謂的Zalgo字符,。以這件事情為契機(jī),,我開始思考如何自動(dòng)識(shí)別這些字符。我們并不能使用DOM來檢查某些字符的行為比較異常,,需要使用屏幕截圖來查看瀏覽器所看到的內(nèi)容。剛開始我使用的是JavaScript以及canvas來截圖,,但得到的圖片與瀏覽器中顯示的實(shí)際圖片并不匹配,,因此我需要使用另一種方法,而Headless Chrome正是我苦苦尋找的解決方案,。我使用的是puppeteer,,這是一個(gè)NodeJS模塊,我們可以借此控制Headless Chrome并截取屏幕,。 二,、生成字符 為了生成Zalgo文本,我們可以重復(fù)單個(gè)字符,,也可以組合兩個(gè)字符然后多次重復(fù)第二個(gè)字符,。比如,如下碼點(diǎn)(code point)可以在自我重復(fù)時(shí)產(chǎn)生不好的視覺體驗(yàn),,而實(shí)際上它們大多都是unicode組合字符: 834,1425,1427,1430,1434,1435,1442,1443,1444,1445,1446,1447,1450,1453,1557,1623,1626,3633,3636,3637,3638,3639,3640,3641,3642,3655,3656,3657,3658,3659,3660,3661,3662 比如,,如下JavaScript代碼可以使用上面的某個(gè)字符來生成非常難看的文本: ???????????????????? 輸出結(jié)果為: 這里比較有趣的是,多個(gè)字符可以組合在一起并產(chǎn)生不同的效果,。以311以及844字符為例,,使用相同技術(shù)將這兩個(gè)字符組合在一起,會(huì)得到不同的爬升效果: ????????????????????? 得到的效果為: ? 三,、構(gòu)造Fuzzer Fuzzer其實(shí)構(gòu)造起來非常簡(jiǎn)單,。我們需要一個(gè)能正確渲染字符的網(wǎng)頁,加入一些CSS使頁面足夠?qū)?,這樣合法字符可以移動(dòng)到屏幕右側(cè),,我們就可以檢查渲染頁面左側(cè)、頂部以及底部的區(qū)域,,將fuzz這個(gè)div元素移到頁面中央,。 舉個(gè)例子,,fuzzer中渲染的字符“a”以及字符“b”如下圖所示。為了幫助大家理解fuzzer的操作過程,,我把fuzzer檢查的區(qū)域標(biāo)注出來,,具體如下:
而字符?以及 ?的屏幕圖像如下所示(這兩個(gè)字符的碼點(diǎn)分別為311以及834)。在fuzzer看來這兩個(gè)字符會(huì)產(chǎn)生較為有趣的效果,,因?yàn)樯傻奈谋疚挥谏戏絽^(qū)域,。
上述JavaScript代碼會(huì)從查詢字符串中讀取1到2個(gè)字符編號(hào),然后使用innerHTML以及String.fromCharCode輸出這些字符,。當(dāng)然,,這些代碼會(huì)在客戶端執(zhí)行。 然后,,我在NodeJS中用到了png以及puppeteer庫,。 const PNGReader=require('png.js');const puppeteer=require('puppeteer'); 接下來構(gòu)造兩個(gè)函數(shù),檢查某個(gè)像素是否是白色像素,,是否位于我期待的區(qū)域中(即頂部,、左側(cè)以及底部)。 function isWhite(pixel) { if(pixel[0]===255 && pixel[1]===255 && pixel[2]===255) { return true; } else { return false; } }function isInRange(x,y) { if(y <=120) { return true; } if(y >=220) { return true; } if(x <=180) { return true; } return false; } fuzzBrowser函數(shù)是一個(gè)異步函數(shù),,可以截取屏幕,,使用png庫讀取png文件。該函數(shù)會(huì)將有趣的字符輸出到古董控制臺(tái)(console)以及chars.txt文本文件中,。 async function fuzzBrowser(writeStream, page, chr1, chr2) { if(typeof chr2 !=='undefined') { await page.goto('localhost/visualfuzzer/index.php?'+chr1+','+chr2); } else { await page.goto('localhost/visualfuzzer/index.php?'+chr1); } await page.screenshot({clip:{x:0,y:0,width: 400,height: 300}}).then((buf)=>{ var reader=new PNGReader(buf); reader.parse(function(err, png){ if(err) throw err; outerLoop:for(let x=0;x<400;x++) { for(let y=0;y<300;y++) { if(!isWhite(png.getPixel(x,y)) && isInRange(x,y)) { if(typeof chr2 !=='undefined') { writeStream.write(chr1+','+chr2+'n'); console.log('Interesting chars: '+chr1+','+chr2); } else { writeStream.write(chr1+'n'); console.log('Interesting char: '+chr1); } break outerLoop; } } } }); }); } 然后構(gòu)造一個(gè)異步匿名函數(shù),,循環(huán)處理目標(biāo)字符并調(diào)用fuzzBrowser函數(shù)。在測(cè)試多個(gè)字符組合場(chǎng)景時(shí),,我排除了會(huì)導(dǎo)致副作用的單個(gè)字符,。 (async ()=> { const browser=await puppeteer.launch(); const page=await browser.newPage(); const singleChars={834:1,1425:1,1427:1,1430:1,1434:1,1435:1,1442:1,1443:1,1444:1,1445:1,1446:1,1447:1,1450:1,1453:1,1557:1,1623:1,1626:1,3633:1,3636:1,3637:1,3638:1,3639:1,3640:1,3641:1,3642:1,3655:1,3656:1,3657:1,3658:1,3659:1,3660:1,3661:1,3662:1}; const fs=require('fs'); let writeStream=fs.createWriteStream('logs.txt', {flags: 'a'}); for(let i=768;i<=879;i++) { for(let j=768;j<=879;j++) { if(singleChars[i] || singleChars[j]) { continue; } process.stdout.write("Fuzzing chars "+i+","+j+" r"); await fuzzBrowser(writeStream, page, i, j).catch(err=>{ console.log("Failed fuzzing browser:"+err); }); } } await browser.close(); await writeStream.end(); })();四、ZalgoScript 前不久我發(fā)現(xiàn)了Edge上存在一個(gè)有趣的bug,,簡(jiǎn)單說來,,就是Edge會(huì)錯(cuò)誤地將某些字符當(dāng)成空白符,因?yàn)槟承﹗nicode字符組合在一起就會(huì)出現(xiàn)這種行為,。那么如果我們將這個(gè)bug與Zalgo結(jié)合在一起會(huì)出現(xiàn)什么情況,?這樣做我們就可以得到ZalgoScript!首先我生成了一份字符列表,,Edge會(huì)將該列表中的所有字符都當(dāng)成空白符(有很多這樣的字符,,大家可以訪問Github了解完整列表)。我決定fuzz 768-879之間的字符(fuzzer代碼默認(rèn)情況下已經(jīng)包含該范圍),,根據(jù)fuzzer的結(jié)果,,837字符與768-879之間的字符組合在一起會(huì)得到非常難看的視覺效果。這個(gè)思路很棒,我可以遍歷這個(gè)列表,,將字符結(jié)合在一起,,生成既是Zalgo文本又是有效的JavaScript的輸出結(jié)果。 a=[];for(i=768;i<=858;i++){ a.push(String.fromCharCode(837)+String.fromCharCode(i).repeat(5)); } a[10]+='alert('a[15]+='1'; a[20]+=')'; input.value=a.join('')eval(a.join('')); 這也是我們?nèi)绾紊杀疚拈_頭提到的 alert(1)語句的具體方法,。 我已經(jīng)將visualfuzzer的代碼公布在Github上,。 如果你喜歡這方面內(nèi)容,你可能也會(huì)對(duì)非字母數(shù)字形式的JavaScript代碼感興趣,。 |
|