xy2_

How an anti ad-blocker works: Reverse-engineering BlockAdBlock

March 26, 2020

If you've used an adblocker, you may have seen BlockAdBlock. This script detects your ad-blocker and disables website access until you deactivate your adblocker. But I found myself wondering how it worked. How does an anti ad-blocker detect adblockers? And how do adblockers react and block ad-block-blockers?

Reverse-engineering through time

The first thing I did was look at their site. BlockAdBlock offers a configurator that allows to specify how long to wait, and how the script even appeared, creating different versions of the script.

And this got me thinking about versions. What if I could not look at one version, but all of them? So I did. I went back in time, using the Wayback Machine. After I downloaded all versions, I took a look and hashed them:

The list of all BlockAdBlock versions, with sha1sum. Click to unroll!
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

There were six versions, and the last one is from 2016, although I still see sites using BlockAdBlock today. This is a huge win, because we can reverse the script once, then reverse each diff. We can see scrapped ideas and even leftover debug code.

You can find each version on GitHub. If you'd like to look at each diff, see this repository where each commit is a different version. I'll include links to the source for each section of this reversing, don't worry.

Unpacking

As we look at the code, we find that it is not minified, but instead packed by a JS packer by Dean Edwards.1

Dean Edwards’ packer in BlockAdBlock: only an argument's name changes.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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,{}))

Thankfully, we don't have to worry about this. The packer's weakness is that any code it unpacks must be passed to eval(). If we replace the eval() with something like console.log(), suddently we get the whole source code and the packer is defeated.2

Once we've done that for each version, we can examine each version, as well as the added features over the years.

Version 1 (? - November 2015): initial script

We start by taking a look at 20151007203811.js, around November 2015.3 Although this first version does very little adblock-blocking, it allows us to take a look at BlockAdBlock's architecture, without the cruft that accumulated over the years.

Architecture

In three sentences:

  • BlockAdBlock is a closure returning an object with three functions:
    • bab(), which sets up bait most of the time, calling check
    • check(), which checks if the adblocker blocked the bait, calling arm
    • arm(), which creates the overlay.
  • The entrypoint, bab(), is then fired after a set amount of time.
  • The three returned functions are generated with arguments from the closure, which are set in the BlockAdBlock customizer.
The code is built around a closure, assigned to a global object with a random name.
1
2
3
4
5
6
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 + ''] = ...
It returns a three-function object: bab, check and arm.
1
2
3
4
5
6
7
8
window['' + randomID + ''] = (function() {
    var eid = ...
    return {
        bab: function(check, passed_eid) {},
        check: function(checkPredicate, unused) {},
        arm: function() {}
    }
})();
The entrypoint, bab(), is called via setTimeout().
1
2
3
setTimeout('window[\'\' + randomID + \'\'] \
.bab(window[\'\' + randomID + \'\'].check, \
     window[\'\' + randomID + \'\'].bab_elementid)', setTimeoutDelay * 1000);

The closure has outer variables. Two of them serve to keep state in the script:

  • adblockDetected is 1 if an adblocker is detected.
  • nagMode is a customization option. If set, the script will only nag you once to disable your adblocker, rather than block access.
Other outer variables in the closure control apparence and behavior, set in the customizer.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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: bait ad creation

BlockAdBlock's central detection method is by creating “bait” ad elements, that look like real ads. It then checks if the adblocker blocked them.

A bait is created: a fake div pretending to be an ad, but hidden out of view.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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);
    ...
Afterwards, check if the bait was removed by the adblocker.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    ...
    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 will trigger if the predicate was true, and trigger arm.
1
2
3
4
5
6
check: function(checkPredicate, unused) {
    if ((checkPredicate) && (adblockDetected == 0)) {
        adblockDetected = 1;
        window['' + randomID + ''].arm()
    } else {}
}

Nag mode

BlockAdBlock has an feature called “nag mode”: in this mode, BlockAdBlock will only tell you to remove your adblocker once, instead of blocking you on each visit. It does so by setting a localStorage item after the first visit.

If we could set this for every, could we bypass BlockAdBlock forever? Unfortunately, BlockAdBlock checks beforehand if the script has been configured to nag mode, so this won't work for default usage, which is to block every time.

The start of arm, checking for nag mode.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)
        }
    };
    ...

Blocking BlockAdBlock, version 1

Adblockers work by using what's called filters: lines of code that can block network requests and hide elements on the page. By creating “bait” elements, BlockAdBlock triggers these filters on purpose.

With this simple defense, BlockAdBlock works against all major adblockers, like uBlock Origin, AdBlock Plus and Ghostery. To counter against this, we must write our own filter that's active only on BlockAdBlock-enhanced websites.

Writing adblock filters is a bit tricky. The kind of filter we need is a content filter, which blocks elements on the page generated after download. Since the bait ad has an id of banner_ad, we create an element hiding exception, marked #@#, for all elements # with id banner_ad, and put it in our adblocker's custom filter list.

Putting it all together, we get:

Defeating BlockAdBlock, version 1.
1
localhost#@# #banner_ad

This counters BlockAdBlock successfully. The solution may seem basic, but it got the job done for a long time in the Anti-AdBlock-Killer filter list.

Version 2 (November 2015 - January 2016): a few improvements

Bait ad creation: less bugs

There's a subtle bug in the first bait ad creation implementation above: the div that's created has no content, so it creates a 0 height by 0 width div. Later, the code checks if the div was removed if the height and width of the bait div was empty. But since the div had 0 height, BlockAdBlock would always trigger.4

Fixing the empty div bug.
1
2
3
4
bab: function(...) {
    bait = document.createElement('DIV');
    ...
    bait.appendChild(document.createTextNode('Â '));

Detection via fake image ads

In this method, we create a fake image with a random name on doubleclick.net. Adblockers will block the image, thinking it to be an ad's image. However, this requires no change to block in our filter.

Creating a fake image ad.
1
2
3
4
bab: function(...) {
    bait = document.createElement('DIV');
    bait.innerHTML = '<img src="http://doubleclick.net/' + randomStr() + '.jpg">';
    ...

The other notable difference is the use of a setInterval timer instead of just checking once if the trigger is set. It newly checks if the image ad still exists, and if its src attribute hasn't been modified, by checking the contents of the bait.

A new setInterval, and checking for the image's existence.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    ...
    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

Version 3 (November 2015 - March 2016): generalized baiting

Bait ad creation: randomized IDs

The only change in this version, though a significant one, is the appearance of randomized IDs for the bait ad. A new ID is taken from a list of ad IDs at page load, and is used for the bait ad, now placed in the middle of the page.

The list of random bait IDs.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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"
];
Random ID generation.
1
2
3
4
5
    randomBaitID = baitIDs[ Math.floor(Math.random() * baitIDs.length) ],
    ...
    var passed_eid = randomBaitID;
    bait = document.createElement('DIV');    
    bait.id = passed_eid;

Blocking BlockAdBlock, version 3 to latest

BlockAdBlock uses the blind spot of adblockers: if the filter allows all the above IDs, then it also allows genuine ads to pass through. If you don't allow blocking the above IDs, then other filters which may use these IDs in their filters to target ads will not work anymore.

In a way, BlockAdBlock is forcing the adblocker to make itself useless.

In adblockers, we can run arbitrary JS before anything in the page runs. We could try to delete the BlockAdBlock object prematurely. But to do that, we need the name of the object BlockAdBlock is attached to, which is randomized each run, which would require running the code.

uBlock Origin took another approach. The code is ran by eval, so what if we could define our own eval function that would block execution if we detect BlockAdBlock? In JS the Proxy object can accomplish this: any property, affectation and method can be replaced for any object.

This could be bypassed by not evaling the initial BlockAdBlock payload and using it directly, so we also proxy the entrypoint: the setTimeout call. Since setTimeout is passed a string and not a function, we check the string.

Defeating BlockAdBlock in uBlock Origin (src).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 
};
Proxying the eval and setTimeout functions.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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);
        }
    }
});

As we are now using a scriptlet, a custom piece of code ran by the adblocker, the filter changes slightly:

Defeating BlockAdBlock, all versions: the filter.
1
localhost## +js(nobab)

Version 4 (January 2016 - April 2016): experimental features

The above detection method was made in January 2016, according to uBlock Origin commit history, and has not changed in concept since its inception. BlockAdBlock never tried to work around this filter after its creation, by changing its code architecture. Instead, it continued development with more features. And when we go over to the BlockAdBlock page, we see an interesting tab: “Need more anti-adblock power?".

Advanced detection

Although those defenses are only available in a special tab, they are included in all scripts and executed by fittingly-named variables. In version 4, two are implemented:

  • aDefOne, the “specific defense for AdSense sites”.
  • aDefTwo, the “special element defense”.

Accidental debug comments

There's something I should mention before we go. When reversing this version, one function caught my eye:

A debug console.log() that's used in the code!
1
2
3
4
5
6
function consolelog(e) {
    // "Dev mode" check: developpers of BAB must set window.consolelog to 1.
    if (window.consolelog == 1) {
        console.log(e)
    }
};

These debug comments are only available in this version of the code. If I hadn't been reversing each version, I never would have caught it. These comments provide valuable information on how the code works.

Advanced defense: AdSense

All of these special defenses are put in check and not in arm, like the architecture would suggest. This would suggest a change of developer that was perhaps unfamiliar with the codebase.

If AdSense is active on the page, we check that the ads which are supposed to be there still exist. If they're gone because of the adblocker, then BlockAdBlock activates.

A clever defense: check if existing ads, created by AdSense, are gone.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
    }
    ...

The scriptExists implementation looks for a given script within the page. In this case, it will detect the Adsense script if it exists.5

Compare the passed script URL against all scripts currently on the page.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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
};

Advanced defense: Special element defense

This method, unlike the first one, has a disclaimer: “Please test after installation to ensure compatibility with your site." To contextualize where we are in the code, let's look at check:

This special defense only triggers if adblock wasn't detected and there is no AdSense script on the page.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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
                }
            }
        }
    }

So why the disclaimer? This method tries to include the AdSense script. If it doens't load, it's likely the adblocker blocked the network request, so BlockAdBlock triggers. But this may mess up some web sites, hence the warning.

If we fail to load AdSense, trigger the overlay.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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
    }
}

And indeed, most adblockers fall for it and block the request. However, there's one adblocker that I haven't mentionned until this point. Let's talk about the Brave browser.

Brave Browser's answer to BlockAdBlock

Until now I've examined uBlock Origin's response against BlockAdBlock. And it works, but it needs a specific filter to be added for each site that has BlockAdBlock on it. Brave is impressive because it detects and circumvents BlockAdBlock on all versions, without any action needed. To do so, it spoofs the request directly at the network level.6

Instead of blocking the ad_status.js request, it lets it through but loads a 0-byte fake Google Ads instead. This clever trick fools BlockAdBlock, because onerror fires only if the network request fails.

Chromium with adblocker & Brave vs BlockAdBlock: network requests

Version 5 (March 2016 - November 2016)

Advanced defense: Favicon spam

The only change of note in this version is that the second advanced defense was rewritten, but still holds the same basic principle: try network requests that will be blocked by the adblocker. This time, however, it tries to load favicons instead of AdSense.

Brave evades this detection in the same was as above. It loads the images correctly, but creates fake 1x1 images.

Favicon spam. baitImages generates bait images, too.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
    };
}

Version 6 (April 2016 - November 2016): blocking Brave

So far BlockAdBlock's techniques, although simplistic at first, have grown in terms of complexity and detection rate. But there's still one enemy left unconquered: the Brave browser.

Advanced defense: Fake favicon detection

Why did BlockAdBlock switch from trying to load a script to an image (a favicon)? The answer is this code, which is put inside the “favicon spam” defense and activates if the Brave defense is active.

Detecting Brave Browser: check the response for a fake image.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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()
            }
        }
    };
}

With this method, Brave is defeated, and other adblockers will be detected if they run the code (most, like uBlock Origin, outright block it in the first place.)

After this update, around the end of November 2016, BlockAdBlock disappeared from the web. Although their “advanced defense” techniques work, they were never enabled for the majority of users. This was their last update, and their last post on both Twitter and their site was sometimes in late 2017.

However, the legacy BlockAdBlock left is significant. And even though it can be trivially blocked nowadays, I still see BlockAdBlock used in today's sites.

Conclusion

In the end, who will win the arms race between ad-blockers and ad-blockers-blockers? Only time can tell, but I think ad-blockers still have the advantage. As the arms race evolves, ad-blockers will have to use more and more contrived techniques and completely custom code, as watching BlockAdBlock's evolution over time hopefully shows.

On the other hand, blockers have the advantage of stable systems and powerful filtering tools via filter lists, and have access to JavaScript, too: with these systems, it only takes one person to figure out how to defeat the adblocker and update the filter list with new sites.

By analysing BlockAdBlock's evolution over time, as well as various ad blocker's responses, we managed to draw a picture of the small war between BlockAdBlock and ad blockers, and in the process learnt how ad-blockers-blockers block the ad-blockers.

You can find my reverse-engineering on GitHub. Thank you for reading.


  1. If you want an intuition of how it works, try pasting the below example into a JS console and then look at the code. If you're interested in its inner workings, here's the source code. ↩︎

  2. Don't believe me? Try changing eval to console.log in the first line of the above example and you might find something hidden… ↩︎

  3. The timestamp says 201510, so shouldn't it be October? The reason for this is that we don't know when the script changed. All we know is:

    • On 2015-10, there was one version saved: 20151007203811.js.
    • On 2015-11, there was a new version: 20151113115955.js.

    As far as we know, the script could have been changed the day before the second timestamp. As such, I err on the cautious side when timing the versions. ↩︎

  4. The tests for v1, above, were made while fixing this bug in the v1 script. ↩︎

  5. Thanks to McStroyer on Reddit for pointing this out. ↩︎

  6. Brave's adblock component is open source, so we can peek at the source to get an intuition on how it works.

    Thanks to Francois Marier for pointing out that Brave is open-source. ↩︎