13 минут
Лунные сонеты. Язык Lua на службе у пентестеров
Репост моей статьи про язык Lua из журнала “Хакер” за декабрь 2013 (№ 179).
Разработчики давно поняли: чтобы сделать программу по-настоящему гибкой и расширяемой, нужно добавить внутрь хороший скриптовый язык. Тем более что придумывать ничего не надо: есть Lua, который прекрасно интегрируется, чем и воспользовались создатели Wireshark, Nmap и даже малвари.
Intro
Ранее мы уже писали немного об этом лунном языке (Lua в переводе с португальского означает «Луна») в одном из спецномеров “Хакера” (№ 64, март 2006-го), выпуск которого был посвящен программированию игр. Сегодня же мы поговорим о применении Lua в сфере информационной безопасности. Написать этот раздел и в принципе статью меня подвигло прочтение исследования популярного вируса Flame, авторы которого использовали Lua для быстрого расширения возможностей своего оружия. Если уж даже создатели малвари используют его, стало быть, он и правда хорош.
После небольшого изучения темы оказалось, что поддержка языка реализована в популярных утилитах, с которыми приходится иметь дело каждый день, при этом я почему-то обходил такую возможность стороной. Пришло время это исправить.
Расширение для Wireshark
В работе мне довольно часто приходится анализировать большие объемы трафика, причем искать в них вполне конкретные паттерны. Поддержка Lua для написания расширений в Wireshark может сэкономить кучу времени (которую можно потратить, скажем, на сон).
При небольшой сноровке можно писать довольно сложные сценарии для поиска паттернов и анализа трафика. Но для примера мы возьмем что-нибудь простое и показательное — напишем простой скрипт для парсинга полей имени хоста и cookies. И параллельно разберем несколько ошибок, которые могут возникнуть в процессе разработки расширения.
Интерпретатор языка находится по следующему пути: Tools -> Lua -> Evalute
, куда мы и вставляем наш скрипт.
Скрипт также можно выполнить через консоль, используя tshark:
tshark -r $FILE.pcap -X lua_script:script.lua
Так как скрипт небольшой, я приведу код сразу, а далее мы его разберем по частям:
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
Итак, поехали. Сначала для более удобного вывода нужных нам данных создадим отдельное окно для текстовой информации:
cookiewindow = TextWindow.new("List cookies")
В принципе, можно уже выполнить эту строку после запуска Wireshark, и мы получим желанное окно, которое представлено на скриншоте.
Далее создадим переменные, которые будут обращаться к полям host
и cookie
HTTP-протокола:
local hostname = Field.new("http.host")
local cookiedata = Field.new("http.cookie")
Таким образом можно обращаться к любым полям любого протокола, а как мы все помним, этот замечательный снифер из коробки знает структуру колоссального числа протоколов и умеет «молотить» их на понятные части на лету (впрочем, для неизвестного протокола легко прописать структуру).
Теперь напишем функцию инициализации приемника пакетов и установим фильтр на HTTP-протокол:
local function init_listener()
local tap = Listener.new("http")
Здесь можно также написать фильтр на определенный IP-адрес — в общем, все то, что мы обычно указываем в поле Filter
при обычной работе с Wireshark. После этого распишем функцию tap.packet
, которая будет выполняться каждый раз при поимке пакета, и определим переменные для так называемых extractor
, которые получают значения указанных выше полей.
function tap.packet(pinfo,buffer,userdata)
local targethost = hostname()
local targetcookie = cookiedata()
Далее все совсем просто, переводим в строки полученные переменные и добавляем к созданному окну:
cookiewindow:append(tostring(targethost))
Единственный нюанс: объединить переменные с помощью операции объединения строк нельзя, поскольку такие данные имеют тип userdata, а Lua не может напрямую их изменять. Но это можно обойти через метатаблицы.
Теперь выполним наш скрипт и запустим снифинг пакетов.
Работает! Но как видишь, у расширения есть минус — он выводит одинаковые печеньки, поэтому в идеале надо сделать таблицу, где будут храниться старые значения, и добавить проверку на уникальность. Но это я оставляю тебе в качестве домашнего задания. Если же ты не справишься или ты из тех людей, которые любят готовые рецепты, то полная версия будет ждать тебя на моем GitHub-аккаунте.
Если при запуске скрипта возникла ошибка с текстом «Field_get: A Field extractor must be defined before Taps or Dissectors get called», значит, скрипт был запущен после старта снифинга пакета и нужно будет перезапустить Wireshark.
В сноске ты найдешь ссылки на вики и API с более подробной информацией по написанию расширений. Помимо этого, приведены примеры рабочих скриптов, один из которых сохраняет VoIP-звонки из пакетов в отдельные файлы и работает с SQL базой данных.
Расширение для Nmap
Nmap так же, как и Wireshark, позволяет активно использовать Lua, чтобы серьезно прокачать возможности сканера и превратить его в своего рода Metasploit (мы об этом уже писали). Часть программы, которая отвечает за работу с языком, называется Nmap Scripting Engine (NSE), поэтому все скрипты для сканера имеют расширение nse
.
Разберем стандартную структуру скрипта. В начале каждого сценария добавляются стандартные библиотеки:
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
Далее идут метаданные:
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
После этого идет непосредственно код сценария. Для примера напишем простейший скрипт для брута пользователей популярной CMS LiveStreet.
Вначале определим стандартные переменные и подключим недостающие библиотеки:
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 -- количество потоков
Далее составим класс Driver
для брута из соответствующей библиотеки. Напишем функцию инициализации с указанием хоста, порта и пути до формы логина:
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,
Функции коннекта и дисконнекта оставим без изменений:
connect = function( self )
return true
end,
disconnect = function( self )
return true
end,
Далее пишем проверку, чтобы посмотреть успешность коннекта и наличие поля для пароля:
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
Ну и самая главная часть — функция логина. Сначала составляем HTTP-запрос:
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 = "" } )
Далее указываем результат, после которого считается, что пара логин с паролем, переданные в запросе, были правильными:
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,
В случае с сайтами на WordPress значение будет 302. Другой вариант — проверять появление нового HTML-элемента на странице (например, после логина обязательно появится ссылка для выхода пользователя из системы):
<a href="http://site.com/login/exit/?security_ls_key=<key>">выход</a>
Теперь распишем главную функцию action
, которая обязательно должна быть в коде:
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()
Сохраним полученный скрипт с расширением nse
и положим рядом два файла: users.txt
и passwords.txt
— с некоторым количеством имен пользователей и паролей. В случае LiveStreet можно даже напарсить валидные имена пользователей или добавить этот функционал в скрипт.
Теперь запустим полученный сценарий (полный исходник скрипта можно скачать из моего GitHub-репозитория):
nmap -p80 --script http-livestreet-brute --script-args 'userdb=users.txt,passdb=passwords.txt' <target>
Если все пройдет успешно, то ты увидишь отчет о проделанном брутфорсе.
Как видишь, код не такой сложный, что позволяет быстро написать свой скрипт под определенную цель, с которой ты столкнулся в процессе проведения пентеста.
Outro
Вообще, использование Lua растет и растет. Так, на момент написания статьи была опубликована новость, что появился плагин для Olly Debugger версии 2+, добавляющий поддержку Lua.
Впрочем, язык не ограничивается одними программами для ИБ-сферы и может пригодиться в самых разных ситуациях. Как я уже говорил, его часто используют для игр, например World of Warcraft в своих аддонах (предоставляя API и довольно неплохую документацию по нему). Помимо игровых приложений, его задействуют такие программы, как Setup Factory. На нем написаны многие инсталляторы: Apache, nginx, Adobe Photoshop Lightroom, VLC и многие другие. Там, где нужно реализовать программируемое поведение программы, — интегрированный интерпретатор Lua точно будет очень неплохим вариантом.
Ссылки
Встраиваем Lua в различные языки
- Встраиваем Lua в программы на C
- Встраиваем Lua в программы или игры на C#
- Список библиотек и фреймворков для работы с Lua из программ на различных языках программирования
Изучаем Lua
- Изучаем Lua за 15 минут
- Изучаем Lua за 60 минут
- Список официальных книг и онлайн-версия первой редакции
Материалы для Wireshark
Материалы для Nmap
Врезки
Шпаргалка по Lua
Первое, что надо знать, — Lua — это язык с динамическим определением данных, то есть переменные получают тип на лету в зависимости от своего содержания. Всего используется восемь типов:
- nil (пустое значение);
- boolean (логический);
- number (числовой);
- string (строковый);
- function (функция);
- userdata (пользовательские данные, если вкратце, то это данные программы, с которой взаимодействует Lua);
- thread (поток);
- table (таблица — самый интересный тип, он включает в себя свойства как массива, структуры, так и списка, множества и представляет собой набор пар (ключ, значение), то есть является хеш-таблицей).
Условные операторы и циклы довольны стандартны:
-- комментарии обозначаются двумя тире
-- условные операторы
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
Операции также почти все знакомые:
- присваивание: x = 0;
- арифметические: +, -, *, /, % , ^;
- логические: and, or, not;
- сравнение: >, <, ==, <=, >=, ~= (первое отличие, такой оператор вместо
!=
); - объединение строк:
..
(второе отличие, вместо+
или.
); - длина/размер переменной: #;
- получение элемента по индексу: array[2].
Начиная с 5.2 доступны наши любимые битовые операции через таблицу bit32
. Например, выведем XOR-значение в запущенном интерпретаторе Lua:
> value = 0xffff0000
> key = 0x00ffff00
> = string.format("%08x",bit32.bxor(value, key))
ff00ff00
Функцию мы рассмотрим на примере тестового скрипта в нашей программе. Более подробно про этот язык можешь прочитать по ссылкам в сносках. Для написания программ достаточно будет Notepad++ или Sublime Text.
Встраиваем компилятор в свою программу
Итак, теперь главная фишка Lua в том, что его можно встроить в любую программу и предложить всем желающим расширить ее возможности. Как это сделать? Я покажу на примере Visual C++ программы. Стоит отметить, что сам Lua написан на чистом C, и это создает некоторые сложности при добавлении в программы с плюсами. На просторах Сети предлагается несколько вариантов добавления поддержки Lua в C++ программы. Кто-то предпочитает скачивать уже прекомпилированные библиотеки или готовые фреймворки, кто-то — компилировать из исходников самому, а кто-то — подключать исходники к своему проекту и, немного разобравшись с настройками в проекте, добиться действительного добавления Lua в свою программу. Мы с тобой как раз и разберем последний вариант.
Создадим новый консольный проект (я пользуюсь Visual Studio 2010). После чего добавим через свойства проекта директорию с исходниками:
Properties -> Configuration properties -> C/C++ -> General
Properties -> Configuration properties -> Linker -> General
где в каждом из разделов добавим в соответствующие поля ... Directories
путь к исходникам языка. Далее создаем раздел Lua, куда добавляем все файлы из папки src
:
Project\New Filter
...
Project\Add Existing Item
Как я уже упомянул выше, Lua написан на чистом C, поэтому применим небольшой хак — добавим в начало своего файла следующие строки:
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
};
Далее инициализируем наш язык и подключим его библиотеки:
lua_State * L = luaL_newstate();
luaL_openlibs(L);
Теперь создадим цикл, который будет проверять входящие данные, пока они не закончатся (или не переполнят буфер), и выполнять их:
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);
}
}
Команда luaL_loadbuffer
загружает строку из буфера в Lua, но не исполняет ее. Для выполнения нужно вызвать lua_pcall
. После окончания цикла закрываем экземпляр с Lua
lua_close(L);
Теперь можно компилировать проект и запустить.
В ходе компиляции могут возникнуть ошибки:
file.с 'Debug\lua_interpretator.pch' precompiled header file ...
;- после запуска постоянно появляется родное окно интерпретатора Lua.
Для исправления первой ошибки нужно отменить PCH-компиляцию для всех проблемных C-файлов. В случае второй ошибки исключи из проекта файлы lua.c
и luac.c
.
Следующий проект создадим аналогичный первому, но теперь будем запускать свои сохраненные скрипты в самой программе, инициализируем Lua и добавим после:
const char *testscript = {
"function double(n)\n"
" return n * 2\n"
"end\n"
"\n"
"io.write(double(1))\n"
};
Это и есть обещанный пример объявления и работы с функцией. Теперь загрузим его в память и выполним:
luaL_dostring(L, testscript);
В этом случае мы обошлись без pcall
, так как используем макрос с приставкой do-
вместо load-
, который вызывает одновременно loadstring
и pcall
, как в следующем примере:
luaL_loadstring(L, "io.write('Hello XAKEP from C++')");
lua_pcall(L, 0, 0, 0);
Теперь рассмотрим возможность вызова функций C-программы из Lua-скриптов. И для начала объявим функцию, которая будет вызываться из нашего языка:
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; // Количество возвращаемых значений
}
Далее регистрируем функцию:
lua_register(L, "my_function", my_function);
Прописываем функцию, которая загружает по переданному пути скрипт:
int s = luaL_loadfile(L, file);
И запускаем загруженный скрипт:
s = lua_pcall(L, 0, LUA_MULTRET, 0);
Но, если заметил, вызов немного отличается от предыдущего проекта. Константа LUA_MULTRET
используется вместе с функцией lua_gettop
для подсчета стека до и после вызова. Компилируем, не забывая про описанные выше ошибки.
Теперь пропишем вызов этой функции из Lua в отдельный скрипт и назовем его call_func.lua
:
io.write("Running ", _VERSION, "\n")
a = my_function(1, 2, 3, " xa", "kep ")
io.write("my_function() returned ", a, "\n") -- Выводим значение, которое вернула функция
Вызовем полученный файл из нашей программы:
calling_functions.exe call_func.lua
И получим следующий вывод в консоль:
Готовые исходники проектов ты можешь взять из GitHub, но не забывай, что в них прописаны пути к моей Lua и для экономии места вырезаны функции обработки ошибок Lua-скриптов. Еще я не подробно расписал одну интересную тему — работу со стеком, которая позволяет обмениваться данными с основной программой, хотя главное мы затронули в последнем проекте.