# 資訊工程研討心得 409410021 陳緯杰 409410016 王彥珽 ## 主題 以[iOS Privacy: Announcing InAppBrowser.com - see what JavaScript commands get injected through an in-app browser](https://krausefx.com/blog/announcing-inappbrowsercom-see-what-javascript-commands-get-executed-in-an-in-app-browser)為主題並以其中的技術寫一份研讀心得。 觀察作者所使用的javascript code,從中學習。 ## Initialization 辨識裝置及瀏覽器、新增Object儲存有潛在危險的指令、function宣告 ### Third party identification ```! 下面的程式碼是用於判斷使用的網站或App是否為第三方瀏覽器 目前有接受的瀏覽器為: Brave, Chrome, DuckDuckGo, Firefox, Edge and the Google search app 而因為在Android系統上判斷方式與iOS有不一樣的部分,因此有進行額外的判斷處理 ``` ```javascript=78 // Check the user agent string if the current browser is a third party browser // Currently detecting Brave, Chrome, DuckDuckGo, Firefox, Edge and the Google search app let isThirdPartyBrowser = navigator.userAgent.includes("CriOS") || navigator.userAgent.includes("EdgiOS") || navigator.userAgent.includes("FxiOS") || navigator.userAgent.includes("GSA") || navigator.userAgent.includes("DuckDuckGo") || navigator.brave; // Additionally check for Android third party user agents if (navigator.userAgent.includes("Android") && (navigator.userAgent.includes("Chrome") || navigator.userAgent.includes("Firefox") || navigator.userAgent.includes("EdgA"))) { isThirdPartyBrowser = true } ``` - - - ```! 此處是接承上方判斷是否為第三方瀏覽器的結果,再去設定將會顯示在網頁中的HTML id是否要顯現出來 並且宣告之後為了展示使用的陣列elementsToShowOnNonClean,並且先將其內部的element都先設定為不顯現 ``` ```javascript=88 if (isThirdPartyBrowser) { document.getElementById("third-party-browser").style.display = "block" dangerousSummary.style.display = "none" mediumSummary.style.display = "none" cleanSummary.style.display = "none" } else { document.getElementById("third-party-browser").style.display = "none" } const elementsToShowOnNonClean = [ resultsList, document.getElementById("top-summary-1"), ] for (const element of elementsToShowOnNonClean) { element.style.display = "none" } ``` ### Function ```! 這個清單包含了許多Javascript的指令,但是對於資料洩漏的方面比較不會有危險性,因此之後對於指令判別時,可以先參照有沒有此清單的函數,可以跳過一連串不需的判斷及修改。 ``` ```javascript=107 const denyList = [ "globalThis", "Object", "log", "parseInt", "Array", "Promise", "Reflect", "location", "onmouseout", "onmouseover", "onpointerout", "onmousemove", "onmouseleave", "onpointerrawupdate", "onpointerover", "onpointerenter", "onpointermove", "onmouseenter", "onpointerleave", "onscroll", "onpointerdown", "ongotpointercapture", "onpointercancel", "onlostpointercapture", "defaultView", "toString" ] ``` --- ```! potentiallyDangerous則是儲存許多極有可能有潛在風險的指令,例如createElement則會插入額外的JS code、addEventListener可能會對於鍵盤輸入做監聽,這些都很有可能存在資料安全漏洞或是將使用者資訊被洩漏。 ``` ```javascript=136 const potentiallyDangerous = { "createElement('script'": "Injects external JavaScript code", "http://": "Uses an unencrypted URL, making the whole page insecure", "addEventListener('selectionchange'": "Monitors all text selections on websites", "addEventListener('click'": "Monitors all taps happening on websites, including taps on all buttons & links", "addEventListener('keypress'": "Monitors all keyboard inputs on websites", "addEventListener('keyup'": "Monitors all keyboard inputs on websites", "addEventListener('keydown'": "Monitors all keyboard inputs on websites", "createElement('a'": "Creates a new link on website", "elementFromPoint": "Gets information about an element based on coordinates, which can be used to track which elements the user clicks on", "querySelector('input[type=password]": "Accesses all password fields: This could either mean the app has a password manager, or the app reads or enters passwords on the page", "addEventListener('message'": "Monitors all JavaScript messages, this can be used for many things, including communication with iframes", "addEventListener('focusin'": "Monitors when the user focuses on an element, like a textfield", } ``` --- ```! 因為在程式中有些會使用到如createElement或querySelectorAll進行中立或正當的使用,需要與potentiallyDangerous區分開來,因此建立neutralNotes,goodNotes進行儲存與偵測。 ``` ```javascript=150 const neutralNotes = { "createElement('style')": "Adds CSS code, allows app to customize appearance of website", "addEventListener('focus'": "Monitors whenever you focus a text field or other element", "addEventListener('blur'": "Monitors whenever you remove the focus from an element", "querySelectorAll('img'": "Accesses all images on the page", } const goodNotes = { "querySelector('head > meta[property=\"og:site_name\"]')": "Gets the website's name", "querySelector('head > meta[name=\"title\"]')": "Gets the website title", "querySelectorAll('head > link[rel~=\"icon\"]')": "Gets the website's favicon(s)", "querySelector('head > title')": "Gets the website title", "getElementsByTagName('meta')": "Gets the page's metadata (like share text), this is unrelated to Meta the company", } ``` --- ```! function appendEvent將在denyList外的其他函數進行字串分析比對,看其是危險、中立或是安全;順便進行輸出排版 (173)遍歷potentiallyDangerous進行危險的code比對 (187)遍歷neutralNotes進行中立code比對 (194)遍歷goodNotes進行安全的code比對 ``` ```javascript=167 function appendEvent(str, level) { let classesToUse = "" let comments = "" let allCommentToPush = null; // Iterate over potentiallyDangerous, and see if any of them are included in the `str` for (const [key, value] of Object.entries(potentiallyDangerous)) { if (str.indexOf(key) > -1) { classesToUse += " danger " comments += `<p class='bad-comment'>^&nbsp;&nbsp;${value}</p>` allCommentToPush = `<li class='bad-in-comments'>${value}</li>` cleanSummary.style.display = "none" mediumSummary.style.display = "none" if (!isThirdPartyBrowser) { dangerousSummary.style.display = "block" } } } // Iterate over neutralNotes, and see if any of them are included in the `str` for (const [key, value] of Object.entries(neutralNotes)) { if (str.indexOf(key) > -1) { comments += `<p class='neutral-comment'>^&nbsp;&nbsp;${value}</p>` allCommentToPush = `<li class='neutral-in-comments'>${value}</li>` } } // Iterate over goodNotes, and see if any of them are included in the `str` for (const [key, value] of Object.entries(goodNotes)) { if (str.indexOf(key) > -1) { comments += `<p class='good-comment'>^&nbsp;&nbsp;${value}</p>` allCommentToPush = `<li class='good-in-comments'>${value}</li>` } } if (allCommentToPush) { if (!existingEntries.includes(allCommentToPush)) { allComments.innerHTML += allCommentToPush } existingEntries.push(allCommentToPush) } if (level < lastLevel) { classesToUse += " reducedLevel " } lastLevel = level; resultsList.innerHTML += "<li class='" + classesToUse + "'>" + str + comments + "</li>" for (const element of elementsToShowOnNonClean) { element.style.display = "block" } if (cleanSummary.style.display !== "none") { for (const element of elementsToHideOnNonClean) { element.style.display = "none" } if (!isThirdPartyBrowser) { mediumSummary.style.display = "block" } } } ``` --- ```! renderParam會將字串以"'"重新包裝。 ``` ```javascript=228 // Function to help surround the value by ' if it's a string function renderParam(param) { if (typeof param === "string") { return "'" + param.replace("'", "\\'") + "'" } else { return param } } ``` --- ```! 判斷node[prop](=document.prop())是否為function, True-->留存一個變數original,接者讓node[prop]進入appendEvent的str去判斷內部的安全性。 false-->使用原本留存的original function獲得一個物件fakeObj,若此物件存在,使用investigate也去判斷內部有無危險的指令,使用get,set做two-way binding並用Proxy做代理。 ``` ```javascript=237 function overrideFunction(node, prop, level) { if (typeof node[prop] !== "function") { return; } try { const original = node[prop] node[prop] = function() { let str = prop; if (arguments) { str = `${node.constructor.name}.${prop}(` str += Object.values(arguments).map((arg) => renderParam(arg)).join(", ") str += ")" } // `str` would be something like // "HTMLDocument.querySelector('head > meta['property="og:site_name"]')" appendEvent(str, level) const fakeObj = original.apply(node, arguments) if (fakeObj) { investigate(fakeObj, level + 1); const handler1 = { get(target, prop, receiver) { // For some reason, prop is always a string it seems const spaces = "&nbsp;".repeat(level * 4) if (Math.abs(parseInt(prop)) > 0 || prop == "0") { appendEvent(spaces + fakeObj + "[" + renderParam(prop) + "]", level + 1) } else { appendEvent(spaces + fakeObj + "." + prop + "", level + 1) } return target[prop] }, set(target, prop, value, receiver) { const spaces = "&nbsp;".repeat(level * 4) if (Math.abs(parseInt(prop)) > 0 || prop == "0") { appendEvent(spaces + fakeObj + "[" + renderParam(prop) + "] = '" + value + "'", level + 1) } else { appendEvent(spaces + fakeObj + "." + prop + " = " + renderParam(value), level + 1) } return target[prop] = value } } return new Proxy(fakeObj, handler1); } return fakeObj; } console.log("Sucessfully overwrote " + prop + " on " + node) } catch (e) { console.error(e); } } ``` --- ```! 先判斷node當中的prop是否在denyList當中,如果是的話就直接判斷下一個prop,不用進行overrideFunction ``` ```javascript=289 function investigate(node, level) { for (var prop in node) { if (denyList.includes(prop)) { continue; } value = node[prop] overrideFunction(node, prop, level) } } ``` ## Execute ```! 執行起始位置,先針對document object做上面的investigate函數,對整個瀏覽器做監聽,而瀏覽器的監聽還未進行安全性的確認,因此對於window object上得到的內容也做AppendEvent。 最後再移除addEventListener添加的函數。 ``` ```javascript=298 investigate(document, 1); const originalAddEventListener = window.addEventListener; const originalRemoveEventListener = window.removeEventListener; window.addEventListener = function(...args) { const str = `window.addEventListener('${args[0]}', ${args.slice(1).map((arg) => renderParam(arg)).join(", ")}` appendEvent(str, 1) return originalAddEventListener.apply(window, args) } window.removeEventListener = function() { appendEvent("window.removeEventListener('" + arguments[0] + "')", 1) return originalRemoveEventListener.apply(window, arguments) } ``` ## 心得 王彥珽: 看完這次的程式碼,對於javascript又有多一點的了解,平常除了解picoctf的題目以外,就不太會有機會去看javascript的程式碼,透過這次的機會,詳細的學習了javascript的各種語法,不過還是有不太理解的地方,像是proxy代理、get、set(two-way binding)、如何流利的使用Object,都是這次學到比較新,不太理解的地方,以後還得要多多加強。 --- 陳緯杰: 雖然上次去資安營有稍稍到某些javasript的code,但是在這次比較深入的去理解他人實際寫的網站或著是功能時,有時還是容易處在很難理解的狀態,讓我們知道自己對於Javascript還有很長一段路要學習。但這份code讓我們可以自行找查學到一些不曾接觸的使用方式或語法,也使我們知道現實生活中其他工程師可能是如何把自己的構想實做出來的,因此整體我們受益良多。 程式碼來源[Github](https://github.com/KrauseFx/inAppBrowser.com)