Kailo
Участник
- Сообщения
- 194
- Реакции
- 896
Теперь мы поговорим о том как получить из байт-кода исходный код. Для примера я написал маленький плагин с показательными моментами, который мы и будем разбирать как пример.
Байт-код плагина имеет вид:
Байт-код такого вида я получаю от своего декомпилятора, в начале строки обозначен адрес инструкции, а комментариями показаны адреса инструкций с переходами по данному адресу.
Впрочем smxviewer или SPEdit дают похожую картинку, иногда уже заранее преобразовав адреса в имена, что делает код более похожим на ассемблер.
Первым делом мы находим proc инструкции и разбиваем байт-код на 4 функции.
Теперь будем по очереди разбирать эти функции.
Получение прототипа функции
Первым делом для первой функции мы ищем как можно больше информации о ней что бы получить её прототип:
По адресу функции (3192) находим в .dbg.symbols секции описание функции
По tagid смотрим секцию .tags и по ссылки на имя типа в секции .names находим, что tagid 6 соответствует void.
ident 9 позволяет нам убедится что мы нашли правильный символ, т.к. 9 - IDENT_FUNCTION.
По name ищем в .dbg.strings название функции, и для 209 в данном случае получаем "OnPluginStart".
В результате имеем:
Теперь надо получить информацию о аргументах функции.
Делаем поиск в .dbg.symbols по codestart равным адресу нашей функции (аргументы создаются в начале вызова и уничтожаются в конце вызова, имея время жизни и видимость на протяжении всей функции.
В данном случае символов с codestart равным 3192 не найдено (кроме символа самой функции), это значит что аргументов эта функция не имеет (о чем вы и так уже догадались по её названию).
Теперь определим квалификаторы функции.
Смотрим в .public секцию в поиске адреса нашей функции и находим
Теперь важная ремарка связанная с разными версиями компилятора. Ранее квалификатор на уровне smx имели только обозначены public в исходном коде функции. Позднее при компиляции стали все функции приравнивать к public, даже те, что не имеют в объявлении public, для того чтобы иметь возможность найти их с помощью GetFunctionByName и вызвать их через Call_StartFunction, имена функций, которые не имели квалификатора public будут с припиской адреса в начале. К примеру если бы OnPluginStart не имел бы public, то его имя в .names секции было записано как ".3192.OnPluginStart".
Таким образом чтобы понять, надо ли добавлять public, надо заглянуть не только в .publics секцию, но и посмотреть её имя в .names секции.
В итоге мы получаем, что функция имеет прототип:
Преобразование байт-код в псевдокод
Начнем разбирать содержимое функции.
Первые две инструкции proc и break мы уже преобразовали в тело самой фукнции, отбрасываем их.
Далее надо выделить структурные блоки кода. В этом нам очень помогают break инструкции, в работе кода они не участвуют и нужны для работы отладчика, но при этом подсказывают нам где начинается новая строка и новый кусок кода. (т.е. образно break нам показывает где были ';' в исходном коде и как делится код.
Заглянем в начале в конец функции: Если вы внимательно читали прошлые уроки и помните о том как преобразуется "return" конструкция в байт-код, то по отсутствию break инструкции перед "3248: zero.pri" можно понять, что это функция, которая не имела return в конце её тела.
Таким образом, делим код на блоки и отбрасываем уже разобранные нами, получим:
Имитация виртуальной машины
При должном опыте и сноровке можно понять в голове как будет происходить преобразование инструкций, но когда инструкций много (при сложных математических выражения или вложенных вызовах) это становится не так легко, и чтобы не запутаться я прибегаю к методу имитации работы виртуальной машины по исполнению этого кода.
Отслеживаем значения хранимые в основном и вспомогательном регистре, а так же текущий кадр стека. Считаем их изначально пустыми.
Начинаем имитировать выполнение инструкций
Далее инструкций нет, а это значит что блок был окончен. Получаем:
Важно помнить, что все аргументы константы, а это значит, что это либо действительно просто числа или адреса.
Чтобы понять, что есть что, по индексу ищем в .dbg.natives информацию о нативе.
Если кто-то намерено испортил или удалил .dbg.natives, что не будет мешать плагину работать, мы можем из .natives секции узнать только имя функции. В данном случае получим:
Здесь несколько вариантов приводящих к одному результату. Мы можем найти описание всем нам известной функции в интернете:
Или проанализировав .dbg.natives информацию построить его самим:
Здесь же вы сразу же можете заметить, что const квалификаторы используются только на стадии компиляции и информацию о них найти в smx нельзя. Собственно так же, как если бы их убрать до компиляции, результат компиляции получился бы тот же.
Из прототипа мы видим, что первый и третий параметры являются ссылками (адресами), второй параметр является ссылкой на функцию, а четвертый просто значением. Приступим к обработки аргументов вызова:
Начнем со 2-го аргумента, ссылки на функцию. Эти ссылки указывают на номер public функции внутри smx. .public секция имеет следующее содержание:
Индекс public фукнции (назовем pid) преобразуется в ссылку по следующему алгоритму:
Таким образом, что бы прийти от ссылки к индексу нам нужна целая часть от деления ссылки на 2.
Данная система нужна, чтобы INVALID_FUNCTION имел значение 0. А ссылки будут иметь вид 1, 3, 5, 7, 9 и т.д.
В нашем вызове второй параметр имеет значение 5, значит это public с индексом 2 - Cmd_Menu.
Теперь разберемся с адресами (ссылками).
Пояснение: правильно называть это передачей аргумента по ссылке (англ. by reference), т.к. мы не можем с ним работать как с просто числом, т.е. адресом.
Первый аргумент, 2224, может обозначать адрес глобальной или статической (static) переменной, или просто адрес в .data секции, откуда берется константное значение.
Ищем в .dbg.symbols переменную с адресом 2224, и ничего не находим. Следовательно это константное значение, которое надо достать из .data секции. Смотрим туда по адресу 2224:
По этой вырезки из .data секции видно, что по адресу 2224, текст "sm_menu".
Проделываем аналогичный поиск со вторым адресом (2232), и не найдя глобальных переменных видимо по этому адресу только NUL символы. Это обозначает, что там передается пустая строка.
Пояснение: Вся память должна быть выровнена по ячейкам (4 байта). Когда в память добавляться строка, к кол-во символов в строке прибавляется один NUL символ как символ окончания строки и потом дополняется NUL символами до кол-во кратного 4. Соответственно при пустой строке будет один NUL символ, дополненный до 4-х байт, что и видно по адресу 2232.
Соединив все наши выяснения получим завершенный код функции:
Теперь перейдем ко 2-ой функции.
Получаем информацию о её прототипе:
Анализирую полученную информацию получаем:
Теперь разбиваем байт-код на блоки:
Если вы видите блок оканчивающийся на инструкцию условного перехода, то это или if, или while или for. Но for и while могут быть опознаны наличием далее по коду безусловного перехода (jump) в обратном чтению кода направлении и своеобразному началу (об этом вы должны были прочитать в прошлом уроке). Резюмируем: первый блок, это if со сложным условием (составным).
Разберемся в составе условия. В конце условия:
Предыдущие операторы перехода ведут на const.pri 1, это признак того, что здесь ИЛИ условие. (В случае срабатывания перехода, устанавливается значение 1 и вызывается jzer, который как можно понять не производит перехода и выполняется код идущий после jzer.)
Теперь разберем 2 условия:
Без лишних объяснений, получаем:
Надеюсь что со следующим все понятно
И последний блок
Суммируя получаем:
Теперь приведем псевдокод к исходному коду. 3 явно должно иметь Action тип, можем сделать или view_as<Action>(3) или используем эквивалентное значение из перечисления Plugin_Handled. И так же найдем имя функции с адресом 3404.
Разберем 3ю функцию.
Готовый прототип:
Разобьем на блоки
Блок 1:
stack -4 говорит нам о объявлении переменной размером 4 байта. Указатель вершины равен указателю кадра в начале работы функции, поэтому адрес этой локальной переменной будет "0 + -4 = -4". Найти описание локальной переменной в .dbg.symbols легче всего по codestart, он равен адресу инструкции stack - 3416.
Т.к. у нас блок объявления переменной, в том же блоке могу быть только инициализации значения и объявления других переменных.
Далее мы видим stor.s.pri -4, что дает нам понять, что это инициализация объявленной переменной.
Смотрим информацию о нативе 7.
По наличию точки в названии функции можно распознать метод methodmap объявления. Т.к. название методмапа и самой функции одинаковы - это конструктор, а т.к. мы знаем, что Menu производный от Handle, должны использовать new.
Теперь преобразуем первый параметр в индекс паблик функции (7 / 2 = 3) и найдем её имя (Menu_Handler). Второй параметр просто обернём в преобразователь типа. Итог
Блок 2:
Параметры передаваемые в vararg (те что, в параметре с именем ... и далее) передаются только по адресу.
С первым параметром все понятно, переходим ко второму. Найти глобальную переменную с адресом 2236 мы не смогли, поэтому обращаемся к .data секции по указному адресу.
По адресу 2236 мы видим строку "%T", значит два переданных параметра должны быть строкой с обозначением имени фразы перевода и индекс клиента. Как видим 4-ый параметр как раз и есть индекс клиента, а значит 2240 должна быть строкой. Глобальной переменной с таким адресом мы опять не находим, а значит обращаемся опять в .data и получаем "MENU_TITLE".
Т.к. это функция часть methodmap, и должна вызывать относительно объекта, то мы обрезаем первую часть имени и переносим первый параметр на её место, в итоге получая:
Блок 3:
Надеюсь к этому моменту основная мысль стала уже понятно, поэтому тут в сокращенном режиме сначала получаем
2252 - переменной нету, из .data получаем ""
2256 - переменной нету, из .data получаем "Ping"
Итог:
Блок 4:
Он полностью аналогичен блоку 3 и в итоге получим:
Блок 5:
Здесь мы видим в конце неявный return, значит у самой функции он отсутствовал. Так же мы видим автоматически генерируемое освобождение стека (stack 4) от созданных ранее локальных переменных. Получается что сам блок будет
Все как и ранее
Соединив все блоки получим:
Последняя функция:
Начав анализировать байт-код, мы замечаем switch, значит здесь будет switch конструкция. Адрес после switch отсылает нас к casetbl в котором содержится описание нашего switch. Первая 2 обозначает что здесь две case конструкции. Значение 3852 это адрес default кейса, и он указывает на следующую после casetbl инструкцию, а это значит что здесь нет default случая. Первый кейс имеет число 4 и адрес его кода 3712, второй имеет число 16 и адрес его начала 3668. Как видно, адрес начала второго кейса ранее по коду, это из-за того, что в casetable кейсы записываются в обратном порядке их объявления в исходном коде, а в самом байт-коде они расположены логически правильно, поэтому первым будет кейс 16, а вторым 4. Структурно я специально разбил кейсы, но не разбивал их содержимое на блоки. Как видно, в конце каждого кейса добавляется jump инструкция указывающая на конец switch конструкции. Так же видно по load.s.pri 16 видно, что значением передаваемым в switch является второй аргумент функции.
Так же можно заметить, что между casetbl и zero.pri ничего нет, а значит return здесь отсутствует. Преобразим switch в исходный код:
Начнем разбирать код кейсов.
Как мы видим, мы должны как-то после вызова функции в той же инструкции присвоить значение для параметра 0. Мы вполне можем сами разделить это на два блока:
Посмотрев информацию о нативе, кому-то сразу может статья яснее, как же получился такой код.
Значит по итогу получим
Те кто хорошо читали прошлые уроки и так все знают, те кто догадался молодцы, а для остальных отвечу, то в исходном коде на этом месте была конструкция delete. Т.е. наш код:
Мы вполне могли оставить и наш прошлый вариант, т.к. данный код выполняет точно те же действия, но его байт-код будет немного отличаться.
Один case разобрали, теперь второй.
В первом блоке мы сразу же замечаем jnz 3764, и по всем признакам понимает что тут простой if со значением четвертого аргумента. Адрес нас ведет к 3му блоку, а значит в самом if только второй блок.
Изучим второй блок
По адресу 2920 находится функция PrintToChatAll. Мы могли бы её декомпилировать, но в этом нет смысла, т.к. её исходный код есть в инклудах SM. И значит 2276 - строка. Переменной с таким адресом нет, идем в .data.
И значит
Но думаю у вас уже возник вопрос "Что там делает jump 3788?" Если вы читали уроки, то знаете конечно же, но для остальных: наличие jump в конце if блока обозначает наличие else блока, и судя по адресу 3788 код с 3764 по 3788 будет внутри else.
И последний блок:
Собираем все вместе
Теперь соединим все функции в единый плагин:
А теперь сравним с исходном кодом плагина до компиляции
Как видно, в большинстве код совпадает, где-то код заменился на эквивалентный, а где-то вылез код определяемый по-умолчанию во время компиляции. Но в любой случае полученный нами код будет вести себя точно так же как и оригинальный.
Автоматизация процесса декомпиляции
Теперь, когда стало видно как происходит процесс декомпиляции и порядок действий выполняемых для этого, не сложно догадаться, что все это надо автоматизировать и запрограммировать. Большинство действий переносятся в алгоритмы, а потом в функции "как есть", к примеру получение имени натива по его индексу. Но какие-то алгоритмы, легко понятные нам людям, придется преобразовать в более простые для понимания машиной алгоритмы. В итоге весь процесс работы декомпилятора будет разделятся:
1) Предварительный парсинг байт-кода для определения сложных конструкций, таких как for, while циклы, и д.р.
2) Имитация работы виртуальной машины для преобразования кода во вложенные структуры псевдокода.
3) Трансляция псевдокода в исходный код с использованием всей сопутствующей информации сохраненной в smx (.data, .natives, .publics, .pubvars, .tags, .dbg.*).
Конец
Спасибо за прочтение.
Буду рад отзывам и вопросам по данному уроку.
Байт-код плагина имеет вид:
PHP:
3192: proc
3196: break
3200: break
3204: push2.c 0 2232
3216: const.pri 5
3224: push.pri
3228: push.c 2224
3236: sysreq.n 6 4
3248: zero.pri
3252: retn
3256: proc
3260: break
3264: break
3268: load.s.pri 12
3276: jzer 3328
3284: push.s 12
3292: sysreq.n 2 1
3304: not
3308: jnz 3328
3316: zero.pri
3320: jump 3336
3328: const.pri 1 // 3276, 3308
3336: jzer 3360 // 3320
3344: break
3348: const.pri 3
3356: retn
3360: break // 3336
3364: push.s 12
3372: push.c 1
3380: call 3404
3388: break
3392: const.pri 3
3400: retn
3404: proc
3408: break
3412: break
3416: stack -4
3424: push.c 28
3432: const.pri 7
3440: push.pri
3444: sysreq.n 7 2
3456: stor.s.pri -4
3464: break
3468: push.adr 12
3476: push2.c 2240 2236
3488: push.s -4
3496: sysreq.n 8 4
3508: break
3512: push3.c 0 2256 2252
3528: push.s -4
3536: sysreq.n 9 4
3548: break
3552: push3.c 0 2268 2264
3568: push.s -4
3576: sysreq.n 9 4
3588: break
3592: push.c 0
3600: push2.s 12 -4
3612: sysreq.n 10 3
3624: stack 4
3632: zero.pri
3636: retn
3640: proc
3644: break
3648: break
3652: load.s.pri 16
3660: switch 3824
3668: break // 3824
3672: load.s.pri 12
3680: push.pri
3684: sysreq.n 11 1
3696: zero.s 12
3704: jump 3852
3712: break // 3824
3716: load.s.pri 24
3724: jnz 3764
3732: break
3736: push2.c 2276 1
3748: call 2920
3756: jump 3788
3764: break // 3724
3768: push2.c 2284 1
3780: call 2920
3788: break // 3756
3792: push.s 20
3800: push.c 1
3808: call 3404
3816: jump 3852
3824: casetbl 2 3852 4 3712 16 3668 // 3660
3852: zero.pri // 3704, 3816, 3824
3856: retn
Впрочем smxviewer или SPEdit дают похожую картинку, иногда уже заранее преобразовав адреса в имена, что делает код более похожим на ассемблер.
Первым делом мы находим proc инструкции и разбиваем байт-код на 4 функции.
PHP:
Func3192()
{
3192: proc
3196: break
3200: break
3204: push2.c 0 2232
3216: const.pri 5
3224: push.pri
3228: push.c 2224
3236: sysreq.n 6 4
3248: zero.pri
3252: retn
}
Func3256()
{
3256: proc
3260: break
3264: break
3268: load.s.pri 12
3276: jzer 3328
3284: push.s 12
3292: sysreq.n 2 1
3304: not
3308: jnz 3328
3316: zero.pri
3320: jump 3336
3328: const.pri 1 // 3276, 3308
3336: jzer 3360 // 3320
3344: break
3348: const.pri 3
3356: retn
3360: break // 3336
3364: push.s 12
3372: push.c 1
3380: call 3404
3388: break
3392: const.pri 3
3400: retn
}
Func3404()
{
3404: proc
3408: break
3412: break
3416: stack -4
3424: push.c 28
3432: const.pri 7
3440: push.pri
3444: sysreq.n 7 2
3456: stor.s.pri -4
3464: break
3468: push.adr 12
3476: push2.c 2240 2236
3488: push.s -4
3496: sysreq.n 8 4
3508: break
3512: push3.c 0 2256 2252
3528: push.s -4
3536: sysreq.n 9 4
3548: break
3552: push3.c 0 2268 2264
3568: push.s -4
3576: sysreq.n 9 4
3588: break
3592: push.c 0
3600: push2.s 12 -4
3612: sysreq.n 10 3
3624: stack 4
3632: zero.pri
3636: retn
}
Func3640()
{
3640: proc
3644: break
3648: break
3652: load.s.pri 16
3660: switch 3824
3668: break // 3824
3672: load.s.pri 12
3680: push.pri
3684: sysreq.n 11 1
3696: zero.s 12
3704: jump 3852
3712: break // 3824
3716: load.s.pri 24
3724: jnz 3764
3732: break
3736: push2.c 2276 1
3748: call 2920
3756: jump 3788
3764: break // 3724
3768: push2.c 2284 1
3780: call 2920
3788: break // 3756
3792: push.s 20
3800: push.c 1
3808: call 3404
3816: jump 3852
3824: casetbl 2 3852 4 3712 16 3668 // 3660
3852: zero.pri // 3704, 3816, 3824
3856: retn
}
Теперь будем по очереди разбирать эти функции.
Получение прототипа функции
Первым делом для первой функции мы ищем как можно больше информации о ней что бы получить её прототип:
По адресу функции (3192) находим в .dbg.symbols секции описание функции
C-подобный:
address: 3192
tagid: 6
codestart: 3192
codeend: 3256
ident: 9
vclass: 0
dimcount: 0
name: 209
ident 9 позволяет нам убедится что мы нашли правильный символ, т.к. 9 - IDENT_FUNCTION.
По name ищем в .dbg.strings название функции, и для 209 в данном случае получаем "OnPluginStart".
В результате имеем:
PHP:
void OnPluginStart()
Делаем поиск в .dbg.symbols по codestart равным адресу нашей функции (аргументы создаются в начале вызова и уничтожаются в конце вызова, имея время жизни и видимость на протяжении всей функции.
В данном случае символов с codestart равным 3192 не найдено (кроме символа самой функции), это значит что аргументов эта функция не имеет (о чем вы и так уже догадались по её названию).
Теперь определим квалификаторы функции.
Смотрим в .public секцию в поиске адреса нашей функции и находим
C-подобный:
#num address name
#4 3192 114 // в секции .names по адресу 114 находим OnPluginStart
Таким образом чтобы понять, надо ли добавлять public, надо заглянуть не только в .publics секцию, но и посмотреть её имя в .names секции.
В итоге мы получаем, что функция имеет прототип:
PHP:
public void OnPluginStart()
Преобразование байт-код в псевдокод
Начнем разбирать содержимое функции.
PHP:
public void OnPluginStart()
{
3192: proc
3196: break
3200: break
3204: push2.c 0 2232
3216: const.pri 5
3224: push.pri
3228: push.c 2224
3236: sysreq.n 6 4
3248: zero.pri
3252: retn
}
Далее надо выделить структурные блоки кода. В этом нам очень помогают break инструкции, в работе кода они не участвуют и нужны для работы отладчика, но при этом подсказывают нам где начинается новая строка и новый кусок кода. (т.е. образно break нам показывает где были ';' в исходном коде и как делится код.
Заглянем в начале в конец функции: Если вы внимательно читали прошлые уроки и помните о том как преобразуется "return" конструкция в байт-код, то по отсутствию break инструкции перед "3248: zero.pri" можно понять, что это функция, которая не имела return в конце её тела.
Таким образом, делим код на блоки и отбрасываем уже разобранные нами, получим:
PHP:
public void OnPluginStart()
{
3200: break
3204: push2.c 0 2232
3216: const.pri 5
3224: push.pri
3228: push.c 2224
3236: sysreq.n 6 4
}
Имитация виртуальной машины
При должном опыте и сноровке можно понять в голове как будет происходить преобразование инструкций, но когда инструкций много (при сложных математических выражения или вложенных вызовах) это становится не так легко, и чтобы не запутаться я прибегаю к методу имитации работы виртуальной машины по исполнению этого кода.
Отслеживаем значения хранимые в основном и вспомогательном регистре, а так же текущий кадр стека. Считаем их изначально пустыми.
C-подобный:
stk // стек
pri // основной регистр
alt // вспомогательный регистр
C-подобный:
// выполняем push2.c 0 2232, при этом новое значение в стек я добавляю слева, напоминая в очередной раз, что стек в памяти перевернут.
// При этом параметры инструкции читаются в обычном порядке, добавляем 0.
stk 0
pri
alt
// добавляем 2232
stk 2232, 0
pri
alt
// const.pri 5
stk 2232, 0
pri 5
alt
// push.pri
stk 5, 2232, 0
pri 5
alt
// push.c 2224
stk 2224, 5, 2232, 0
pri 5
alt
// sysreq.n 6 4
// Здесь делается вызов натива с индексом 6 и 4-мя параметрами. В конце вызова указанное кол-во параметров извлекаются из стека, а результат вызова помещается в основной регистр.
// данное действие запишем псевдокодом.
stk
pri Native6(2224, 5, 2232, 0)
alt
PHP:
public void OnPluginStart()
{
Native6(2224, 5, 2232, 0);
}
Чтобы понять, что есть что, по индексу ищем в .dbg.natives информацию о нативе.
Если кто-то намерено испортил или удалил .dbg.natives, что не будет мешать плагину работать, мы можем из .natives секции узнать только имя функции. В данном случае получим:
C-подобный:
// из .dbg.natives секции
#
index: 6
name: 434
tagid: 6
nargs: 4
##
ident: 4
tagid: 4
dimcount: 1
name: 448
###
tagid: 0
size: 0
##
ident: 1
tagid: 74
dimcount: 0
name: 452
##
ident: 4
tagid: 4
dimcount: 1
name: 461
###
tagid: 0
size: 0
##
ident: 1
tagid: 0
dimcount: 0
name: 473
// из .natives секции
#6 249 // RegConsoleCmd
PHP:
native void RegConsoleCmd(const char[] cmd, ConCmd callback, const char[] description, int flags);
PHP:
native void RegConsoleCmd(char[] cmd, ConCmd callback, char[] description, int flags);
Из прототипа мы видим, что первый и третий параметры являются ссылками (адресами), второй параметр является ссылкой на функцию, а четвертый просто значением. Приступим к обработки аргументов вызова:
Начнем со 2-го аргумента, ссылки на функцию. Эти ссылки указывают на номер public функции внутри smx. .public секция имеет следующее содержание:
C-подобный:
> Publics
address name
#0 2920 56 // .2920.PrintToChatAll
#1 3404 77 // .3404.ShowMenu
#2 3256 92 // Cmd_Menu
#3 3640 101 // Menu_Handler
#4 3192 114 // OnPluginStart
#5 8 128 // __ext_core_SetNTVOptional
C-подобный:
ссылка = pid * 2 + 1;
// К примеру ссылка на OnPlugnStart будет равна
4 * 2 + 1 = 9
C-подобный:
9 / 2 = 4
В нашем вызове второй параметр имеет значение 5, значит это public с индексом 2 - Cmd_Menu.
Теперь разберемся с адресами (ссылками).
Пояснение: правильно называть это передачей аргумента по ссылке (англ. by reference), т.к. мы не можем с ним работать как с просто числом, т.е. адресом.
Первый аргумент, 2224, может обозначать адрес глобальной или статической (static) переменной, или просто адрес в .data секции, откуда берется константное значение.
Ищем в .dbg.symbols переменную с адресом 2224, и ничего не находим. Следовательно это константное значение, которое надо достать из .data секции. Смотрим туда по адресу 2224:
C-подобный:
// Символом '*' будут заменяться NUL символы, т.к. они не могут быть тут отображены.
2208: Message*****%s**sm_menu*****%T**
Проделываем аналогичный поиск со вторым адресом (2232), и не найдя глобальных переменных видимо по этому адресу только NUL символы. Это обозначает, что там передается пустая строка.
Пояснение: Вся память должна быть выровнена по ячейкам (4 байта). Когда в память добавляться строка, к кол-во символов в строке прибавляется один NUL символ как символ окончания строки и потом дополняется NUL символами до кол-во кратного 4. Соответственно при пустой строке будет один NUL символ, дополненный до 4-х байт, что и видно по адресу 2232.
Соединив все наши выяснения получим завершенный код функции:
PHP:
public void OnPluginStart()
{
RegConsoleCmd("sm_menu", Cmd_Menu, "", 0);
}
Теперь перейдем ко 2-ой функции.
Получаем информацию о её прототипе:
C-подобный:
// .publics
#2 3256 92 // Cmd_Menu
// .dbg.symbols
#
address: 16
tagid: 0
codestart: 3256
codeend: 3404
ident: 1
vclass: 1
dimcount: 0
name: 102 // args
#
address: 12
tagid: 0
codestart: 3256
codeend: 3404
ident: 1
vclass: 1
dimcount: 0
name: 107 // client
#
address: 3256
tagid: 12
codestart: 3256
codeend: 3404
ident: 9
vclass: 0
dimcount: 0
name: 152 // Cmd_Menu
PHP:
public Action Cmd_Menu(int client, int args)
Теперь разбиваем байт-код на блоки:
PHP:
public Action Cmd_Menu(int client, int args)
{
3264: break
3268: load.s.pri 12
3276: jzer 3328
3284: push.s 12
3292: sysreq.n 2 1
3304: not
3308: jnz 3328
3316: zero.pri
3320: jump 3336
3328: const.pri 1 // 3276, 3308
3336: jzer 3360 // 3320
3344: break
3348: const.pri 3
3356: retn
3360: break // 3336
3364: push.s 12
3372: push.c 1
3380: call 3404
3388: break
3392: const.pri 3
3400: retn
}
Разберемся в составе условия. В конце условия:
C-подобный:
3316: zero.pri
3320: jump 3336
3328: const.pri 1 // 3276, 3308
3336: jzer 3360 // 3320
Теперь разберем 2 условия:
C-подобный:
3268: load.s.pri 12
3276: jzer 3328
3284: push.s 12
3292: sysreq.n 2 1
3304: not
3308: jnz 3328
PHP:
native bool IsClientInGame(int client); // index: 2
PHP:
public Action Cmd_Menu(int client, int args)
{
if (!client || !IsClientInGame(client))
{
3344: break
3348: const.pri 3
3356: retn
} // 3336
3360: break // 3336
3364: push.s 12
3372: push.c 1
3380: call 3404
3388: break
3392: const.pri 3
3400: retn
}
PHP:
const.pri 3
retn
C-подобный:
stk
pri
alt
// push.s 12
// это .s, значит 12 адрес аругмента
stk client
pri
alt
// push.c 1
stk 1, client
pri
alt
// call 3404
// Вызов функции 3404, кол-во аргументов передано через стек после аргументов. т.е. 1.
stk
pri Call3404(client)
alt
PHP:
public Action Cmd_Menu(int client, int args)
{
if (!client || !IsClientInGame(client))
{
return 3;
} // 3336
Call3404(client);
return 3;
}
PHP:
public Action Cmd_Menu(int client, int args)
{
if (!client || !IsClientInGame(client))
{
return Plugin_Handled;
} // 3336
ShowMenu(client);
return Plugin_Handled;
}
Разберем 3ю функцию.
C-подобный:
// .dbg.symbols секция
#
address: 12
tagid: 0
codestart: 3404
codeend: 3640
ident: 1
vclass: 1
dimcount: 0
name: 119 // client
#
address: 3404
tagid: 6
codestart: 3404
codeend: 3640
ident: 9
vclass: 0
dimcount: 0
name: 238 // ShowMenu
// .publics секция
#1 3404 77 // .3404.ShowMenu
PHP:
void ShowMenu(int client)
PHP:
void ShowMenu(int client)
{
// блок 1
3412: break
3416: stack -4
3424: push.c 28
3432: const.pri 7
3440: push.pri
3444: sysreq.n 7 2
3456: stor.s.pri -4
// блок 2
3464: break
3468: push.adr 12
3476: push2.c 2240 2236
3488: push.s -4
3496: sysreq.n 8 4
// блок 3
3508: break
3512: push3.c 0 2256 2252
3528: push.s -4
3536: sysreq.n 9 4
// блок 4
3548: break
3552: push3.c 0 2268 2264
3568: push.s -4
3576: sysreq.n 9 4
// блок 5
3588: break
3592: push.c 0
3600: push2.s 12 -4
3612: sysreq.n 10 3
3624: stack 4
3632: zero.pri
3636: retn
}
stack -4 говорит нам о объявлении переменной размером 4 байта. Указатель вершины равен указателю кадра в начале работы функции, поэтому адрес этой локальной переменной будет "0 + -4 = -4". Найти описание локальной переменной в .dbg.symbols легче всего по codestart, он равен адресу инструкции stack - 3416.
C-подобный:
#
address: -4
tagid: 96 // Menu
codestart: 3416
codeend: 3632
ident: 1 // Vatible
vclass: 1
dimcount: 0
name: 114 // menu
C-подобный:
// push.c 28
stk 28
pri
alt
// const.pri 7
stk 28
pri 7
alt
// push.pri
stk 7, 28
pri 7
alt
// sysreq.n 7 2
stk
pri Native7(7, 28)
alt
Смотрим информацию о нативе 7.
PHP:
native Menu Menu.Menu(MenuHandler handler, MenuAction actions); // index: 7
Теперь преобразуем первый параметр в индекс паблик функции (7 / 2 = 3) и найдем её имя (Menu_Handler). Второй параметр просто обернём в преобразователь типа. Итог
PHP:
Menu menu = new Menu(Menu_Handler, view_as<MenuAction>(28));
Блок 2:
C-подобный:
// push.adr 12
// В исходном коде визуально не отличается передача значения переменной/аргумента или её адреса, и зависит лишь о прототипа вызываемой функции.
stk client
pri
alt
// push2.c 2240 2236
stk 2236, 2240, client
pri
alt
// push.s -4
stk menu, 2236, 2240, client
pri
alt
// sysreq.n 8 4
stk
pri Native8(menu, 2236, 2240, client)
alt
PHP:
native void Menu.SetTitle(Menu this, char[] fmt, any ...); // index: 8
С первым параметром все понятно, переходим ко второму. Найти глобальную переменную с адресом 2236 мы не смогли, поэтому обращаемся к .data секции по указному адресу.
C-подобный:
2208: Message*****%s**sm_menu*****%T**
2240: MENU_TITLE******Ping********Pong
Т.к. это функция часть methodmap, и должна вызывать относительно объекта, то мы обрезаем первую часть имени и переносим первый параметр на её место, в итоге получая:
PHP:
menu.SetTitle("%T", "MENU_TITLE", client);
Блок 3:
Надеюсь к этому моменту основная мысль стала уже понятно, поэтому тут в сокращенном режиме сначала получаем
PHP:
Native9(menu, 2252, 2256, 0)
PHP:
native bool Menu.AddItem(Menu this, char[] info, char[] display, int style); // index: 9
2256 - переменной нету, из .data получаем "Ping"
Итог:
PHP:
menu.AddItem("", "Ping", 0);
Блок 4:
Он полностью аналогичен блоку 3 и в итоге получим:
PHP:
menu.AddItem("", "Pong", 0);
Блок 5:
PHP:
3588: break
3592: push.c 0
3600: push2.s 12 -4
3612: sysreq.n 10 3
3624: stack 4
3632: zero.pri
3636: retn
PHP:
3592: push.c 0
3600: push2.s 12 -4
3612: sysreq.n 10 3
PHP:
Native10(menu, client, 0)
PHP:
native bool Menu.Display(Menu this, int client, int time); // index: 10
PHP:
menu.Display(client, 0);
Соединив все блоки получим:
PHP:
void ShowMenu(int client)
{
Menu menu = new Menu(Menu_Handler, view_as<MenuAction>(28));
menu.SetTitle("%T", "MENU_TITLE", client);
menu.AddItem("", "Ping", 0);
menu.AddItem("", "Pong", 0);
menu.Display(client, 0);
}
Последняя функция:
C-подобный:
#3 3640 101 // Menu_Handler
#
address: 24
tagid: 0
codestart: 3640
codeend: 3860
ident: 1
vclass: 1
dimcount: 0
name: 126 // param2
#
address: 20
tagid: 0
codestart: 3640
codeend: 3860
ident: 1
vclass: 1
dimcount: 0
name: 133 // param1
#
address: 16
tagid: 92
codestart: 3640
codeend: 3860
ident: 1
vclass: 1
dimcount: 0
name: 140 // action
#
address: 12
tagid: 96
codestart: 3640
codeend: 3860
ident: 1
vclass: 1
dimcount: 0
name: 147 // menu
#
address: 3640
tagid: 0
codestart: 3640
codeend: 3860
ident: 9
vclass: 0
dimcount: 0
name: 172 // Menu_Handler
PHP:
public int Menu_Handler(Menu menu, MenuAction action, int param1, int param2)
{
3640: proc
3644: break
3648: break
3652: load.s.pri 16
3660: switch 3824
3668: break // 3824
3672: load.s.pri 12
3680: push.pri
3684: sysreq.n 11 1
3696: zero.s 12
3704: jump 3852
3712: break // 3824
3716: load.s.pri 24
3724: jnz 3764
3732: break
3736: push2.c 2276 1
3748: call 2920
3756: jump 3788
3764: break // 3724
3768: push2.c 2284 1
3780: call 2920
3788: break // 3756
3792: push.s 20
3800: push.c 1
3808: call 3404
3816: jump 3852
3824: casetbl 2 3852 4 3712 16 3668 // 3660
3852: zero.pri // 3704, 3816, 3824
3856: retn
}
Так же можно заметить, что между casetbl и zero.pri ничего нет, а значит return здесь отсутствует. Преобразим switch в исходный код:
PHP:
public int Menu_Handler(Menu menu, MenuAction action, int param1, int param2)
{
switch (action)
{
case 16:
{
3668: break // 3824
3672: load.s.pri 12
3680: push.pri
3684: sysreq.n 11 1
3696: zero.s 12
}
case 4:
{
3712: break // 3824
3716: load.s.pri 24
3724: jnz 3764
3732: break
3736: push2.c 2276 1
3748: call 2920
3756: jump 3788
3764: break // 3724
3768: push2.c 2284 1
3780: call 2920
3788: break // 3756
3792: push.s 20
3800: push.c 1
3808: call 3404
}
}
C-подобный:
// load.s.pri 12
stk
pri menu
alt
// push.pri
stk menu
pri menu
alt
// sysreq.n 11 1
stk menu
pri Native11(menu)
alt
// zero.s 12
?
PHP:
Native11(menu);
menu = 0;
PHP:
native void CloseHandle(Handle hndl); // index: 11
PHP:
CloseHandle(menu);
menu = null;
PHP:
delete menu;
PHP:
// если delete
break
load.s.pri 12
push.pri
sysreq.n 11 1
zero.s 12
// Если две отдельные операции
break
push.s 12
sysreq.n 11 1
break
zero.s 12
В первом блоке мы сразу же замечаем jnz 3764, и по всем признакам понимает что тут простой if со значением четвертого аргумента. Адрес нас ведет к 3му блоку, а значит в самом if только второй блок.
Изучим второй блок
C-подобный:
3732: break
3736: push2.c 2276 1
3748: call 2920
3756: jump 3788
PHP:
Call2920(2276)
C-подобный:
2272: ****Ping!***Pong!***
PHP:
PrintToChatAll("Ping!");
C-подобный:
3764: break // 3724
3768: push2.c 2284 1
3780: call 2920
PHP:
Call2920(2284) => PrintToChatAll("Pong!")
PHP:
if (!param2)
{
PrintToChatAll("Ping!");
} // 3764
else
{
PrintToChatAll("Pong!");
} // 3788
И последний блок:
C-подобный:
3788: break // 3756
3792: push.s 20
3800: push.c 1
3808: call 3404
PHP:
Call3404(param1) => ShowMenu(param1);
Собираем все вместе
PHP:
public int Menu_Handler(Menu menu, MenuAction action, int param1, int param2)
{
switch (action)
{
case 16:
{
delete menu;
}
case 4:
{
if (!param2)
{
PrintToChatAll("Ping!");
} // 3764
else
{
PrintToChatAll("Pong!");
} // 3788
ShowMenu(param1);
}
}
Теперь соединим все функции в единый плагин:
PHP:
public void OnPluginStart()
{
RegConsoleCmd("sm_menu", Cmd_Menu, "", 0);
}
public Action Cmd_Menu(int client, int args)
{
if (!client || !IsClientInGame(client))
{
return Plugin_Handled;
} // 3336
ShowMenu(client);
return Plugin_Handled;
}
void ShowMenu(int client)
{
Menu menu = new Menu(Menu_Handler, view_as<MenuAction>(28));
menu.SetTitle("%T", "MENU_TITLE", client);
menu.AddItem("", "Ping", 0);
menu.AddItem("", "Pong", 0);
menu.Display(client, 0);
}
public int Menu_Handler(Menu menu, MenuAction action, int param1, int param2)
{
switch (action)
{
case 16:
{
delete menu;
}
case 4:
{
if (!param2)
{
PrintToChatAll("Ping!");
} // 3764
else
{
PrintToChatAll("Pong!");
} // 3788
ShowMenu(param1);
}
}
PHP:
public void OnPluginStart()
{
RegConsoleCmd("sm_menu", Cmd_Menu);
}
public Action Cmd_Menu(int client, int args)
{
if (client == 0 || !IsClientInGame(client))
return Plugin_Handled;
ShowMenu(client);
return Plugin_Handled;
}
void ShowMenu(int client)
{
Menu menu = new Menu(Menu_Handler);
menu.SetTitle("%T", "MENU_TITLE", client);
menu.AddItem("", "Ping");
menu.AddItem("", "Pong");
menu.Display(client, MENU_TIME_FOREVER);
}
public int Menu_Handler(Menu menu, MenuAction action, int param1, int param2)
{
switch (action)
{
case MenuAction_End:
delete menu;
case MenuAction_Select:
{
if (param2 == 0)
{
PrintToChatAll("Ping!");
}
else
{
PrintToChatAll("Pong!");
}
ShowMenu(param1);
}
}
}
Автоматизация процесса декомпиляции
Теперь, когда стало видно как происходит процесс декомпиляции и порядок действий выполняемых для этого, не сложно догадаться, что все это надо автоматизировать и запрограммировать. Большинство действий переносятся в алгоритмы, а потом в функции "как есть", к примеру получение имени натива по его индексу. Но какие-то алгоритмы, легко понятные нам людям, придется преобразовать в более простые для понимания машиной алгоритмы. В итоге весь процесс работы декомпилятора будет разделятся:
1) Предварительный парсинг байт-кода для определения сложных конструкций, таких как for, while циклы, и д.р.
2) Имитация работы виртуальной машины для преобразования кода во вложенные структуры псевдокода.
3) Трансляция псевдокода в исходный код с использованием всей сопутствующей информации сохраненной в smx (.data, .natives, .publics, .pubvars, .tags, .dbg.*).
Конец
Спасибо за прочтение.
Буду рад отзывам и вопросам по данному уроку.
Последнее редактирование: