[Урок] Основы декомиляции smx

Kailo

Участник
Сообщения
168
Реакции
755
Теперь мы поговорим о том как получить из байт-кода исходный код. Для примера я написал маленький плагин с показательными моментами, который мы и будем разбирать как пример.
Байт-код плагина имеет вид:
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 секции описание функции
Код:
 address: 3192
 tagid: 6
 codestart: 3192
 codeend: 3256
 ident: 9
 vclass: 0
 dimcount: 0
 name: 209
По tagid смотрим секцию .tags и по ссылки на имя типа в секции .names находим, что tagid 6 соответствует void.
ident 9 позволяет нам убедится что мы нашли правильный символ, т.к. 9 - IDENT_FUNCTION.
По name ищем в .dbg.strings название функции, и для 209 в данном случае получаем "OnPluginStart".
В результате имеем:
PHP:
void OnPluginStart()
Теперь надо получить информацию о аргументах функции.
Делаем поиск в .dbg.symbols по codestart равным адресу нашей функции (аргументы создаются в начале вызова и уничтожаются в конце вызова, имея время жизни и видимость на протяжении всей функции.
В данном случае символов с codestart равным 3192 не найдено (кроме символа самой функции), это значит что аргументов эта функция не имеет (о чем вы и так уже догадались по её названию).

Теперь определим квалификаторы функции.
Смотрим в .public секцию в поиске адреса нашей функции и находим
Код:
#num address name
#4 3192 114 // в секции .names по адресу 114 находим OnPluginStart
Теперь важная ремарка связанная с разными версиями компилятора. Ранее квалификатор на уровне smx имели только обозначены public в исходном коде функции. Позднее при компиляции стали все функции приравнивать к public, даже те, что не имеют в объявлении public, для того чтобы иметь возможность найти их с помощью GetFunctionByName и вызвать их через Call_StartFunction, имена функций, которые не имели квалификатора public будут с припиской адреса в начале. К примеру если бы OnPluginStart не имел бы public, то его имя в .names секции было записано как ".3192.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
}
Первые две инструкции proc и break мы уже преобразовали в тело самой фукнции, отбрасываем их.
Далее надо выделить структурные блоки кода. В этом нам очень помогают 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
}
Имитация виртуальной машины
При должном опыте и сноровке можно понять в голове как будет происходить преобразование инструкций, но когда инструкций много (при сложных математических выражения или вложенных вызовах) это становится не так легко, и чтобы не запутаться я прибегаю к методу имитации работы виртуальной машины по исполнению этого кода.
Отслеживаем значения хранимые в основном и вспомогательном регистре, а так же текущий кадр стека. Считаем их изначально пустыми.
Код:
stk // стек
pri // основной регистр
alt // вспомогательный регистр
Начинаем имитировать выполнение инструкций
Код:
// выполняем 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 секции узнать только имя функции. В данном случае получим:
Код:
// из .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);
Или проанализировав .dbg.natives информацию построить его самим:
PHP:
native void RegConsoleCmd(char[] cmd, ConCmd callback, char[] description, int flags);
Здесь же вы сразу же можете заметить, что const квалификаторы используются только на стадии компиляции и информацию о них найти в smx нельзя. Собственно так же, как если бы их убрать до компиляции, результат компиляции получился бы тот же.

Из прототипа мы видим, что первый и третий параметры являются ссылками (адресами), второй параметр является ссылкой на функцию, а четвертый просто значением. Приступим к обработки аргументов вызова:
Начнем со 2-го аргумента, ссылки на функцию. Эти ссылки указывают на номер public функции внутри smx. .public секция имеет следующее содержание:
Код:
> 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
Индекс public фукнции (назовем pid) преобразуется в ссылку по следующему алгоритму:
Код:
ссылка = pid * 2 + 1;
// К примеру ссылка на OnPlugnStart будет равна
4 * 2 + 1 = 9
Таким образом, что бы прийти от ссылки к индексу нам нужна целая часть от деления ссылки на 2.
Код:
9 / 2  = 4
Данная система нужна, чтобы INVALID_FUNCTION имел значение 0. А ссылки будут иметь вид 1, 3, 5, 7, 9 и т.д.
В нашем вызове второй параметр имеет значение 5, значит это public с индексом 2 - Cmd_Menu.

Теперь разберемся с адресами (ссылками).
Пояснение: правильно называть это передачей аргумента по ссылке (англ. by reference), т.к. мы не можем с ним работать как с просто числом, т.е. адресом.
Первый аргумент, 2224, может обозначать адрес глобальной или статической (static) переменной, или просто адрес в .data секции, откуда берется константное значение.
Ищем в .dbg.symbols переменную с адресом 2224, и ничего не находим. Следовательно это константное значение, которое надо достать из .data секции. Смотрим туда по адресу 2224:
Код:
// Символом '*' будут заменяться NUL символы, т.к. они не могут быть тут отображены.
2208: Message*****%s**sm_menu*****%T**
По этой вырезки из .data секции видно, что по адресу 2224, текст "sm_menu".
Проделываем аналогичный поиск со вторым адресом (2232), и не найдя глобальных переменных видимо по этому адресу только NUL символы. Это обозначает, что там передается пустая строка.
Пояснение: Вся память должна быть выровнена по ячейкам (4 байта). Когда в память добавляться строка, к кол-во символов в строке прибавляется один NUL символ как символ окончания строки и потом дополняется NUL символами до кол-во кратного 4. Соответственно при пустой строке будет один NUL символ, дополненный до 4-х байт, что и видно по адресу 2232.

Соединив все наши выяснения получим завершенный код функции:
PHP:
public void OnPluginStart()
{
    RegConsoleCmd("sm_menu", Cmd_Menu, "", 0);
}
Теперь перейдем ко 2-ой функции.
Получаем информацию о её прототипе:
Код:
// .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
}
Если вы видите блок оканчивающийся на инструкцию условного перехода, то это или if, или while или for. Но for и while могут быть опознаны наличием далее по коду безусловного перехода (jump) в обратном чтению кода направлении и своеобразному началу (об этом вы должны были прочитать в прошлом уроке). Резюмируем: первый блок, это if со сложным условием (составным).
Разберемся в составе условия. В конце условия:
Код:
3316: zero.pri
3320: jump 3336
3328: const.pri 1 // 3276, 3308
3336: jzer 3360 // 3320
Предыдущие операторы перехода ведут на const.pri 1, это признак того, что здесь ИЛИ условие. (В случае срабатывания перехода, устанавливается значение 1 и вызывается jzer, который как можно понять не производит перехода и выполняется код идущий после jzer.)
Теперь разберем 2 условия:
Код:
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
И последний блок
Код:
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;
}
Теперь приведем псевдокод к исходному коду. 3 явно должно иметь Action тип, можем сделать или view_as<Action>(3) или используем эквивалентное значение из перечисления Plugin_Handled. И так же найдем имя функции с адресом 3404.
PHP:
public Action Cmd_Menu(int client, int args)
{
    if (!client || !IsClientInGame(client))
    {
        return Plugin_Handled;
    } // 3336
    ShowMenu(client);
    return Plugin_Handled;
}
Разберем 3ю функцию.
Код:
// .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
}
Блок 1:
stack -4 говорит нам о объявлении переменной размером 4 байта. Указатель вершины равен указателю кадра в начале работы функции, поэтому адрес этой локальной переменной будет "0 + -4 = -4". Найти описание локальной переменной в .dbg.symbols легче всего по codestart, он равен адресу инструкции stack - 3416.
Код:
#
 address: -4
 tagid: 96 // Menu
 codestart: 3416
 codeend: 3632
 ident: 1 // Vatible
 vclass: 1
 dimcount: 0
 name: 114 // menu
Т.к. у нас блок объявления переменной, в том же блоке могу быть только инициализации значения и объявления других переменных.
Код:
// 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
Далее мы видим stor.s.pri -4, что дает нам понять, что это инициализация объявленной переменной.
Смотрим информацию о нативе 7.
PHP:
native Menu Menu.Menu(MenuHandler handler, MenuAction actions); // index: 7
По наличию точки в названии функции можно распознать метод methodmap объявления. Т.к. название методмапа и самой функции одинаковы - это конструктор, а т.к. мы знаем, что Menu производный от Handle, должны использовать new.
Теперь преобразуем первый параметр в индекс паблик функции (7 / 2 = 3) и найдем её имя (Menu_Handler). Второй параметр просто обернём в преобразователь типа. Итог
PHP:
Menu menu = new Menu(Menu_Handler, view_as<MenuAction>(28));
Блок 2:
Код:
// 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
Параметры передаваемые в vararg (те что, в параметре с именем ... и далее) передаются только по адресу.
С первым параметром все понятно, переходим ко второму. Найти глобальную переменную с адресом 2236 мы не смогли, поэтому обращаемся к .data секции по указному адресу.
Код:
2208: Message*****%s**sm_menu*****%T**
2240: MENU_TITLE******Ping********Pong
По адресу 2236 мы видим строку "%T", значит два переданных параметра должны быть строкой с обозначением имени фразы перевода и индекс клиента. Как видим 4-ый параметр как раз и есть индекс клиента, а значит 2240 должна быть строкой. Глобальной переменной с таким адресом мы опять не находим, а значит обращаемся опять в .data и получаем "MENU_TITLE".
Т.к. это функция часть 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
2252 - переменной нету, из .data получаем ""
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
Здесь мы видим в конце неявный return, значит у самой функции он отсутствовал. Так же мы видим автоматически генерируемое освобождение стека (stack 4) от созданных ранее локальных переменных. Получается что сам блок будет
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);
}
Последняя функция:
Код:
 #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
}
Начав анализировать байт-код, мы замечаем 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 в исходный код:
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
       }
}
Начнем разбирать код кейсов.
Код:
// 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
?
Как мы видим, мы должны как-то после вызова функции в той же инструкции присвоить значение для параметра 0. Мы вполне можем сами разделить это на два блока:
PHP:
Native11(menu);
menu = 0;
Посмотрев информацию о нативе, кому-то сразу может статья яснее, как же получился такой код.
PHP:
native void CloseHandle(Handle hndl); // index: 11
Значит по итогу получим
PHP:
CloseHandle(menu);
menu = null;
Те кто хорошо читали прошлые уроки и так все знают, те кто догадался молодцы, а для остальных отвечу, то в исходном коде на этом месте была конструкция delete. Т.е. наш код:
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
Один case разобрали, теперь второй.
В первом блоке мы сразу же замечаем jnz 3764, и по всем признакам понимает что тут простой if со значением четвертого аргумента. Адрес нас ведет к 3му блоку, а значит в самом if только второй блок.

Изучим второй блок
Код:
3732: break
3736: push2.c 2276 1
3748: call 2920
3756: jump 3788
PHP:
Call2920(2276)
По адресу 2920 находится функция PrintToChatAll. Мы могли бы её декомпилировать, но в этом нет смысла, т.к. её исходный код есть в инклудах SM. И значит 2276 - строка. Переменной с таким адресом нет, идем в .data.
Код:
2272: ****Ping!***Pong!***
И значит
PHP:
PrintToChatAll("Ping!");
Но думаю у вас уже возник вопрос "Что там делает jump 3788?" Если вы читали уроки, то знаете конечно же, но для остальных: наличие jump в конце if блока обозначает наличие else блока, и судя по адресу 3788 код с 3764 по 3788 будет внутри else.
Код:
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
И последний блок:
Код:
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.*).

Конец
Спасибо за прочтение.
Буду рад отзывам и вопросам по данному уроку.
 
Последнее редактирование:

Crocell

Мошенник
Сообщения
101
Реакции
40
Урок годный, но хотелось бы еще увидеть уроки о защите самих плагинов.
Оффтоп
 

Rabb1t

Хранитель кеша
Команда форума
Сообщения
2,745
Реакции
1,150
Знал, конечно, что декомпиляция не простая наука, но чтобы настолько...
За урок однозначно респект.
 

Kruzya

Социопат
Команда форума
Сообщения
9,148
Реакции
7,424
хотелось бы еще увидеть уроки о защите самих плагинов.
Никак ты их не защитишь, не модифицировав под себя виртуальную машину, и не создав ещё один тип упаковки файла.
У Феникса сейчас именно так и сделано, например.

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

timowka

Участник
Сообщения
15
Реакции
3
Спасибо за урок. А получение enum (списков) возможен? Допустим, в коде используется g_Test[client][Test], где Test - список, который еще и находится в инклюде. Мне нужно восстановить этот список, а то при обычной декомпиляции кода, я получаю лишь индексы, что логично. Такой же вопрос про дефайны и их возможное восстановление.
 

R1KO

fuck society
Команда форума
Сообщения
8,876
Реакции
6,502
который еще и находится в инклюде
не важно где он находится, при компиляции все составные части собираются в один "файл"
Такой же вопрос про дефайны и их возможное восстановление.
Нет. Дефайны - это инструкции компилятора, при компиляции он заменяет их на соотв. им значения.
 

Rabb1t

Хранитель кеша
Команда форума
Сообщения
2,745
Реакции
1,150
@timowka, дали же ответ. Инклюды будут в одном файле, вернее его функции, нативы и прочее, т.е. разделять тебе самому придется по разным файлам. Дефайны так же самому придется, если, конечно, сможешь понять где они должны быть.
 

R1KO

fuck society
Команда форума
Сообщения
8,876
Реакции
6,502
@timowka, по крайней мере обычным lysis вряд ли
 
Сверху