Как видишь, интерпретатор инструкций можно загрузить в отладчик и трассировать шитый код с его помощью. В нашем случае он встроен в Apache HTTP Server, находящийся в модуле httpd.exe. Нельзя просто так взять и запустить его из отладчика. Более того, к его процессу нельзя даже присоединиться прямым способом — он работает в фоновом режиме и скрыт в списке активных процессов для присоединения x64dbg. Но есть маленькая хитрость: если зайти в параметры x64dbg и в самой дальней, обычно скрытой вкладке «Прочее» выбрать режим «Установить x64dbg оперативным отладчиком (JIT)», то можно отлаживать даже скрытые процессы. Для этого в окне диспетчера задач жмем правой кнопкой мыши на Apache HTTP Server и выбираем «Отладка».
Итак, мы влезли в самое ядро виртуальной машины Zend. Во вкладке «Отладочные символы» в списке загруженных библиотек мы видим модуль php7ts.dll, в котором располагается эта машина, и модуль ixed.7.2ts.win, реализующий ее расширение защиты SourceGuardian. Место внедрения защиты выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
00007FFCBBE35B20 | jne ixed.7.2ts.7FFCBBE35B92 | 00007FFCBBE35B22 | cmp qword ptr ss:[rsp+1C0],0 | 00007FFCBBE35B2B | je ixed.7.2ts.7FFCBBE35B92 | 00007FFCBBE35B2D | call qword ptr ds:[<&zend_get_executed_ | 00007FFCBBE35B33 | mov rcx,qword ptr ss:[rsp+1C0] | 00007FFCBBE35B3B | mov qword ptr ds:[rcx+10],rax | 00007FFCBBE35B3F | call qword ptr ds:[<&tsrm_get_ls_cache> | 00007FFCBBE35B45 | mov ecx,dword ptr ds:[7FFCBBE4C95C] | 00007FFCBBE35B4B | dec ecx | 00007FFCBBE35B4D | movsxd rcx,ecx | 00007FFCBBE35B50 | mov rax,qword ptr ds:[rax] | 00007FFCBBE35B53 | mov rax,qword ptr ds:[rax+rcx*8] | 00007FFCBBE35B57 | mov dword ptr ds:[rax+18],1 | 00007FFCBBE35B5E | mov rdx,qword ptr ss:[rsp+13E0] | 00007FFCBBE35B66 | mov rcx,qword ptr ss:[rsp+1C0] | 00007FFCBBE35B6E | call qword ptr ds:[<&zend_execute>] | 00007FFCBBE35B74 | mov rcx,qword ptr ss:[rsp+1C0] | 00007FFCBBE35B7C | call qword ptr ds:[<&destroy_op_array>] | 00007FFCBBE35B82 | mov rcx,qword ptr ss:[rsp+1C0] | 00007FFCBBE35B8A | call qword ptr ds:[<&_efree@@8>] | 00007FFCBBE35B90 | jmp ixed.7.2ts.7FFCBBE35BA7 | 00007FFCBBE35B92 | mov rax,qword ptr ss:[rsp+13E0] | |
Здесь расшифрованный и распакованный код из строки‑аргумента уже заботливо преобразован в последовательность инструкций и подан на вход интерпретатору, реализованному функцией zend_execute.
Для удобства начнем с того, что составим список команд нашей виртуальной машины. В интернете гуглится много разных вариантов, сильно и не очень отличающихся друг от друга, но нам нужна именно наша, конкретно скомпилированная для нас версия. Для этого мы открываем дизассемблер IDA и при помощи него ищем в библиотеке php7ts.dll функцию zend_get_opcode_name. Функция совсем простая — по опкоду Zend-инструкции возвращает имя. Реализация ее тоже предельно проста, она всего‑навсего берет имя из следующего массива строк (не буду приводить весь список полностью, каждый может легко сгенерировать его самостоятельно):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
.rdata:00000001806F2160 off_1806F2160 dq offset aZendNop ; DATA XREF: zend_get_opcode_name+3^o .rdata:00000001806F2160 ; "ZEND_NOP" .rdata:00000001806F2168 dq offset aZendAdd ; "ZEND_ADD" .rdata:00000001806F2170 dq offset aZendSub ; "ZEND_SUB" .rdata:00000001806F2178 dq offset aZendMul ; "ZEND_MUL" .rdata:00000001806F2180 dq offset aZendDiv ; "ZEND_DIV" .rdata:00000001806F2188 dq offset aZendMod ; "ZEND_MOD" .rdata:00000001806F2190 dq offset aZendSl ; "ZEND_SL" .rdata:00000001806F2198 dq offset aZendSr ; "ZEND_SR" .rdata:00000001806F21A0 dq offset aZendConcat ; "ZEND_CONCAT" .rdata:00000001806F21A8 dq offset aZendBwOr ; "ZEND_BW_OR" .rdata:00000001806F21B0 dq offset aZendBwAnd ; "ZEND_BW_AND" .rdata:00000001806F21B8 dq offset aZendBwXor ; "ZEND_BW_XOR" .rdata:00000001806F21C0 dq offset aZendBwNot ; "ZEND_BW_NOT" .rdata:00000001806F21C8 dq offset aZendBoolNot ; "ZEND_BOOL_NOT" |
Этот список отличается от нагугленных вариантов, зато теперь мы имеем представление, какая из команд исполняется в данный момент времени. Чтобы ты не расслаблялся, добавлю ложку дегтя: некоторые обфускаторы могут подменять исполнительные адреса хэндлеров команд собственными, поэтому при расшифровке команды все время надо держать ухо востро, не полагаясь полностью на опкод. Как минимум нужно следить за тем, чтобы адрес хэндлера попадал в адресное пространство php7ts.dll.
Итак, список команд у нас есть, можно приступить к отладке. Чтобы не возиться с обвязкой интерпретатора, ставим точку останова прямо в основной цикл интерпретатора шитого кода внутри функции execute_ex:
1 2 3 4 5 6 7 8 9 |
00007FFCB6236280 | 48:895C24 08 | mov qword ptr ss:[rsp+8],rbx | execute_ex 00007FFCB6236285 | 57 | push rdi | 00007FFCB6236286 | 48:83EC 20 | sub rsp,20 | 00007FFCB623628A | 6548:8B0425 58000000 | mov rax,qword ptr gs:[58] | 00007FFCB6236293 | 48:8BD9 | mov rbx,rcx | 00007FFCB6236296 | 8B15 A4758200 | mov edx,dword ptr ds:[7FFCB6A5D840] | 00007FFCB623629C | B9 18000000 | mov ecx,18 | 00007FFCB62362A1 | 48:8B3CD0 | mov rdi,qword ptr ds:[rax+rdx*8] | 00007FFCB62362A5 | 8B05 2DAE8200 | mov eax,dword ptr ds:[ |
Как видно из этого фрагмента, в данной реализации интерпретатор устроен предельно просто. На входе в RDI у нас указатель на структуру следующего вида:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
struct _zend_execute_data { // Текущая инструкция const zend_op *opline; // кадр стека текущей функции zend_execute_data *call; // Возвращаемые данные zval *return_value; zend_function *func; zval This; /* this + call_info + num_args */ // Вызываемый кадр стека текущей функции zend_execute_data *prev_execute_data; // таблица символов zend_array *symbol_table; #if ZEND_EX_USE_RUN_TIME_CACHE void **run_time_cache; #endif #if ZEND_EX_USE_LITERALS // постоянный массив констант zval *literals; #endif }; |
В этой структуре нам интересны счетчик команд opline и указатель на массив констант literals. Во время цикла исполнения счетчик команд загружен в регистр rbx, из него в регистр rax извлекается хэндлер текущей инструкции, по которому и делается вызов. При этом в команду в регистре rcx в качестве параметра передается указатель на структуру _zend_execute_data.
Есть еще один полезный малодокументированный момент. В структуре zend_execute_data есть параметр func (в моем случае по относительному смещению 0x18). На самом деле, он указывает на порождающую структуру
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
zend_op_array (структура может меняться в зависимости от версии PHP). struct _zend_op_array { zend_uchar type; zend_uchar arg_flags[3]; uint32_t fn_flags; zend_string *function_name; /* Имя функции */ zend_class_entry *scope; zend_function *prototype; uint32_t num_args; uint32_t required_num_args; zend_arg_info *arg_info; HashTable *attributes; int cache_size; int last_var; /* количество компилируемых переменных */ uint32_t T; /* количество временных переменных */ uint32_t last; /* количество опкодов */ zend_op *opcodes; /* указатель на наш массив zend_op */ ZEND_MAP_PTR_DEF(void **, run_time_cache); ZEND_MAP_PTR_DEF(HashTable *, static_variables_ptr); HashTable *static_variables; zend_string **vars; /* names of CV variables */ uint32_t *refcount; int last_live_range; int last_try_catch; zend_live_range *live_range; zend_try_catch_element *try_catch_array; zend_string *filename; /* Полный путь с именем файла активного скрипта */ uint32_t line_start; /* Номер первой строки */ uint32_t line_end; /* Номер последней строки */ zend_string *doc_comment; int last_literal; /* количество констант */ uint32_t num_dynamic_func_defs; zval *literals; ./* массив констант */ zend_op_array **dynamic_func_defs; void *reserved[ZEND_MAX_RESERVED_RESOURCES]; }; |
Порождающая структураПорождающая структура
Как видишь, структура содержит много полезной информации, к примеру, c ее помощью можно ориентироваться, какой модуль исполняется в текущий момент. Но продолжим процесс изучения нашего веб‑приложения. Рассмотрим фрагмент шитого кода по указателю opline.
Фрагмент шитого кода по указателю oplineФрагмент шитого кода по указателю opline
Для понимания его структуры снова покурим документацию, из которой следует, что это массив скомпилированных инструкций виртуальной машины, каждая из которых описывается следующей конструкцией:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct _zend_op { const void * handler; // указатель на функцию выполнения текущей инструкции znode_op op1; // Операнд 1 znode_op op2; // Операнд 2 znode_op result; // Возвращаемое значение uint32_t extended_value; // расширенный uint32_t lineno; // номер строки zend_uchar opcode; // Тип инструкции zend_uchar op1_type; // тип операнда 1 (этот тип не представляет типы данных, такие как строки и массивы; он указывает, что этот операнд является константой, временной переменной, скомпилированной переменной и т. д.) zend_uchar op2_type; // тип операнда 2 zend_uchar result_type; // тип возвращаемого значения }; |
Каждая запись занимает 0x20 байт, поэтому на рисунке разбиение на инструкции выглядит достаточно наглядно. Зеленым подчеркнут хэндлер инструкции, то есть абсолютный 64-битный адрес исполняемого кода обработчика, красным обведены опкоды, а сбоку приведена их расшифровка исходя из вышеописанной таблицы. Синим отмечены действующие операнды инструкции и их типы:
1 2 3 4 5 6 7 8 9 10 11 |
// константа #define IS_CONST (1<<0) // Временные переменные виртуальной машины, не объявленные в коде PHP. Используются, например, для возврата значений из функции $a = time(). // Данный тип временной переменной отличается от IS_VAR фактически только тем, что операнд данного типа может содержать ссылки #define IS_TMP_VAR (1<<1) // #define IS_VAR (1<<2) // операнд, который либо фактически не используется, либо используется как 32-битное числовое значение (так называемый непосредственный операнд). Например, инструкция перехода будет хранить адрес перехода в операнде UNUSED. #define IS_UNUSED (1<<3) /* Unused */ // Компилируемые переменные, то есть переменные, объявленные в PHP; #define IS_CV (1<<4) /* Compiled variable */ |
Судя по имени модуля и номерам строк, это открытый незашифрованный код, предшествующий sg_load. Попробуем потренироваться на нем в восстановлении логики программы, поскольку у нас есть возможность сравнить его с исходным нескомпилированным текстом скрипта. С именами инструкций мы уже разобрались, попробуем разобраться с параметрами. Первая инструкция ZEND_INIT_FCALL, второй операнд у нее — константа (тип IS_CONST=1) имеет значение 0, суть его — смещение в таблице литералов, на которую указывает literals.
Таблица литераловТаблица литералов
Элементами этой таблицы являются значения типа zval. Это базовый тип виртуальной машины Zend, и для описания его структуры потребуется отдельная статья. По счастью, подобные статьи уже есть на Хабре, и желающие могут их нагуглить. Мы же ограничимся общим описанием структуры применительно к нашему случаю PHP7:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
struct _zval_struct { zend_value value; union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar type, zend_uchar type_flags, zend_uchar const_flags, zend_uchar reserved) } v; uint32_t type_info; } u1; union { uint32_t var_flags; uint32_t next; // hash collision chain uint32_t cache_slot; // literal cache slot uint32_t lineno; // line number (for ast nodes) uint32_t num_args; // arguments number for EX(This) uint32_t fe_pos; // foreach position uint32_t fe_iter_idx; // foreach iterator index } u2; }; |
Выглядит этот код страшновато, но реально структура имеет три поля: собственно value (оно в PHP7 64-битное), 32-битное type_info, представляющее собой тип значения, и третье 32-битное поле широкого применения, которое в данный момент нас не интересует.
Типы значений в нашем случае бывают следующими:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// обычные типы данных #define IS_UNDEF 0 #define IS_NULL 1 #define IS_FALSE 2 #define IS_TRUE 3 #define IS_LONG 4 #define IS_DOUBLE 5 #define IS_STRING 6 #define IS_ARRAY 7 #define IS_OBJECT 8 #define IS_RESOURCE 9 #define IS_REFERENCE 10 // константные выражения #define IS_CONSTANT 11 #define IS_CONSTANT_AST 12 // внутренние типы #define IS_INDIRECT 15 #define IS_PTR 17 |
В нашей таблице литералов содержатся в основном строковые константы (IS_STRING=6) и длинные целочисленные (IS_LONG=4). Искомая константа, на которую указывает наш операнд, — строковая, а значит, 64-битное value интерпретируется как указатель на zend_string. Обещаю, что это последняя внутренняя структура, описываемая в сегодняшней статье. Если ты обратил внимание, она уже упоминалась выше: это базовая структура для хранения строк в данной виртуальной машине — в частности, она служит для хранения полного имени скрипта и исполняемой функции. Итак, ткнемся в отладчике в эту ссылку.
Продолжаем отладкуПродолжаем отладку
Наконец‑то появилась какая‑то наглядность — вместо слепых цифр видны осмысленные строки. Для пущего понимания снова смотрим описание структуры:
1 2 3 4 5 6 |
struct _zend_string { zend_refcounted gc; zend_ulong h; /* hash value */ size_t len; char val[1]; }; |
Структура предельно простая: первые 8 байт ее занимает объект refcounted, его ввели недавно для системного сборщика мусора и для нас он интереса не представляет. Так же, как и следующие 8 байт — 64-битный хеш. А вот дальше идет собственно строка с 64-битным счетчиком: в нашем случае это function_exists.
С константами разобрались, но как быть с переменными? А с переменными дело обстоит весьма сурово. Дело в том, что все типы переменных, как компилированных, так и временных, хранятся вo фрейме стека прямо следом за структурой zend_execute:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
+----------------------------------------+ | zend_execute_data | +----------------------------------------+ | VAR[0] = ARG[1] | arguments | ... | | VAR[num_args-1] = ARG[N] | | VAR[num_args] = CV[num_args] | remaining CVs | ... | | VAR[last_var-1] = CV[last_var-1] | | VAR[last_var] = TMP[0] | TMP/VARs | ... | | VAR[last_var+T-1] = TMP[T] | | ARG[N+1] (extra_args) | extra arguments | ... | +----------------------------------------+ |
Сперва адресуются аргументы функции, затем компилируемые переменные, затем временные. Легко увидеть, что операнды, соответствующие переменным в инструкции, — суть смещения относительно фрейма стека. Беда заключается в том, что в этой области, условно размеченной под 16-байтовые структуры zval, нет указаний, какой именно переменной она принадлежит. Примерное представление об этом можно получить, вспомнив, что в порождающей структуре zend_op_array есть поле vars (отмечено на рисунке), которое представляет собой указатель на массив указателей имен переменных.
Разумеется, это касается только компилированных переменных CV (тип 0x10), ибо у временных переменных типов 2 и 4 никаких имен нет. Соответственно, эмпирический способ получения имени переменной из операнда инструкции выглядит так: берем смещение (в случае приведенной на рисунке инструкции ZEND_ASSIGN первый параметр 0x50), отнимаем от него размер структуры zend_execute_data (в нашем случае тоже 0x50), результат делим на размер структуры zval (0x10). Полученное число используем как индекс в таблице имен vars — перейдя по соответствующей ссылке, получаем имя нулевой компилируемой переменной «__v». Cоответственно, операнд 0x60 следующей команды ZEND_ASSIGN будет переменной с индексом 1 в этой таблице и названием «__x» и так далее.
Итак, присовокупив к инструкциям их параметры, получим следующую последовательность псевдокода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
ZEND_INIT_FCALL "function_exists" ZEND_SEND_VAL "sg_load" ZEND_DO_ICALL ~0 ZEND_BOOL_NOT ~0,!1 ZEND_JMPZ !1,->2C80 ZEND_INIT_FCALL "phpversion" ZEND_DO_ICALL ~2 ZEND_ASSIGN $__v,~2 ZEND_INIT_FCALL "explode" ZEND_SEND_VAL "." ZEND_SEND_VAR $__v ZEND_DO_ICALL ~4 ZEND_ASSIGN $__x,~4 ZEND_FETCH_DIM_R $__x,0,~6 ZEND_CONCAT ~6,".","~7" ... |
Смотрим на то, как это выражение выглядит в исходном скрипте:
1 |
<?php if(!function_exists('sg_load')){$__v=phpversion();$__x=explode('.',$__v);$__v2=-$__x[0].'.'... |
Бинго! Путем титанических усилий мы почти восстановили логику маленькой части уже известной нам строки кода. Однако мы это сделали руками без всяких дамперов, средствами отладчика x64dbg, попутно получив представление о функционировании виртуальной машины PHP прямо внутри нее.
Каковы наши дальнейшие действия? Разумеется, пошагово тащиться по известному нам коду довольно скучно, поэтому мы временно блокируем точку останова на execute_ex и устанавливаем ее непосредственно перед вызовом расшифрованного кода внутри модуля ixed.7.ts2.win. Как только остановка наступит, блокируем этот брекпоинт и снова включаем точку останова внутри виртуальной машины. Теперь мы находимся в самом начале расшифрованного фрагмента и видим все его инструкции, константы, переменные. Можно сдампить это все на диск и написать парсер, можно медленно, но верно тащиться пошагово до проверки нужного нам условия, все зависит от твоей усидчивости и конкретной задачи.
Попробую дать совет по оптимизации дальнейшего процесса. Метод простой и универсальный для всех виртуальных машин — по сути, нас интересуют только узловые моменты ветвления алгоритма. Cтавим точки останова именно в этих местах, то есть на хэндлерах инструкций условных переходов ZEND_JMPZ, ZEND_JMPNZ, ZEND_JMPZNZ, ZEND_JMPZ_EX, ZEND_JMPNZ_EX, ZEND_CASE. Реально останавливаться в этих точках не обязательно, достаточно писать прохождение данной точки в лог. Вдобавок неплохо бы писать в лог названия вызываемых функций, благо инструкций с установкой их имен немного: ZEND_INIT_FCALL_BY_NAME, ZEND_INIT_FCALL, ZEND_INIT_NS_FCALL_BY_NAME, ZEND_INIT_METHOD_CALL, ZEND_INIT_STATIC_METHOD_CALL, ZEND_INIT_USER_CALL, ZEND_INIT_DYNAMIC_CALL.
Полученные треки можно анализировать на предмет мест для патча на лету. Поскольку виртуальная машина не компилирует код, а исполняет его пошагово, патчер можно вешать непосредственно на вход в execute_ex. А возможно, эта статья натолкнет тебя на написание своего собственного универсального дампера или даже прямого анпакера закодированных PHP? Ведь вышеописанный способ годится для распаковки и отладки скриптов, защищенных не только SourceGuardian, но и другими аналогичными системами защиты.
Подобных систем великое множество: среди них — AROHA PHPencoder, BCompiler, ByteRun Protector for PHP, ByteScrambler, CNCrypto, CodeCanyon PHP Encoder, CodeLock, CodeTangler… и это лишь малая часть списка. Причем для большинства из них, например для того же SourceGuardian или небезызвестного ionCube, не существует публичных декодеров — лишь платные онлайн‑сервисы. Посмотрев на прайс подобных сервисов, можно убедиться в том, насколько знания материальны.
Так, а как в итоге-то получить из опкодов нормальный файл с кодом?) Не уже ли самому пилить сопостовитель опкода к php коду? Если да, то как это примерно сделать, чтобы не помереть от вечных тестов и багов от кривого своего кода, потому что не хватает знаний в этом…