Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
В сегодняшней статье мы поговорим о Ruby, вернее — о тонкостях и нюансах реверса написанных на этом языке приложений. Мы разберем особенности таких программ, перечислим полезный инструментарий для их исследования и найдем простой способ их отладки.
Когда‑то о Ruby было написано множество рекламных статей. Для тех, кто хочет более детально ознакомиться с его концепцией и внутренней структурой, есть книга Ruby under a microscope. В общем, как ты уже понял, цель сегодняшней статьи — не постижение дзена программирования на Ruby, а сухой практический разбор особенностей реверса приложений, реализованных на этой экзотике.
Итак, дано приложение, читающее и валидирующее некий бинарный файл. Нам нужно разобраться с алгоритмом валидации. Первичный автоматический анализ приложения при помощи Detect It Easy на этот раз нам толком ничего не дает — приложение детектится как собранное из C++ в банальном Microsoft Visual Studio 2015. Однако в комплект динамических библиотек программы входит интересный файл x64-msvcr100-ruby200.dll
, на который полно ссылок из исполняемого модуля. Файл позиционирует себя как интерпретатор Ruby за авторством самого отца‑основателя Юкихиро Мацумото со ссылкой на соответствующий сайт.
Вдобавок в комплект программы входит около двух с половиной тысяч файлов *.rb
, при ближайшем рассмотрении оказавшихся обычными текстовыми ничем не защищенными Ruby-скриптами. Как ты уже догадался, есть маленький подвох, без которого эту статью писать было бы неинтересно: ни в одном из этих файлов обращений к нашему целевому файлу не нашлось, а значит, придется загружать нашу программу в отладчик x64dbg и искать обращение к файлу в динамике интерпретированного кода.
Действуем по стандартной схеме, наверняка уже знакомой тебе по моим предыдущим статьям. Ставим точку останова на функцию чтения файла kernel32.dll.ReadFile
. Конечно же, при загрузке программы будут тысячи подобных обращений ко всевозможным файлам настроек и библиотек, входящих в пакет, но нам немного облегчит задачу мониторинг чтения файлов с помощью тоже известной нам программы Process Monitor (ProcMon). Благодаря ей мы замечаем, что чтение нашего искомого файла, в отличие от всех остальных, идет исключительно блоками по 0x10000
байт. Поэтому, чтобы отсечь все ненужные нам чтения, можно поставить в качестве условия для останова Size==0x10000
.
Запускаем нашу программу и ждем достижения этой точки. Проанализируем стек вызова полученного ReadFile
.
Верхние пять вызовов представляют собой нативную обвязку чтения файла и нам неинтересны. А вот ниже явно идут два вложенных вызова интерпретатора, до боли знакомых нам по разбору виртуальных машин других скриптовых языков. Чтобы не отвлекаться на повторение того, что я многократно писал в своих предыдущих статьях, постараюсь сказать в двух словах. Любой, даже самый упоротый (вроде питона) интерпретатор скриптового языка никогда не разбирает текстовую семантику во время выполнения. Для оптимизации работы интерпретатора во время загрузки модуля (класса, объекта, метода и так далее) происходит компиляция текста в натив или шитый байт‑код, сам процесс называется JIT-компиляцией (just-in-time, в нужное время) или попросту динамической компиляцией.
Разумеется, несмотря на заявленную экстравагантность, в точности так же поступает и Ruby. Про особенности JIT-компиляции для разных реализаций Ruby можно почитать на сайте patshaughnessy.net, а мы попробуем разобраться с нашей виртуальной машиной. Немного поизучав вложенные вызовы стека с предыдущего скриншота, находим основной цикл выборки команд шитого кода (выделены стрелками). В IDA код этой процедуры (sub_18001B6E0
, экспортируемая функция — rb_vm_get_insns_address_table
) выглядит так.
Как видим, опкод занимает 8 байт, виртуальная машина содержит 83 команды ассемблера, реализация каждой из которых представлена в этой функции. Оказывается, библиотека интерпретатора даже содержит в себе дизассемблер для компилированного байт‑кода (экспорты rb_iseq_disasm_insn
, rb_iseq_disasm
). Анализируя эти функции, можно найти таблицу мнемоник команд, находящуюся по адресу 180200500
:
00 nop 01 getlocal 02 setlocal 03 getspecial 04 setspecial 05 getinstancevariable 06 setinstancevariable 07 getclassvariable 08 setclassvariable 09 getconstant 0A setconstant 0B getglobal 0C setglobal 0D putnil 0E putself 0F putobject 10 putspecialobject 11 putiseq 12 putstring 13 concatstrings 14 tostring 15 toregexp 16 newarray 17 duparray 18 expandarray 19 concatarray 1A splatarray 1B newhash 1C newrange 1D pop 1E dup 1F dupn 20 swap 21 reput 22 topn 23 setn 24 adjuststack 25 defined 26 checkmatch 27 trace 28 defineclass 29 send 2A opt_send_simple 2B invokesuper 2C invokeblock 2D leave 2E throw 2F jump 30 branchif 31 branchunless 32 getinlinecache 33 onceinlinecache 34 setinlinecache 35 opt_case_dispatch 36 opt_plus 37 opt_minus 38 opt_mult 39 opt_div 3A opt_mod 3B opt_eq 3C opt_neq 3D opt_lt 3E opt_le 3F opt_gt 40 opt_ge 41 opt_ltlt 42 opt_aref 43 opt_aset 44 opt_length 45 opt_size 46 opt_empty_p 47 opt_succ 48 opt_not 49 opt_regexpmatch1 4A opt_regexpmatch2 4B opt_call_c_function 4C bitblt 4D answer 4E getlocal_OP__WC__0 4F getlocal_OP__WC__1 50 setlocal_OP__WC__0 51 setlocal_OP__WC__1 52 putobject_OP_INT2FIX_O_0_C_ 53 putobject_OP_INT2FIX_O_1_C_
При внимательном рассмотрении идентифицируется и сам тип интерпретатора байт‑кода, это разновидность YARV.
Интерпретатор мы определили, байт‑код, из которого вызывается чтение нашего файла, нашли, но что дальше? Реверсить работу с файлом, анализируя скомпилированный байт‑код, даже при наличии встроенного дизассемблера как‑то грустно. Хочется найти текстовый исходник Ruby-скрипта, хотя, конечно, не исключена возможность, что интерпретатору подсунули уже искусственно скомпилированный байт‑код. Но пока что не будем рассматривать столь крайние варианты. Попробуем хотя бы определить имя метода, вызывающего чтение файла.
Источник: xakep.ru