Pull to refresh

AdBlockBlock — обходим блокировщики рекламы. Метод 1

Reading time10 min
Views20K
Заниматься чем-то, нарушающим священное волеизъявление здешних господ на контент, который и только который они хотят видеть вокруг себя — дело, конечно, неблагодарное и кармически опасное. Но гонки вооружений между блокировщиками рекламы и рекламными системами не избежать, поэтому говорить об этом нужно. Сейчас, когда общий объем вырезанного трафика крутится около 1% — всё несколько вяленько, но уже есть оглядывающиеся владельцы сайтов, недополучающие до 30% денег с рекламы. Рекламные сети начинают общаться между собой, обмениваться спецификациями, есть уже какой-то израильский стартап на эту тему — думаю, соблазнительно с минимальными усилиями увеличить доход сразу и на проценты. В российском сегменте всё пока обходится увещевательными объявлениями вида «Вы отключили рекламу — это мешает нам развиваться» или простым игнорированием факта существования таких пользователей. Надо сказать, пусть оно всё так и остается.

Здесь, исключительно в режиме минимального доказательства работоспособности — будем обходить самый распространенный тип блокировщиков рекламы — по паттерну URL. Метод должен поддерживать:

  • хранения cookie рекламных систем на стороне пользователя
  • передавать не меньшее количество информации о пользователя, чем браузер: User-Agent, IP
  • требовать минимальной настройки большинства стандартных рекламных тэгов
  • быть легко подключаемым и изменяемым для случаев, когда кто-то не поленился и всё-таки добавил кастомное правило, попавшее в мейнстрим

Для достижения результата — будем маскировать все URL рекламных сетей через своеобразное прокси между сервером издателя и рекламодателя. Метод не самый дешевый c точки зрения ресурсов, потенциально опасен и для рекламодателей: источник фрода, не обойтись без оценкци качества трафика через клики или конверсии, и для издателей — если забанит гугл, то забанит весь сайт. Соответственно требует некоторого порога доверия между ними, но это всё отдельные вопросы. Да, все совпадения или упоминания случайны, брал то, что под руку попало.

Итак, мы хотим все вызовы сайта для тех у кого детектированы блокировщики рекламы, вида
ads.*.ru/228129/prepareCode?pp=g&ps=bugf&p2=ezfl&pct=a&plp=a&pli=a&pop=a'


Заменить на локальные, неотличимые от свойственного сайту контента, например

/meduza/2015/09/28/shapitorbFgFQ4Y7_Z3jaPSRix09Pn5VyrnV5Pcki64JcXvjIyzlgYXerh3yMMgY8DB5vleYAkS_gbRiHDSyQHU7QscAd38-1tKyYnnLjLSlpHq6aJ4sEo


Для наглядности начну с того, что будет нужно.

1. Взять рекламный тэг, зашифровать первую точку входа (та, которая в самом рекламном тэге) вручную

<!--Тип баннера: 990x90js-->
<!--Расположение: <верх страницы>-->
<script type="text/javascript">
    <!--
    (function(){
         //var link = 'http://ads.XXXXXX.ru/228129/prepareCode?pp=g&ps=bugf&p2=ezfl&pct=a&plp=a&pli=a&pop=a',
         var link = '/meduza/2015/09/28/shapitorbFgFQ4Y7_Z3jaPSRix09Pn5VyrnV5Pcki64JcXvjIyzlgYXerh3yMMgY8DB5vleYAkS_gbRiHDSyQHU7QscAd38-1tKyYnnLjLSlpHq6aJ4sEo',
         
     params = 'phone';
     new AXXXBanner(link, params).createBanner();
     })();
    //-->
</script>

2. Чуть расширить конфигурацию сервера (в данном случае nginx), начав обрабатывать такие урлы в особом режиме и отдавая обработку запроса на откуп скрипту:

    location /meduza/2015/09/28/shapito {
        set $prefix "http://localhost:9090/meduza/2015/09/28/shapito";
        rewrite shapito(.*)$ /adbb.php?query=$1&url_mask_prefix=$prefix&ua=$http_user_agent&remote_addr=$remote_addr;
    }

Всё — после этого реклама будет показана.

Как это работает.

Отступление, выбор средств
Хотел написать всю логику на Lua, но было лень пересобирать nginx и, так как PHP потенциально ближе к сайтовладельцам — использовал его. Код тривиальный писался для proof-of-concept, в две страницы, можно воспроизвести на любом языке — приложен в конце. Есть зависимость от mbcrypt и curl.

Алгоритм работы.

1. Расшифровать переданную строку ключом из конфигурации, получив целевой URL

2. Взять оттуда домен, зашифровать его

ads.XXXXXX.ru -> pPM9l7raWppVawqO


3. По этому ключу поищем в присланных нам cookie, найдем значение вида

niFJ2HLxzm27hCLnQUvcmLx62sEU-worI4tjmSAfqxNSMR6DSZ279lampNh_CN2jlu7FXaVk0WRVt-HMxy4vdm0uEncngawC6RvcKBwRXrT0wIi0icl4BvSXPJzH99C_5-mTmneEISfz


И расшифруем его, получив исходные куки, когда-то присланные нам от этого сервера для установки.

4. Если URL содержит маркеры клика, не делаем больше ничего — просто отдаем 302 редирект и выходим.

5. Иначе делаем запрос на целевой URL, передавая cookie, исходный пользовательский User-Agent и реальный IP в X-Forwarded-For заголовке.

6. Анализируем ответ от сервера:

5.1. Если были присланы Set-Cookie — перепаковываем их (ключ — зашифрованный домен, значение — зашифрованное Set-Cookie из результатов вызова), и устанавливаем их, получится что-то вроде

Set-Cookie:oI5upmClXJaq6DY4QWT5g5ZsvQ=niFJ3pLNsqSh0x19ux1HB-3XQiMb3XDhuJC5Byrefm_xOIDJBlZ2FL5q2zvyVtPcNOimtTk-lfoY; expires=Mon, 28-Dec-2015 08:23:57 GMT; Max-Age=7776000


5.2. Если content-type не содержит text или javascript — например, изображения, gif-пиксели — отдаем контент as-is, настроив кэширование по вкусу (но лучше не надо). И выходим

5.3. Для всех остальных content-type ищем в теле ответа URL по паттерну, шифруем каждый, и используя переданный от сервера префикс, заменяем вызовы на замаскированные, то есть, места вида

object_1986381203 += '<a href="http://ads.XXXXX.ru/228129/goLink?pr=gleurtb&p5=dcxnh&p1=bohgi&p2=ezfl" target="_blank"><img src="http://content.XXXXX.ru/150924/XXXX/507850/1421973.jpg" width="990" height="90" alt="" border=0></a>';

Будут отданы так:

object_1986381203 += '<a href="/meduza/2015/09/28/shapitorbFgFQ4Y7_Z3jaPSRix09Pn5VyrnV5P" target="_blank"><img src="/meduza/2015/09/28/shapitorbFgFQ4Y7_Z3jaPSRix09Pn5VyrnV5Pcki64JcXvjIyzlgYXerh3yMMgY8DB5vleYA" width="990" height="90" alt="" border=0></a>';

Отдаем измененный контент и выходим.

6. Браузер сам сделает нужные вызовы, для каждого из которых будет пройдено всё с п.1

Секция конфигурации скрипта

$CONF = array(
	'mask_urls' => array(
		'prefix' => '/meduza/2015/09/28/shapito', //default, will be overriden by `url_mask_prefix`
		'url_search_patterns' => array(
			'@https?://[^\\\'\"\n\r\?]+@i',
		),
	),

	'redirect_if_contains' => array(
		'/click', '/reference', '/link', '/goLink'
	),

	'encrypt' => array (
		'iv' => '2uV17Dil',
		'key' => 'JbaSyaXwD46qIlKdt8mJ4',
		'cipher' => MCRYPT_GOST, 
		'mode' => MCRYPT_MODE_CFB
	),

	'cookie' => array(
		'expire' => 90*24*60*60,
	),

	'url_call' => array(),
);

Ещё раз, это концепт — использовать его в продакшн нельзя! Он сделает из вашего сервера анонимный прокси

adbb.php
<?php 

$CONF = array(
	'mask_urls' => array(
		'prefix' => '/meduza/2015/09/28/shapito', //default could be taken from `url_
		'url_search_patterns' => array(
			'@https?://[^\\\'\"\n\r\?]+@i',
		),
	),

	'redirect_if_contains' => array(
		'/click', '/reference', '/link', '/goLink'
	),

	'encrypt' => array (
		'iv' => '2uV17Dil',
		'key' => 'JbaSyaXwD46qIlKdt8mJ4',
		'cipher' => MCRYPT_GOST, 
		'mode' => MCRYPT_MODE_CFB
	),

	'cookie' => array(
		'expire' => 90*24*60*60,
	),

	'url_call' => array(),
);

require_once __DIR__."/adbb_functions.php";

if (   !array_key_exists('query', $_REQUEST) or !$query = $_REQUEST['query']) return;


adbb_debug_log('Request params', $_REQUEST);

$extra_query = '';
if (($delpos = strpos($query, '&')) !== false) {
	adbb_debug_log('Extra parameters passed, getting encypted part only. @ position ', $delpos);	
	$extra_query = substr($query, $delpos);
	adbb_debug_log('Extra query ', $extra_query);	
	$query = substr($query, 0, $delpos);
	adbb_debug_log('Encrypted query after cut ', $query);	
}

adbb_debug_log('We should have successfully decrypted url with the key for all further logic, decrypting...');

if (  !$url = adbb_decrypt($query, $CONF['encrypt']) )   { 
	adbb_debug_log('Failed. Exiting');	
	return;	
}

adbb_debug_log('Decrypted URL', $url);

if (array_key_exists('url_mask_prefix', $_REQUEST)) {

	adbb_debug_log('Overriding initial url_mask_prefix ['.$CONF['mask_urls']['prefix'].'] to', $_REQUEST['url_mask_prefix']);
	$CONF['mask_urls']['prefix'] = $_REQUEST['url_mask_prefix'];

}

$extra_args = $_REQUEST;
unset($extra_args['query'], $extra_args['ua'], $extra_args['remote_addr'], $extra_args['url_mask_prefix']);

$url_to_call = $url . (strpos($url, '?') === false ? '?' : '&' ) . http_build_query($extra_args).'&'.$extra_query;


adbb_debug_log('Looking for cookies to send to remote host...');
$domain_cookies = ''; 
$parse = parse_url($url_to_call);
$domain_to_call = $parse['host'];

if ($domain_to_call) {
	$domain_cookie_key = adbb_encrypt($domain_to_call, $CONF['encrypt']);
	
	adbb_debug_log('Will look for cookies for domain ['.$domain_to_call.'], key', $domain_cookie_key);
	if (isset($_COOKIE) and array_key_exists($domain_cookie_key, $_COOKIE) and $_COOKIE[$domain_cookie_key]) {

		adbb_debug_log('Found something, will try to decrypt cookie value ', $_COOKIE[$domain_cookie_key]);
		if ($json_encoded_cookies = adbb_decrypt($_COOKIE[$domain_cookie_key], $CONF['encrypt'])
			and $cookies = @json_decode($json_encoded_cookies) ) {

			adbb_debug_log('Going to send cookies decrypted', $cookies);
			$domain_cookies = implode('; ', $cookies);

		}
	}
}

adbb_debug_log('Checking if redirect is needed...');

foreach ($CONF['redirect_if_contains'] as $needle) {

	if (strpos($url_to_call, $needle) !== false ) {

		$url_to_call .= '&cookies='.urlencode($domain_cookies);
		adbb_debug_log('URL contains '.$needle.' going to redirect to ', $url_to_call);
		
		header('Location: '.$url_to_call);
		exit;
	}
}

adbb_debug_log('About to call remote URL ', $url_to_call);
adbb_debug_log('With User-Agent ', $_REQUEST['ua']);
adbb_debug_log('With X-Forwarded-For ', $_REQUEST['remote_addr']);
adbb_debug_log('With Cookie  ', $domain_cookies);

$result = adbb_call_url(
	$url_to_call, '', $_REQUEST['ua'], 
	array(
		'X-Forwarded-For' => $_REQUEST['remote_addr'],
		'Cookie' => $domain_cookies
	)
);

if ($result) {
	adbb_debug_log('Call successfull.');		
	$info = $result['info'];
	$header = $result['header'];
	$content = $result['content'];

	adbb_debug_log('Remote response info', $info);
	adbb_debug_log('Remote response headers', $info);

	$parse = parse_url($url);
	$domain = $parse['host'];

	if (array_key_exists('Set-Cookie', $header[count($header)-1])) {

		adbb_debug_log('Found Set-Cookie in response, taking last one');
		$domain_set_cookies = $header[count($header)-1]['Set-Cookie'];

		$domain_cookies = adbb_translate_cookie_values($domain_set_cookies);
		adbb_debug_log('Translated domain cookies', $domain_cookies);

		$json_encoded_cookies = json_encode($domain_cookies);
		adbb_debug_log('Json encoded cookies', $json_encoded_cookies);

		$encrypted_cookies_domain = adbb_encrypt( $domain, $CONF['encrypt'] ) ;
		$encrypted_cookies_values = adbb_encrypt( $json_encoded_cookies, $CONF['encrypt'] ) ;
		adbb_debug_log('About to set encrypted cookie for domain ['.$domain.'] as ['.$encrypted_cookies_domain.'] with value ', $encrypted_cookies_values);

		setcookie($encrypted_cookies_domain, $encrypted_cookies_values, time()+$CONF['cookie']['expire']);

	}

	if (strpos($info['content_type'], 'text') === false and strpos($info['content_type'], 'javascript') === false ) {

		adbb_debug_log('Non-text content type '.$info['content_type'].', passing as is. Cache headers not implemented');
		header('Content-Type: '.$info['content_type']);
		echo $content;

	} else {

		adbb_debug_log('Initial content in remote response', $content);
		$new_content = adbb_mask_urls($content, $CONF['mask_urls'], $CONF['encrypt']);
		echo $new_content;	
	}

}



adbb_functions.php
<?php 

function adbb_encrypt($data, $encrypt_conf) {
	$iv = $encrypt_conf['iv'];

  	return rtrim(strtr(
  			base64_encode(mcrypt_encrypt($encrypt_conf['cipher'], $encrypt_conf['key'], $data, $encrypt_conf['mode'],$iv)),
  			 '+/', '-_'),
  		'='); 

} 

function adbb_decrypt($data, $encrypt_conf) {

	$encrypted = base64_decode(str_pad(strtr( $data, '-_', '+/'), strlen( $data ) % 4, '=', STR_PAD_RIGHT));

	return mcrypt_decrypt(
		$encrypt_conf['cipher'], $encrypt_conf['key'], $encrypted, $encrypt_conf['mode'], $encrypt_conf['iv']
	);
} 

function adbb_translate_cookie_values($set_cookies) {
	$keys = array();
	foreach ($set_cookies as $sc)   {
		if ($pos = strpos($sc, ';')) {
			$keys[] = substr($sc, 0, $pos);
		} else {
			$keys[] = $sc;
		}
	}

	return $keys;
}

function adbb_get_headers_from_curl_response($headerContent)
{

    $headers = array();

    // Split the string on every "double" new line.
    $arrRequests = explode("\r\n\r\n", $headerContent);

    // Loop of response headers. The "count() -1" is to 
    //avoid an empty row for the extra line break before the body of the response.
    for ($index = 0; $index < count($arrRequests) -1; $index++) {

        foreach (explode("\r\n", $arrRequests[$index]) as $i => $line)
        {
            if ($i === 0)
                $headers[$index]['http_code'] = $line;
            else
            {
                list ($key, $value) = explode(': ', $line);
                if (array_key_exists($key, $headers[$index])) {
                	if (is_array($headers[$index][$key])) {
                		$headers[$index][$key][] = $value;
                	} else {
                		$t = $headers[$index][$key];
                		$headers[$index][$key] = array( $t );
                		$headers[$index][$key][] = $value;

                	}
                } else {
                	$headers[$index][$key] = $value;	
                }
            }
        }
    }

    return $headers;
}

function adbb_call_url($url, $cookie, $ua, $headers) {
	$ch = curl_init();

	$curlopt_headers = array();

	foreach ($headers as $k => $v) $curlopt_headers[] = $k.': '.$v;

	$options = array(
		CURLOPT_URL => $url, 
		CURLOPT_RETURNTRANSFER => true,
		CURLOPT_FOLLOWLOCATION => true,
		CURLOPT_USERAGENT => $ua,
		CURLOPT_CONNECTTIMEOUT => 5,
		CURLOPT_TIMEOUT => 5,
		CURLOPT_MAXREDIRS => 5,
		CURLOPT_SSL_VERIFYHOST => 0,
		CURLOPT_COOKIE => $cookie,
		CURLOPT_HTTPHEADER => $curlopt_headers,

		CURLOPT_VERBOSE => 1,
		CURLOPT_HEADER => 1,
	);

	curl_setopt_array($ch, $options);

	$response = curl_exec($ch);

	if (!$response) return false;

	$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
	$header = adbb_get_headers_from_curl_response(substr($response, 0, $header_size));
	$content = substr($response, $header_size);


	$info = curl_getinfo($ch); 

	return array(
		'header' => $header,
		'content' => $content,
		'info' => $info 
	);

}

function adbb_mask_urls($content, $mask_conf, $encrypt_conf) {
	$prefix = $mask_conf['prefix'];

	$url_search_patterns = $mask_conf['url_search_patterns'];

	foreach ($url_search_patterns as $pattern) {
		$matches = array();
		if (preg_match_all($pattern, $content, $matches)) {
			adbb_debug_log('Url matches ',$matches);
			foreach ($matches[0] as $m) {
				$encrypted_url = adbb_encrypt($m, $encrypt_conf);
				$content = str_replace($m, $prefix.$encrypted_url, $content);
			}
		}
	}

	return $content;
}


function adbb_debug_log($message, $obj = false) { 
	// $s = $message.($obj ? ' ( '.(is_string($obj) ? $obj : var_export($obj,true) ) . ' )' : '' )."\n";
	// static $f = null; 
	// if (!$f and $f = @fopen("/tmp/adbb_tmp_log".date("Ymd"),"a+")) {
	// 	fputs($f, "== New call ==");
	// }
	// if ($f) {
	// 	fputs($f, $s);
	// }
	//echo  $s ;  
} 



Для получения самой ссылки первичного входа, выполните с той же конфигурацией

adbb_encrypt('http://ads.XXX.ru/XXXX/prepareCode?pp=g&ps=bugf&p2',  $CONF['encrypt']);


Спасибо за внимание.
Tags:
Hubs:
+3
Comments57

Articles

Change theme settings