我不小心解開了AdBlock 的會員

我不小心解開了AdBlock 的會員

更新日期:2021年10月18日

先説明:禁止轉載!Write by Mane,個人觀點,僅供參考學習使用

另外我不是計算機系的,寫的肯定是有錯的地方,歡迎留個言糾正下。

起源

我個人是習慣使用 AdBlock — best ad blocker 作爲阻擋廣告插件,原因是個人覺得UI做的比 uBlocker - Ad Block Tool for Chrome 好看,由於谷歌的政策,能發佈在谷歌市場上就意味著代碼不可以被混淆,所以,於是我就開始手癢了。(其實,這玩意一年要給5美元的智商稅我就很。。)。

所以,這篇文章也就記錄下我做了什麽,和具體的實現原理,出事了我不負責

破解

pojie

分析

正如上面所説,谷歌市場上的插件不可以被混淆,這也就意味著大家都可以去看這裏面裝著什麽,如果源碼被混淆了,可以通過底層更暴力的方法(比如:直接暴力調用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_actiondefault_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_scriptsdefault_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.sendMessagechrome.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