[Перевод] Реверс-инжиниринг антиблокировщика рекламы BlockAdBlock

Если вы пользуетесь блокировщиками рекламы, то могли встречать BlockAdBlock. Этот скрипт обнаруживает ваш блокировщик и не пускает на сайт, пока вы его не отключите. Но мне стало интересно, как он работает. Как антиблокировщик обнаруживает блокировщики? А как на это реагируют блокировщики и как они блокируют антиблокировщики?
Первым делом я взглянул на их сайт. BlockAdBlock предлагает конфигуратор с настройками: интервал ожидания и как будет выглядеть предупреждение, генерируя разные версии скрипта.

Это натолкнуло меня на мысль о версиях. А что, если мог посмотреть не на одну версию, а на все сразу? Так я и сделал. Я вернулся назад во времени с помощью Wayback Machine. После этого скачал все версии BlockAdBlock и хэшировал их.

Список всех версий BlockAdBlock, с sha1sum

6d5eafab2ca816ccd049ad8f796358c0a7a43cf3  20151007203811.js
065b4aa813b219abbce76ad20a3216b3481b11bb  20151113115955.js
d5dec97a775b2e563f3e4359e4f8f1c3645ba0e5  20160121132336.js
8add06cbb79bc25114bd7a2083067ceea9fbb354  20160318193101.js
8add06cbb79bc25114bd7a2083067ceea9fbb354  20160319042810.js
8add06cbb79bc25114bd7a2083067ceea9fbb354  20160331051645.js
8add06cbb79bc25114bd7a2083067ceea9fbb354  20160406061855.js
8add06cbb79bc25114bd7a2083067ceea9fbb354  20160408025028.js
555637904dc9e4bfc6f08bdcae92f0ba0f443ebf  20160415083215.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20161120215354.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20170525201720.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20170606090847.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20170703211338.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20170707211652.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20170813090718.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20170915094808.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20171005180631.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20171019162109.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20171109101135.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20171127113945.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20171211042454.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20171227031408.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20180202000800.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20180412213253.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20180419060636.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20180530223228.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20180815042610.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20181029233809.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20181122190948.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20181122205748.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20190324081812.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20190420155244.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20190424200651.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20190903121933.js
d8986247cad3bbc2dd92c3a2a06ac1540da6b286  20200112084838.js


Только шесть версий отличаются друг от друга, а последняя из них относится к 2016 году, хотя некоторые сайты и сейчас используют BlockAdBlock. Это отлично, потому что нам достаточно будет провести реверс-инжиниринг единственной версии, а потом отреверсить диффы. Спойлер: мы увидим развитие идей и даже остатки отладочного кода.

Все версии я выложил на GitHub. Если хотите посмотреть на диффы, то в этом репозитории каждый коммит представляет собой новую версию.


Код скрипта не минифицирован, а упакован JS-упаковщиком от Дина Эдвардса с косметическими изменениями и без какой-либо модификации логики1.

eval(function(p, a, c, k, e, d) {
    e = function(c) {
        return (c < a ? '' : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
    };
    if (!''.replace(/^/, String)) {
        while (c--) {
            d[e(c)] = k[c] || e(c)
        }
        k = [function(e) {
            return d[e]
        }];
        e = function() {
            return '\\w+'
        };
        c = 1
    };
    while (c--) {
        if (k[c]) {
            p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c])
        }
    }
    return p
}('0.1("2 3 4 5 6 7 8\'d. 9, h? a b c d e f g");i j=\'a\'+\'k\'+\'e\'+\'l\'+\'n\'+\'m\'+\'e\';',24,24,
'console|log|This|code|will|get|unpacked|then|eval|Cool||||||||huh|let|you|w|s||o'.split('|'),0,{}))


К счастью, для нас это не проблема. Слабость упаковщика заключается в том, что весь код при распаковке передаётся в eval(). Если мы заменим eval() на что-то вроде console.log(), то внезапно получаем весь исходный код, и упаковщик побеждён2.

Проделав это для каждой версии, мы можем изучить каждую из них и посмотреть, какие функции туда добавлялись со временем.


Исходный код

Начнём с изучения 20151007203811.js, опубликованного около ноября 2015 года3. Хотя эта первая версия не очень хорошо блокирует блокировщики, но позволяет оценить архитектуру BlockAdBlock без мусора, который накопился с годами.

Архитектура


В трёх предложениях:

  • BlockAdBlock — это замыкание, возвращающее объект с тремя функциями:
    • bab() выставляет приманку и вызывает проверку check
    • check() проверяет, заблокировал ли блокировщик приманку, вызывая arm
    • arm() накладывает оверлей
  • Точка входа bab() затем запускается через заданное количество времени.
  • Генерируются три функции с аргументами, которые задаются в конфигураторе BlockAdBlock.


Код строится вокруг замыкания, назначенного глобальному объекту со случайным именем.

var randomID = '',
    e = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (var i = 0; i < 12; i++) randomID += 
    e.charAt(Math.floor(Math.random() * e.length));
var setTimeoutDelay = 7; // Delay after which to call BlockAdBlock
window['' + randomID + ''] = ...


Автор предусмотрел тщательную рандомизацию, чтобы обойти статическую блокировку.

Затем возвращается объект с тремя функциями: bab, check и arm.

window['' + randomID + ''] = (function() {
    var eid = ...
    return {
        bab: function(check, passed_eid) {},
        check: function(checkPredicate, unused) {},
        arm: function() {}
    }
})();


Это мои собственные названия. Все переменные минифицированы, а некоторые специально обфусцированы.
Точка входа bab() вызывается через setTimeout().

setTimeout('window[\'\' + randomID + \'\'] \
.bab(window[\'\' + randomID + \'\'].check, \
     window[\'\' + randomID + \'\'].bab_elementid)', setTimeoutDelay * 1000)


bab_elementid не используется ни в одной версии кода. setTimeout передаётся в виде строки.

У замыкания есть внешние переменные. Две из них служат для сохранения состояния в скрипте:

  • adblockDetected равен 1, если обнаружен блокировщик рекламы.
  • nagMode — это вариант настройки. Если он установлен, то скрипт не блокирует доступ к странице, а только поворчит на вас один раз.


Другие переменные для управления внешним видом и поведением устанавливаются в конфигураторе.

var eid = ' ad_box', // Name of the bait.
    __u1 = 1, // Unused.

    // Colors for the blockadblock prompt.
    overlayColor = '#EEEEEE',
    textColor = '#777777',
    buttonBackgroundColor = '#adb8ff',
    buttonColor = '#FFFFFF',

    __u2 = '', // Unused.

    // Text to display when the blockadblock prompt is shown.
    welcomeText = 'Sorry for the interruption...',
    primaryText = 'It looks like you\'re using an ad blocker. That\'s okay.  Who doesn\'t?',
    subtextText = 'But without advertising-income, we can\'t keep making this site awesome.',
    buttonText = 'I understand, I have disabled my ad blocker.  Let me in!',

    // If 1, adblock was detected.
    adblockDetected = 0,
    // If 1, BlockAdBlock will only nag the visitor once, rather than block access.
    nagMode = 0,

    // The blockadblock domain, reversed.
    bab_domain = 'moc.kcolbdakcolb';


bab_domain устанавливается здесь в попытке обфусцировать домен BlockAdBlock.

bab: создание баннера-приманки


Основной метод работы BlockAdBlock заключается в создании «приманки» или «наживки» из рекламных элементов, которые выглядят как настоящие баннеры. Затем он проверяет, заблокировал ли их блокировщик.

Создаётся приманка: фальшивый div, который притворяется рекламой, но скрыт из виду.

bab: function(check, passed_eid) {
    // Wait for the document to be ready.
    if (typeof document.body == 'undefined') {
        return
    };

    var delay = '0.1', 
        passed_eid = eid ? eid : 'banner_ad',
        bait = document.createElement('DIV');
        
    bait.id = passed_eid;
    bait.style.position = 'absolute';
    bait.style.left = '-999px';
    bait.appendChild(document.createTextNode(' '));
    document.body.appendChild(bait);
    ...


Видимо, passed_eid предназначен для настройки идентификатора приманки, но он не используется.

После этого проверяется, была ли наживка удалена рекламным блоком.

    ...
    setTimeout(function() {
        if (bait) {
            check((bait.clientHeight == 0), delay);
            check((bait.clientWidth == 0), delay);
            check((bait.display == 'hidden'), delay);
            check((bait.visibility == 'none'), delay);
            check((bait.opacity == 0), delay);
            check((bait.left < 1000), delay);
            check((bait.top < 1000), delay)
        } else {
            check(true, delay)
        }
    }, 125)
}


Если приманка больше не существует, то элемент удалён (и мы запускаем оверлей).

Функция check сработает, если Predicate возвращает значение true, и запускает arm.

check: function(checkPredicate, unused) {
    if ((checkPredicate) && (adblockDetected == 0)) {
        adblockDetected = 1;
        window['' + randomID + ''].arm()
    } else {}
}


Поскольку проверка check срабатывает несколько раз, как показано выше, adblockDetected настроен на первую правильную проверку, чтобы избежать многократного срабатывания arm.

Режим ворчания


В скрипте есть функция под названием «режим ворчания» (nag mode): в этом режиме BlockAdBlock только один раз скажет отключить блокировщик рекламы, но не будет блокировать вас при каждом посещении. Это делается путём установки элемента localStorage при первом посещении.

Если бы мы могли сами установить этот элемент, то могли бы мы отключить блокиратор навсегда? К сожалению, BlockAdBlock заранее проверяет, был ли скрипт настроен на режим nag, поэтому такой способ не сработает, когда он работает в режиме по умолчанию, то есть с блокировкой доступа.

arm: function() {
    if (nagMode == 1) {
        var babNag = sessionStorage.getItem('babn');
        if (babNag > 0) {
            return true // Stop the script.
        } else {
            sessionStorage.setItem('babn', (Math.random() + 1) * 1000)
        }
    };
    ...


К сожалению, nagMode устанавливается в конфигураторе, а по умолчанию равен 0.

Блокировка BlockAdBlock, версия 1


Блокировщики рекламы используют так называемые фильтры: строки кода, которые могут блокировать сетевые запросы и скрывать элементы на странице. Создавая элементы «наживки», BlockAdBlock специально запускает эти фильтры.

С помощью такой простой защиты BlockAdBlock эффективен против всех основных блокировщиков рекламы, таких как uBlock Origin, AdBlock Plus и Ghostery. Чтобы противостоять этому, мы должны написать собственный фильтр, который активизируется только на сайтах, на которых работает BlockAdBlock.

Писать фильтры для блокировщика немного сложновато. Нам нужен фильтр контента, который блокирует элементы на странице, сгенерированной после загрузки. Поскольку у наживки идентификатор banner_ad, мы создаём исключение для скрытия элементов с пометкой #@# для всех элементов # с идентификатором banner_ad и помещаем его в список пользовательских фильтров нашего блокировщика.

В итоге получается следующее:

localhost#@# #banner_ad


У меня тут localhost для демонстрации, вы можете заменить его своим URL.

Это успешно дезактивирует BlockAdBlock. Решение может показаться простым, но оно уже давно успешно работает в списке фильтров Anti-AdBlock-Killer.


Исходный код
Разница v1/v2

Создание приманки: меньше багов


В первой версии создания приманки есть тонкая ошибка: у созданного div нет контента, поэтому генерируется div с высотой 0 и шириной 0. Позже код проверяет, удалён ли div, проверяя его высоту и ширину. Но поскольку у него была нулевая высота, то BlockAdBlock срабатывал всегда 4.

Исправлена ошибка пустого div.

bab: function(...) {
    bait = document.createElement('DIV');
    ...
    bait.appendChild(document.createTextNode('Â '));


Создаётся дочерний div с некоторым содержимым.

Обнаружение блокировщиков через поддельные графические баннеры


В этом методе мы создаём поддельное изображение со случайным именем на doubleclick.net. Блокировщики рекламы будут блокировать изображение, думая, что это рекламный баннер. Но это не требует никаких изменений в нашем фильтре.

bab: function(...) {
    bait = document.createElement('DIV');
    bait.innerHTML = '';
    ...


randomStr() генерирует строку произвольной длины.

Другим заметным отличием стало использование таймера setInterval вместо простой однократной проверки, установлен ли триггер. Он заново проверяет, отображается ли графический баннер и не изменён ли его атрибут src, проверяя содержимое приманки.

Новый setInterval и проверка на наличие изображения:

    ...
    checkCallback = setInterval(function() {
        if (bait) {
            check((bait.clientHeight == 0), delay);
            check((bait.clientWidth == 0), delay);
            check((bait.display == 'hidden'), delay);
            check((bait.visibility == 'none'), delay);
            check((bait.opacity == 0), delay);
            try {
                check((document.getElementById('banner_ad').innerHTML.indexOf('click') == -1), delay)
            } catch (e) {}
        } else {
            check(true, delay)
        }
    }, 1000


Почему indexof ('click')? Потому что источник изображения src="doubleclick.net/abcdefg.jpg" и мы проверяем, сохранился ли фрагмент строки click.
Исходный код
Разница v2/v3

Создание приманки: рандомизированные идентификаторы


Единственное изменение в этой версии, хотя и значительное, — появление рандомизированных идентификаторов для приманки. Новый идентификатор берётся из списка идентификаторов при загрузке страницы и используется для приманка, которая теперь помещается в середине страницы.

Длинный список случайных идентификаторов демонстрирует хорошее знание предметной области.

var baitIDs = [
  "ad-left",
  "adBannerWrap",
  "ad-frame",
  "ad-header",
  "ad-img",
  "ad-inner",
  "ad-label",
  "ad-lb",
  "ad-footer",
  "ad-container",
  "ad-container-1",
  "ad-container-2",
  "Ad300x145",
  "Ad300x250",
  "Ad728x90",
  "AdArea",
  "AdFrame1",
  "AdFrame2",
  "AdFrame3",
  "AdFrame4",
  "AdLayer1",
  "AdLayer2",
  "Ads_google_01",
  "Ads_google_02",
  "Ads_google_03",
  "Ads_google_04",
  "DivAd",
  "DivAd1",
  "DivAd2",
  "DivAd3",
  "DivAdA",
  "DivAdB",
  "DivAdC",
  "AdImage",
  "AdDiv",
  "AdBox160",
  "AdContainer",
  "glinkswrapper",
  "adTeaser",
  "banner_ad",
  "adBanner",
  "adbanner",
  "adAd",
  "bannerad",
  " ad_box",
  " ad_channel",
  " adserver",
  " bannerid",
  "adslot",
  "popupad",
  "adsense",
  "google_ad",
  "outbrain-paid",
  "sponsored_link"
];


Случайная генерация идентификаторов

    randomBaitID = baitIDs[ Math.floor(Math.random() * baitIDs.length) ],
    ...
    var passed_eid = randomBaitID;
    bait = document.createElement('DIV');    
    bait.id = passed_eid;


Поскольку это cрабатывает на каждой загрузке, идентификатор будет каждый раз отличаться.

Блокировка BlockAdBlock, версии с третьей до последней


BlockAdBlock эксплуатирует слепое пятно блокировщиков рекламы: если фильтр пропустит все вышеперечисленные идентификаторы, то он также пропустит и настоящую рекламу. Таким образом, BlockAdBlock заставляет блокировщик рекламы сделать себя бесполезным.

В блокировщиках мы можем выполнить произвольные скрипты перед загрузкой страницы. Можно попытаться заранее удалить объект BlockAdBlock. Но для этого нам нужно имя объекта BlockAdBlock, которое рандомизируется при каждом запуске.

uBlock Origin применил другой подход. Поскольку код выполняется функций eval, мы можем определить собственную функцию eval, которая будет блокировать выполнение, если мы обнаружим BlockAdBlock. В JS на такое способен объект Proxy: можно заменить любое свойство и метод в любом объекте.

Это можно обойти, не пропуская начальную полезную нагрузку BlockAdBlock через eval, а используя её напрямую, поэтому мы также проксируем точку входа: вызов setTimeout. Поскольку setTimeout передаётся строкой, а не функцией, мы проверяем эту строку.

Реализация в uBlock Origin (исходный код):

const signatures = [
    [ 'blockadblock' ],
    [ 'babasbm' ],
    [ /getItem\('babn'\)/ ],
    [
        'getElementById',
        'String.fromCharCode',
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
        'charAt',
        'DOMContentLoaded',
        'AdBlock',
        'addEventListener',
        'doScroll',
        'fromCharCode',
        '<<2|r>>4',
        'sessionStorage',
        'clientWidth',
        'localStorage',
        'Math',
        'random'
    ],
];
const check = function(s) {
    // check for signature 
};


Список подписей: определение шаблонов кода. Функция check проверяет строку после eval на соответствие этим шаблонам.

Далее проксируем функции eval и setTimeout.

window.eval = new Proxy(window.eval, {
    apply: function(target, thisArg, args) {
        const a = args[0];
        if ( typeof a !== 'string' || !check(a) ) {
            return target.apply(thisArg, args);
        } 
        // BAB detected: clean up.
        if ( document.body ) {
            document.body.style.removeProperty('visibility');
        }
        let el = document.getElementById('babasbmsgx');
        if ( el ) {
            el.parentNode.removeChild(el);
        }
    }
});
window.setTimeout = new Proxy(window.setTimeout, {
    apply: function(target, thisArg, args) {
        const a = args[0];
        // Check that the passed string is not the BAB entrypoint.
        if (
            typeof a !== 'string' ||
            /\.bab_elementid.$/.test(a) === false
        ) {
            return target.apply(thisArg, args);
        }
    }
});


Поскольку мы теперь используем скриптлет, специальный пользовательский фрагмент кода, выполняемый блокировщиком, фильтр немного меняется:

localhost## +js(nobab)


Он не запускается по умолчанию на всех сайтах из соображений производительности, поэтому его нужно назначить для каждого сайта с помощью скрипта.
Исходный код
Разница v3/v4

Описанный метод блокировки антиблокировщика разработан в январе 2016 года, согласно истории коммитов uBlock Origin, и концептуально не изменился с момента своего создания. BlockAdBlock никогда не пытался обойти этот фильтр, изменив свою архитектуру. Вместо этого он продолжил разработку новых функций. И когда мы переходим на страницу BlockAdBlock, то видим интересную вкладку: «Вам нужна большая мощь антиблокировки?»

rs0meawjau7ljt0-m6viiwoo4cq.png

Хотя эти защитные методы доступны только через специальную вкладку, они включены во все скрипты и выполняются через переменные с соответствующими названиями. В четвёртой версии реализовано два метода:

  • aDefOne, «специфическая защита для сайтов AdSense»
  • aDefTwo, «особый элемент защиты»


Случайные комментарии отладки


Прежде чем попрощаться с вами, должен упомянуть ещё кое-что. В процессе реверс-инжиниринга моё внимание привлекла одна функция:

Отладочный console.log() прямо в коде!

function consolelog(e) {
    // "Dev mode" check: developpers of BAB must set window.consolelog to 1.
    if (window.consolelog == 1) {
        console.log(e)
    }
};


Это выполняется только в том случае, если установлен глобальный consolelog, например, window.consolelog = 1.

Комментарии отладки доступны только в этой версии. Если бы я не отреверсил все версии, то никогда бы их не заметил. Они дают ценную информацию о том, как работает код.

Продвинутая защита: AdSense


Все эти специальные методы защиты закодированы в check, а не в arm, как предполагает архитектура. Возможно, над продуктом начал работать новый разработчик, не знакомый с кодовой базой.

Если на странице активен AdSense, то мы проверяем наличие объявлений. Если они исчезли из-за блокировщика, то активируется BlockAdBlock.

function check() {
    ...
    var q = 'ins.adsbygoogle',
        // Selects all Google ads in the document.
        adsbygoogleQuery = document.querySelector(q);

    if ((adsbygoogleQuery) && (adblockDetected == 0)) {
        // Ads are not blocked, since the bait ad is still there,
        // and adblockDetected hasn't been set
        if (aDefOne == 'yes') {
            consolelog('case2: standard bait says ads are NOT blocked.');
            var adsbygoogle = '//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js';
            if (scriptExists(adsbygoogle)) {
                consolelog('case2: And Adsense pre-exists.');
                if (adsbygoogleQuery.innerHTML.replace(/\s/g, '').length == 0) {
                    // The ad's content was cleared, so...
                    consolelog('case2: Ads are blocked.');
                    window['' + randomID + ''].arm()
                }
            }
        };
        adblockDetected = 1
    }
    ...


scriptExists проверяет всю страницу на наличие скрипта с заданным URL. В данном случае, скрипта AdSense5.

URL-адрес скрипта сравнивается со всеми скриптами на странице. По какой-то причине URL усекается до 15 символов.

function scriptExists(href) {
    if (href) href = href.substr(href.length - 15);
    var scripts = document.getElementsByTagName('script');
    for (var i = scripts.length; i--;) {
        var src = String(scripts[i].src);
        if (src) src = src.substr(src.length - 15);
        if (src === href) return true
    };
    return false
};


Продвинутая защита: специальный элемент


В отличие от первого, этот метод сопровождается оговоркой: «Пожалуйста, протестируйте после установки, чтобы убедиться в совместимости с вашим сайтом».

Эта специальная защита срабатывает только в том случае, если не обнаружен блокировщик и на странице нет скрипта AdSense. Вот соответствующий фрагмент кода check:

check: function(checkPredicate, unused) {
    if ((checkPredicate) && (adblockDetected == 0)) {
        // Adblocker detected, arm
    } else {
        var q = 'ins.adsbygoogle',
            adsbygoogleQuery = document.querySelector(q);

        if ((adsbygoogleQuery) && (adblockDetected == 0)) {
            if (aDefOne == 'yes') {
                // Special defense one: AdSense defense (see above)
            };
        } else {
            if (adblockDetected == 0) {
                if (aDefTwo == 'yes') {
                    // Special defense two: Special element defense
                }
            }
        }
    }


Метод предполагает, что владельцы сайтов используют только AdSense: если скрипта AdSense не существует, значит, что-то не так.

Зачем было предупреждение? Этот метод пытается включить скрипт AdSense. Если он не загружается, то, скорее всего, блокировщик заблокировал сетевой запрос, поэтому срабатывает BlockAdBlock. Но это может испортить некоторые сайты, отсюда и предупреждение.

Если AdSense не загрузился, то запускается оверлей.

if (aDefTwo == 'yes') {
    /* Add Google ad code to head.
        If it errors, the adblocker must have blocked the connection. */
    var googleAdCode = '//static.doubleclick.net/instream/ad_status.js';
    consolelog('case3: standard bait says ads are NOT blocked. Maybe ???\
      No Adsense is found. Attempting to add Google ad code to head...');
    var script = document.createElement('script');
    script.setAttribute('type', 'text/javascript');
    script.setAttribute('src', googleAdCode);
    script.onerror = function() {
        window['' + randomID + ''].arm()
    };
    adblockDetected = 1;
    if (!scriptExists(googleAdCode)) {
        document.getElementsByTagName('head')[0].appendChild(script)
    };
    adsbygoogleQuery = 0;
    window['' + randomID + ''].check = function() {
        return
    }
}


При сбое на сетевом уровне срабатывает onerror, как при работе блокировщика рекламы.

И действительно, большинство блокировщиков рекламы поддаются на это и блокируют запрос. Но есть один блокировщик, который я ещё не упоминал. Поговорим о браузере Brave.

Ответ браузера Brave


До сих пор мы изучали, как детектируется антиблокировщик в uBlock Origin. И это работает, только требуется определённый фильтр для каждого сайта, где установлен BlockAdBlock. Браузер Brave впечатляет тем, что обнаруживает и обходит BlockAdBlock всех версий без каких-либо необходимых действий со стороны пользователя. Для этого он подделывает запрос непосредственно на сетевом уровне6.

Вместо блокировки запроса ad_status.js он пропускает его, но загружает поддельную рекламу Google Ads размером 0 байт. Этот хитрый трюк дурачит BlockAdBlock, потому что onerror срабатывает только в том случае, если сетевой запрос терпит неудачу.

2e45s7wv2nv9licuzwesiao2tkm.png


Исходный код
Разница v4/v5

Продвинутая защита: спам фавиконами


Единственное изменение в этой версии заключается в том, что вторую расширенную защиту переписали, хотя она по-прежнему придерживается того же основного принципа: проверка сетевых запросов, которые будут заблокированы блокировщиком. Но теперь вместо AdSense загружаются фавиконы.

Brave уклоняется от этой атаки прежним способом. Он не блокирует запросы, но создаёт поддельные изображения 1×1.

if (aDefTwo == 'yes') {
    if (! window['' + randomID + ''].ranAlready) {/
        var favicons = [
            "//www.google.com/adsense/start/images/favicon.ico",
            "//www.gstatic.com/adx/doubleclick.ico",
            "//advertising.yahoo.com/favicon.ico",
            "//ads.twitter.com/favicon.ico",
            "//www.doubleclickbygoogle.com/favicon.ico"
            ],
            len = favicons.length,
            img = favicons[Math.floor(Math.random() * len)],
        ...
        baitImages(Math.floor(Math.random() * 2) + 1); // creates bait images
        var m = new Image();
        m.onerror = function() {
            baitImages(Math.floor(Math.random() * 2) + 1);
            c.src = imgCopy;
            baitImages(Math.floor(Math.random() * 2) + 1)
        };
        c.onerror = function() {
            adblockDetected = 1;
            baitImages(Math.floor(Math.random() * 3) + 1);
            window['' + randomID + ''].arm()
        };
        m.src = img;
        baitImages(Math.floor(Math.random() * 3) + 1);
        window['' + randomID + ''].ranAlready = true
    };
}


Функция baitImages может вызываться часто, со случайным количеством изображений, чтобы обойти статические блокировщики.
Исходный код
Разница v5/v6

Методы BlockAdBlock, хотя и простые на первый взгляд, постепенно становились сложнее и эффективнее. Но остался последний непобеждённый враг: браузер Brave.

Продвинутая защита: определение поддельного фавикона


Почему BlockAdBlock перешёл с попытки загрузить скрипт на загрузку изображений (фавиконов)? Ответ — в коде, который добавлен в защиту через спам фавиконами, и который активируется против защиты Brave.

Проверка ответа на наличие поддельного изображения:

if (aDefTwo == 'yes') {
    baitImages(Math.floor(Math.random() * 3) + 1);
    // earlier favicon code...
    var m = new Image();
    if ((aDefThree % 3) == 0) {
        m.onload = function() {
            if ((m.width < 8) && (m.width > 0)) {
                window['' + randomID + ''].arm()
            }
        }
    };
}


Если размер фавикона меньше 8×8, то это, вероятно, подделка от браузера Brave.

С помощью этого приёма BlockAdBlock обходит маскировку Brave и других блокировщиков, которые запускают этот код (большинство, как uBlock Origin, блокируют его в первую очередь).

После этого обновления, примерно в конце ноября 2016 года, BlockAdBlock исчез из интернета. Хотя их «продвинутые методы защиты» работают, они никогда не были активированы для большинства пользователей. Это было последнее обновление. Последний пост в твиттере и на сайте опубликован где-то в конце 2017 года.

Однако наследие BlockAdBlock живёт. Хотя в наши дни его тривиально заблокировать, этот скрипт по-прежнему используют некоторые современные сайты.


Кто победит в гонке вооружений между блокировщиками рекламы и антиблокировщиками, которые блокируют блокировщиков? Только время покажет. По мере развития гонки вооружений антиблокировщикам придётся использовать всё более изощрённые методы и специальный код, как показывает эволюция BlockAdBlock.

С другой стороны, у блокировщиков преимущество стабильных систем и мощных инструментов фильтрации через списки фильтров, а также доступ к JavaScript. С такой системой достаточно, чтобы один человек понял, как победить врага — и обновить список фильтров с новыми сайтами.

Анализируя эволюцию BlockAdBlock, а также различные ответы блокировщиков рекламы, нам удалось нарисовать картину небольшой войны между BlockAdBlock и блокировщиками. В процессе мы узнали, какие методы используются в боевых действиях.

Мой код реверс-инжиниринга опубликован на GitHub. Спасибо вам за чтение.


1. Если хотите получить представление, как это работает, попробуйте вставить приведённый ниже пример в консоль JS, а затем посмотрите на код. Если вас интересует его внутренняя работа, вот исходный код. ︎[вернуться]

2. Не верите? Попробуйте изменить eval на console.log в первой строчке.
[вернуться]

3. Метка времени говорит 201510, так что может быть октябрь. Но мы не знаем, когда изменился скрипт. Всё, что мы знаем:

  • 2015–10 была сохранена одна версия: 20151007203811.js
  • 2015–11 появилась новая версия: 20151113115955.js


Насколько нам известно, скрипт могли изменить за день до второй метки времени. Таким образом, я консервативно подхожу к датировке. ︎[вернуться]

4. Во время исправления этой ошибки были разработаны тесты для v1. ︎[вернуться]

5. Спасибо McStroyer на Reddit, который обратил на это внимание. ︎[вернуться]

6. Компонент блокировщика рекламы Brave открыт, поэтому мы можем посмотреть исходный код, чтобы получить представление о том, как он работает.


Спасибо Франсуа Марье за указание на исходный код Brave. ︎[вернуться]

© Habrahabr.ru