我不小心解開了AdBlock 的會員
我不小心解開了AdBlock 的會員
更新日期:2021年10月18日
先説明:禁止轉載!Write by Mane,個人觀點,僅供參考學習使用
另外:我不是計算機系的,寫的肯定是有錯的地方,歡迎留個言糾正下。
起源
我個人是習慣使用 AdBlock — best ad blocker 作爲阻擋廣告插件,原因是個人覺得UI做的比 uBlocker - Ad Block Tool for Chrome 好看,由於谷歌的政策,能發佈在谷歌市場上就意味著代碼不可以被混淆,所以,於是我就開始手癢了。(其實,這玩意一年要給5美元的智商稅我就很。。)。
所以,這篇文章也就記錄下我做了什麽,和具體的實現原理,出事了我不負責。
破解
分析
正如上面所説,谷歌市場上的插件不可以被混淆,這也就意味著大家都可以去看這裏面裝著什麽,如果源碼被混淆了,可以通過底層更暴力的方法(比如:直接暴力調用chromium底層)來暴搞,不過谷歌不允許你混淆源碼,一方面谷歌很難審查這個插件幹了些什麽,第二方面怕有安全隱患(安全隱患是真的,寫過插件的人都知道這個權限有多高),所以廢話不多説直接開搞。
0x0 插件默認路徑
從Chrome Web Store下載完插件也就是*.crx文件時,會啓動新的綫程void SandboxedUnpacker::Start()
來驗證包的簽名,準備插件路徑:
// chrome\browser\extensions\sandboxed_unpacker.cc
if (!CreateTempDirectory())
return; // ReportFailure() already called.
// Initialize the path that will eventually contain the unpacked extension.
extension_root_ = temp_dir_.path().AppendASCII(
extension_filenames::kTempExtensionName);
隨後執行Unpacker::Run()
進行解包,調用file_util::CreateDirectory(temp_install_dir_)
在本地的目錄下創建目錄,然後把解壓好的文件扔進去之後交給其他綫程Extension::LoadExtent(const char* key,URLPatternSet* extent,const char* list_error,const char* value_error,string16* error)
來load插件。
也就是說,插件的路徑在解包之前就已經決定好了,從上面的代碼段可以看出,路徑都是以插件的ID extension_filenames::kTempExtensionName
為目錄名。
所以,在windows的路徑下,插件的路徑一般是固定的,一般是:
%LOCALAPPDATA%\Google\Chrome\User Data\Default\Extensions\<插件ID>
關於插件的ID,用瀏覽器打開chrome://extensions/
,然後打開開發者模式就可以看到ID了。
這裏給個簡單的例子:假設AdBlock的ID是gighmmpiobklfepjocnamgkkbiglidom
那麽路徑則是%LOCALAPPDATA%\Google\Chrome\User Data\Default\Extensions\gighmmpiobklfepjocnamgkkbiglidom
0x1 瀏覽器load插件的過程
現在有了目錄,那麽就可以load插件了。
在chromium在load插件默認會讀取插件文件夾裏面manifest.json
的文件作爲入口點,這是chromium的核心源碼中强行規定的:
// chrome\common\extensions\extension.cc
const FilePath::CharType Extension::kManifestFilename[] =
FILE_PATH_LITERAL("manifest.json");
在chromium的插件模型中,概況的講有三大類,第一種是background
,如果存在于manifest.json
則在插件load的時候會先執行用作後臺服務:
// chrome\common\extensions\extension.cc
bool Extension::LoadSharedFeatures(
const APIPermissionSet& api_permissions,
string16* error) {
if (
// <Mane省略此處,原因是太長了>
// LoadBackgroundScripts() must be called before LoadBackgroundPage().
!LoadBackgroundScripts(error) ||
!LoadBackgroundPage(api_permissions, error) ||
// <Mane再一次的省略此處,原因是太長了>
return false;
return true;
}
第二種是content_scripts
,通常我會叫它為Injection script,因爲他就用script來inject在某個match的page。
在谷歌裏,每一個tab裏面,也就是容器裏面的内容叫 content。 不過,content_scripts 是有條件的,需要判斷是哪個URL,定義於:
// chrome\common\extensions\user_script.cc
bool Extension::LoadContentScripts(string16* error) {
// <Mane再一次的省略此處,原因是太長了>
UserScript script;
if (!LoadUserScriptHelper(content_script, i, error, &script))
return false; // Failed to parse script context definition.
script.set_extension_id(id());
if (converted_from_user_script_) {
script.set_emulate_greasemonkey(true);
script.set_match_all_frames(true); // Greasemonkey matches all frames.
}
content_scripts_.push_back(script);
}
return true;
}
第三種就是你們經常會看到的UI界面,定義與browser_action
的default_popup
,定義于:
// chrome\common\extensions\extension_action.cc
void ExtensionAction::SetPopupUrl(int tab_id, const GURL& url) {
// We store |url| even if it is empty, rather than removing a URL from the
// map. If an extension has a default popup, and removes it for a tab via
// the API, we must remember that there is no popup for that specific tab.
// If we removed the tab's URL, GetPopupURL would incorrectly return the
// default URL.
SetValue(&popup_url_, tab_id, url);
}
0x2 manifest.json
{
// <Mane省略此處,原因是沒必要>
"background": {
"persistent": true,
"scripts": [ "polyfill.js", "ext/common.js", "ext/background.js", "abp-background.js", "pubnub.min.js" ]
},
"browser_action": {
// <Mane省略此處,原因是沒必要>
"default_popup": "adblock-button-popup.html",
"default_title": "__MSG_name__"
},
// <Mane省略此處,原因是沒必要>
"content_scripts": [ {
"all_frames": true,
"js": [ "polyfill.js", "ext/common.js", "ext/content.js", "include.preload.js", "adblock-functions.js" ],
"match_about_blank": true,
"matches": [ "http://*/*", "https://*/*" ],
"run_at": "document_start"
}, {
"all_frames": true,
"js": [ "adblock-uiscripts-rightclick_hook.js", "adblock-onpage-icon-cs.js" ],
"match_about_blank": true,
"matches": [ "http://*/*", "https://*/*" ],
"run_at": "document_start"
}, {
"all_frames": true,
"js": [ "subscriptionLink.postload.js" ],
"matches": [ "https://*.abpchina.org/*", "https://*.abpindo.blogspot.com/*", "https://*.abpvn.com/*", "https://*.adblock.ee/*", "https://*.adblock.gardar.net/*", "https://*.adblockplus.me/*", "https://*.adblockplus.org/*", "https://*.commentcamarche.net/*", "https://*.droit-finances.commentcamarche.com/*", "https://*.easylist.to/*", "https://*.eyeo.com/*", "https://*.fanboy.co.nz/*", "https://*.filterlists.com/*", "https://*.forums.lanik.us/*", "https://*.gitee.com/*", "https://*.gitee.io/*", "https://*.github.com/*", "https://*.github.io/*", "https://*.gitlab.com/*", "https://*.gitlab.io/*", "https://*.gurud.ee/*", "https://*.hugolescargot.com/*", "https://*.i-dont-care-about-cookies.eu/*", "https://*.journaldesfemmes.fr/*", "https://*.journaldunet.com/*", "https://*.linternaute.com/*", "https://*.spam404.com/*", "https://*.stanev.org/*", "https://*.void.gr/*", "https://*.xfiles.noads.it/*", "https://*.zoso.ro/*" ],
"run_at": "document_start"
}, {
"all_frames": true,
"js": [ "adblock-bandaids.js" ],
"matches": [ "*://*.getadblock.com/*", "*://*.getadblockpremium.com/*", "*://mail.live.com/*" ],
"run_at": "document_start"
}, {
"all_frames": true,
"js": [ "adblock-twitch-contentscript.js" ],
"matches": [ "*://*.twitch.tv/*" ],
"run_at": "document_start"
}, {
"all_frames": true,
"js": [ "adblock-yt-cs.js" ],
"matches": [ "*://*.youtube.com/*" ],
"run_at": "document_start"
} ],
// <Mane省略此處,原因是沒必要>
}
0x3 思路:重點看background源碼
上面提到,在load插件的時候,如果有定義background的話就會首先執行background的内容,一般來説插件在啓動的狀態下都會跑background,儅瀏覽器的進程關掉了的時候,background才會終結。考慮到設計該插件的可能性,驗證服務不太可能出現在content_scripts
和 default_popup
的js裏面,因爲這樣子做太傻逼了,所以現在唯一的方法就是看background的源碼。
通過觀察猜測與實驗發現,在abp-background.js
裏出現有出現了License
類:
const License = (function getLicense() {
// <Mane省略此處,原因是沒必要>
// activate the current license and configure the extension in licensed mode.
// Call with an optional delay parameter (in milliseconds) if the first license
// update should be delayed by a custom delay (default is 0 minutes).
activate(delayMs) {
let delay = delayMs;
const currentLicense = License.get() || {};
currentLicense.status = 'active';
License.set(currentLicense);
reloadOptionsPageTabs();
if (typeof delay !== 'number') {
delay = 0; // 0 minutes
}
if (!this.licenseTimer) {
this.licenseTimer = window.setTimeout(() => {
License.updatePeriodically();
}, delay);
}
setSetting('picreplacement', false);
loadAdBlockSnippets();
},
// <Mane省略此處,原因是沒必要>
isActiveLicense() {
return License && License.get() && License.get().status === 'active';
},
// <Mane省略此處,原因是沒必要>
}())
在閲讀源代碼的時候發現activate(delayMs)
和isActiveLicense()
,這不是激活函數,和判斷有沒有激活的函數嗎?
通過在插件的background進行debug,輸入了License.activate()
,結果不小心激活成功,刪除插件重來。
0x4 問題:那什麽時候才會被調用這個函數?
在開始之前,隨便寫下chromium的消息傳遞機制。
由於安全問題,在chromium内跨容器傳遞消息必須要使用消息傳遞的機制,不相同的容器不可以直接訪問函數,這個機制類似於TCP原理,消息機制定義於chrome\browser\extensions\api\messaging\*
:
This class manages message and event passing between renderer processes.
It maintains a list of processes that are listening to events and a set of open channels.Messaging works this way:
- An extension-owned script context (like a background page or a content script) adds an event listener to the "onConnect" event.
- Another context calls "extension.connect()" to open a channel to the extension process, or an extension context calls "tabs.connect(tabId)" to open a channel to the content scripts for the given tab. The EMS notifies the target process/tab, which then calls the on Connect event in every context owned by the connecting extension in that process/tab.
- Once the channel is established, either side can call postMessage to send a message to the opposite side of the channel, which may have multiple listeners.chrome\browser\extensions\api\messaging\message_service.h
儅建立起通道,傳遞消息的代碼如下:
// chrome\browser\extensions\api\messaging\message_service.cc
void MessageService::PostMessage(
int source_port_id, const std::string& message) {
int channel_id = GET_CHANNEL_ID(source_port_id);
MessageChannelMap::iterator iter = channels_.find(channel_id);
if (iter == channels_.end()) {
// If this channel is pending, queue up the PostMessage to run once
// the channel opens.
PendingChannelMap::iterator pending = pending_channels_.find(channel_id);
if (pending != pending_channels_.end()) {
lazy_background_task_queue_->AddPendingTask(
pending->second.first, pending->second.second,
base::Bind(&MessageService::PendingPostMessage,
weak_factory_.GetWeakPtr(), source_port_id, message));
}
return;
}
// Figure out which port the ID corresponds to.
int dest_port_id = GET_OPPOSITE_PORT_ID(source_port_id);
MessagePort* port = IS_OPENER_PORT_ID(dest_port_id) ?
iter->second->opener.get() : iter->second->receiver.get();
port->DispatchOnMessage(message, dest_port_id);
}
於是,chromium提供了chrome.runtime.sendMessage
和chrome.runtime.onMessage.addListener
這兩個API來傳遞消息,
在abp-background.js
裏,我發現綁定監聽的API,如下:
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.command === 'payment_success' && request.version === 1) {
License.activate(); // <-- Mane 叫你看這裏
sendResponse({ ack: true });
}
});
那麽,事件綁定好了,什麽時候才會觸發呢?
0x5 觸發事件
假設你是一個開發者,通常會做一個付款的網站來讓用戶付費,在插件的視角上得知你有沒有給了錢,有兩種選擇:
- 第一種:在用戶點擊購買的情況下隨機生成一個ID,然後定時向服務器驗證這個key是否是正確的。
- 第二種:通過上面的消息機制來傳遞,使插件執行
License.activate();
函數。
如果要搞第一種的話,只能暴力改代碼去破解,下面一章會講到。
但我還是抱著好奇的心試了下第二種,一般來説在回調購買成功的API時,應該首先向服務器查詢key,看用戶有沒有真正的付款。
普通網頁,也就是網頁自帶的js是無法跨容器去訪問插件的background
的,如果要實現購買成功的回調必須要讓插件使用content_scripts
去渲染網頁的js(請注意,網頁本身自帶的js是無法訪問插件的content_script,但content_script可以直接訪問網頁的js和獲取dom元素),也就是說回調的函數早已在插件定義好了,而不是通過網頁自帶的js去回調。
在我點擊購買的頁面裏面,我看到域名是 getadblock.com
,換句話説一定會有injection的js在插件裏面。
通過尋找manifest.json
,我看到了:
{
"all_frames": true,
"js": [ "adblock-bandaids.js" ],
"matches": [ "*://*.getadblock.com/*", "*://*.getadblockpremium.com/*", "*://mail.live.com/*" ],
"run_at": "document_start"
}
一打開adblock-bandaids.js
,果真是有寫回調函數:
function receiveMessage(event) {
if (
event.data
&& gabHostnamesWithProtocal.includes(event.origin)
&& event.data.command === 'payment_success'
) {
window.removeEventListener('message', receiveMessage);
browser.runtime.onMessage.addListener(onMessage);
browser.runtime.sendMessage({ command: 'payment_success', version: 1, origin: event.origin })
.then((response) => {
window.postMessage(response, '*');
});
}
}
所以這個時候只需要在這三個域名裏面"*://*.getadblock.com/*"
、"*://*.getadblockpremium.com/*"
、"*://mail.live.com/*"
的其中一個,按下F12去手動執行代碼:
var event = {origin:"mane is good! :P"}
console.log(event.origin)
browser.runtime.sendMessage({ command: 'payment_success', version: 1, origin: event.origin })
這樣就激活成功了。
那麽爲什麽按下F12要手動選插件呢?答案上面已經説過了。
不過,插件作者的這番神操作讓我懷疑他是不是小學畢業的。
0x6 暴力破解
改源碼就是啦,小學生都會啦,這麽無聊的操作還要我手動寫嗎?
0x7 直接條用chromium修改本地存儲
但我懶得寫。
總結
經過測試,在chrome和firefox上均測試成功。
Comments
Post a Comment