Если haXe оттранслирован в C++, а из него — в машинные коды, это может показаться безнадёжным, тем более, на первый взгляд этот код пестрит вызовами виртуальных методов, которые, не запуская отладчик, трудно соотнести с адресами тел методов.
Но всё не так уж плохо. Даже при отключенной поддержке сценариев (HXCPP_SCRIPTABLE) в файле можно обнаружить строки с названиями методов и полей. Разбираем, как можно размотать этот клубок, сопоставить имена методов с их адресами и смещениями в таблице виртуальных методов.
Немного теории
После трансляции в C++ все классы haXe наследуются от hx.Object (определён в hxcpp/include/hx/Object.h). Особый интерес представляют следующие методы:
virtual Dynamic __Field(const String &inString, hx::PropertyAccess inCallProp);
virtual Dynamic __SetField(const String &inField,const Dynamic &inValue, hx::PropertyAccess inCallProp);
Эти методы переопределяются в классах, оттранслированных в C++, и реализация их везде выглядит примерно так:
Dynamic Matrix_obj::__Field(const ::String &inName,hx::PropertyAccess inCallProp)
{
switch(inName.length) {
case 1:
if (HX_FIELD_EQ(inName,"a") ) { return a; }
if (HX_FIELD_EQ(inName,"b") ) { return b; }
if (HX_FIELD_EQ(inName,"c") ) { return c; }
if (HX_FIELD_EQ(inName,"d") ) { return d; }
break;
case 2:
if (HX_FIELD_EQ(inName,"tx") ) { return tx; }
if (HX_FIELD_EQ(inName,"ty") ) { return ty; }
break;
case 5:
if (HX_FIELD_EQ(inName,"clone") ) { return clone_dyn(); }
if (HX_FIELD_EQ(inName,"scale") ) { return scale_dyn(); }
if (HX_FIELD_EQ(inName,"setTo") ) { return setTo_dyn(); }
break;
case 6:
if (HX_FIELD_EQ(inName,"concat") ) { return concat_dyn(); }
if (HX_FIELD_EQ(inName,"equals") ) { return equals_dyn(); }
if (HX_FIELD_EQ(inName,"invert") ) { return invert_dyn(); }
if (HX_FIELD_EQ(inName,"rotate") ) { return rotate_dyn(); }
break;
case 7:
if (HX_FIELD_EQ(inName,"__array") ) { return __array; }
if (HX_FIELD_EQ(inName,"toArray") ) { return toArray_dyn(); }
break;
case 8:
if (HX_FIELD_EQ(inName,"copyFrom") ) { return copyFrom_dyn(); }
if (HX_FIELD_EQ(inName,"identity") ) { return identity_dyn(); }
if (HX_FIELD_EQ(inName,"toString") ) { return toString_dyn(); }
break;
case 9:
if (HX_FIELD_EQ(inName,"copyRowTo") ) { return copyRowTo_dyn(); }
if (HX_FIELD_EQ(inName,"createBox") ) { return createBox_dyn(); }
if (HX_FIELD_EQ(inName,"translate") ) { return translate_dyn(); }
break;
case 10:
if (HX_FIELD_EQ(inName,"to3DString") ) { return to3DString_dyn(); }
break;
case 11:
if (HX_FIELD_EQ(inName,"copyRowFrom") ) { return copyRowFrom_dyn(); }
if (HX_FIELD_EQ(inName,"setRotation") ) { return setRotation_dyn(); }
if (HX_FIELD_EQ(inName,"toMozString") ) { return toMozString_dyn(); }
if (HX_FIELD_EQ(inName,"__toMatrix3") ) { return __toMatrix3_dyn(); }
break;
case 12:
if (HX_FIELD_EQ(inName,"copyColumnTo") ) { return copyColumnTo_dyn(); }
if (HX_FIELD_EQ(inName,"__transformX") ) { return __transformX_dyn(); }
if (HX_FIELD_EQ(inName,"__transformY") ) { return __transformY_dyn(); }
break;
case 13:
if (HX_FIELD_EQ(inName,"__cleanValues") ) { return __cleanValues_dyn(); }
break;
case 14:
if (HX_FIELD_EQ(inName,"copyColumnFrom") ) { return copyColumnFrom_dyn(); }
if (HX_FIELD_EQ(inName,"transformPoint") ) { return transformPoint_dyn(); }
break;
case 16:
if (HX_FIELD_EQ(inName,"__transformPoint") ) { return __transformPoint_dyn(); }
break;
case 17:
if (HX_FIELD_EQ(inName,"createGradientBox") ) { return createGradientBox_dyn(); }
break;
case 19:
if (HX_FIELD_EQ(inName,"deltaTransformPoint") ) { return deltaTransformPoint_dyn(); }
if (HX_FIELD_EQ(inName,"__transformInverseX") ) { return __transformInverseX_dyn(); }
if (HX_FIELD_EQ(inName,"__transformInverseY") ) { return __transformInverseY_dyn(); }
break;
case 22:
if (HX_FIELD_EQ(inName,"__translateTransformed") ) { return __translateTransformed_dyn(); }
break;
case 23:
if (HX_FIELD_EQ(inName,"__transformInversePoint") ) { return __transformInversePoint_dyn(); }
}
return super::__Field(inName,inCallProp);
}
Как можно видеть, полями в понимании транслятора хаКс оказывается не только то, что обычно считается полями, но и методы тоже, правда, в динамических обёртках, из которых их ещё предстоит вытащить.
Подготовка
Соответственно, начинать стоит с того, чтобы найти метод __Field. Например, в него можно попасть по обратной ссылке на строку с именем метода. Если почитать, какие в файле есть строки, то по обратным ссылкам можно попасть, например, в __ToString или RTTI. Из них по обратной ссылке надо перейти в VMT. Если строка является именем поля, то вместо __Field можно попасть в похожий метод __SetField, который хуже подходит, так как там нет ссылок на динамические обёртки для методов. Находясь в VMT, открываем переопределённые методы (выделяются по адресам) и ищем, какие из них похожи на __Field (видно большой switch в начале):
.text:010B3DB8 var_30 = -0x30
.text:010B3DB8 var_2C = -0x2C
.text:010B3DB8 var_28 = -0x28
.text:010B3DB8 var_20 = -0x20
.text:010B3DB8
.text:010B3DB8 PUSH.W {R4-R9,LR}
.text:010B3DBC SUB SP, SP, #0x14
.text:010B3DBE MOV R7, R2
.text:010B3DC0 MOV R4, R0
.text:010B3DC2 LDR R0, [R7]
.text:010B3DC4 MOV R9, R3
.text:010B3DC6 MOV R5, R1
.text:010B3DC8 SUBS R0, #4 ; switch 28 cases
.text:010B3DCA CMP R0, #0x1B
.text:010B3DCC BHI.W def_10B3DD0 ; jumptable 010B3DD0 default case
.text:010B3DD0 TBH.W [PC,R0,LSL#1] ; switch jump
.text:010B3DD0 ; ---------------------------------------------------------------------------
.text:010B3DD4 jpt_10B3DD0 DCW 0x1C ; jump table for switch statement
.text:010B3DD6 DCW 0x35
.text:010B48DC var_38 = -0x38
.text:010B48DC var_30 = -0x30
.text:010B48DC var_28 = -0x28
.text:010B48DC var_24 = -0x24
.text:010B48DC var_20 = -0x20
.text:010B48DC arg_0 = 0
.text:010B48DC
.text:010B48DC PUSH.W {R4-R9,LR}
.text:010B48E0 SUB SP, SP, #0x1C
.text:010B48E2 MOV R7, R2
.text:010B48E4 MOV R8, R0
.text:010B48E6 LDR R0, [R7]
.text:010B48E8 MOV R6, R3
.text:010B48EA LDR R5, [SP,#0x38+arg_0]
.text:010B48EC MOV R9, R1
.text:010B48EE SUBS R0, #6 ; switch 13 cases
.text:010B48F0 CMP R0, #0xC
.text:010B48F2 BHI.W def_10B48F6 ; jumptable 010B48F6 default case
.text:010B48F6 TBH.W [PC,R0,LSL#1] ; switch jump
.text:010B48F6 ; ---------------------------------------------------------------------------
.text:010B48FA jpt_10B48F6 DCW 0xD ; DATA XREF: .text:01329970↓o
.text:010B48FA ; jump table for switch statement
.text:010B48FC DCW 0x25
__Field в таблице виртуальных методов идёт раньше, чем __SetField, и вариантов там обычно меньше. В данном примере 13 против 28.
Первый этап: ищем динамические обёртки
Когда найдены оба метода, нужно зайти в __Field, смотреть, куда идёт ветвление после 0 == memcmp и давать имена обёрткам. Попадаться при этом могут как обычные поля, так и обёртки. Научиться отличать их несложно, вот пример сначала обычного поля, потом динамической обёртки для метода:
.text:010B44B0 loc_10B44B0 ; CODE XREF: __Field+16A↑j
.text:010B44B0 LDR R0, [R5,#0x20]
.text:010B44B2 B loc_10B4582
.text:010B44B4 ; ---------------------------------------------------------------------------
.text:010B44B4
.text:010B44B4 loc_10B44B4 ; CODE XREF: __Field+1B0↑j
.text:010B44B4 LDR R2, =(get_error_dyn+1 - 0x10B44BA)
.text:010B44B6 ADD R2, PC ; get_error_dyn
.text:010B44B8 B loc_10B44D2
Встречалась, но не в этом файле, такая проблема, что указатели на обёртки не распознаются. Выглядит это как аномально большой целочисленный операнд оранжевого цвета. Через Ctrl+R его надо в IDA сделать смещением.
Второй этап: самые простые случаи
Для начала посмотрим, как вообще после трансляции в C++ расположены методы и обёртки для них:
// …
::lime::math::Matrix3 Matrix_obj::__toMatrix3( ){
HX_STACK_FRAME("openfl.geom.Matrix","__toMatrix3",0xaf6ed17e,"openfl.geom.Matrix.__toMatrix3","openfl/geom/Matrix.hx",480,0xa0d54189)
HX_STACK_THIS(this)
HX_STACK_LINE(482)
Float tmp = this->a; HX_STACK_VAR(tmp,"tmp");
HX_STACK_LINE(482)
Float tmp1 = this->b; HX_STACK_VAR(tmp1,"tmp1");
HX_STACK_LINE(482)
Float tmp2 = this->c; HX_STACK_VAR(tmp2,"tmp2");
HX_STACK_LINE(482)
Float tmp3 = this->d; HX_STACK_VAR(tmp3,"tmp3");
HX_STACK_LINE(482)
Float tmp4 = this->tx; HX_STACK_VAR(tmp4,"tmp4");
HX_STACK_LINE(482)
Float tmp5 = this->ty; HX_STACK_VAR(tmp5,"tmp5");
HX_STACK_LINE(482)
::lime::math::Matrix3 tmp6 = ::lime::math::Matrix3_obj::__new(tmp,tmp1,tmp2,tmp3,tmp4,tmp5); HX_STACK_VAR(tmp6,"tmp6");
HX_STACK_LINE(482)
return tmp6;
}
HX_DEFINE_DYNAMIC_FUNC0(Matrix_obj,__toMatrix3,return )
Void Matrix_obj::__transformInversePoint( ::openfl::geom::Point point){
{
HX_STACK_FRAME("openfl.geom.Matrix","__transformInversePoint",0xde42fb73,"openfl.geom.Matrix.__transformInversePoint","openfl/geom/Matrix.hx",487,0xa0d54189)
// …
Видно, что идёт сначала тело метода, потом макросом конструируется динамическая обёртка, потом следующий метод, потом динамическая обёртка для него и так далее. Поскольку обёрткам имена на первом этапе дали, а собственно методам — ещё нет, в списке подпрограмм IDA должна получиться «полосатая» картина, когда именованные подпрограммы перемежаются с именованными.
Это не вполне так, но на этом этапе нужно обработать только самые очевидные случаи — когда между динамическими обёртками ровно одна подпрограмма, и, скорее всего, это и есть метод. Ему даётся имя по обёртке, которая ниже него.
Осторожно: попадались случаи, но не в этом файле, когда IDA не распознавала тело метода как подпрограмму, зато распознавала что-то вспомогательное, идущее после метода. На настоящий метод идёт обратная ссылка из VMT.
Третий этап: когда между обёртками две подпрограммы
Динамические обёртки создаются макросом, который выглядит так:
#define HX_DEFINE_DYNAMIC_FUNC0(class,func,ret)
::Dynamic __##class##func(hx::Object *inObj)
{
ret reinterpret_cast<class *>(inObj)->func(); return ::Dynamic();
};
::Dynamic class::func##_dyn()
{
return hx::CreateMemberFunction0(this,__##class##func);
}
Как можно видеть, обёрток здесь создаётся сразу две, типизированная и нетипизированная, но типизированная обычно выбрасывается транслятором C++ за ненадобностью. Если между динамическими обёртками сразу две безымянных подпрограммы, то, скорее всего, первая из них — это нужный метод, а вторая — типизированная обёртка.
К началу третьего этапа большая часть методов уже должна быть проименована, так что если смотреть из VMT, то это будут одиночные пробелы, и на данном этапе они устраняются.
Четвёртый этап: закрываем большие пробелы в VMT
Бывает, остаются большие пробелы в VMT, по два и более метода. В очередной раз можно отметить удобство взгляда из VMT. Так, если при обходе __Field упустить один метод, он будет выглядеть в списке подпрограмм IDA как три безымянные подпрограммы между динамическими обёртками, но хаКс может генерировать дополнительные подпрограммы и для других нужд, и тогда тоже может получаться три безымянные подпрограммы между динамическими обёртками.
Из VMT же видно: если пробел из двух элементов, значит, это упущенная динамическая обёртка в __Field. Находим в списке подпрограмм, где этот пробел, переходим к средней подпрограмме, это должна быть обёртка. С помощью X открываем список обратных ссылок, среди них должен быть __Field. Переходим туда, выясняем имя обёртки, пробел в списке подпрограмм «затягивается» полосой, и дальше по описанному алгоритму проставляем имена методов.
Методы hx.Object
Для полноты можно открыть hxcpp/include/hx/Object.h, выписать оттуда все виртуальные методы по порядку, и так идентифицировать методы в начале VMT.
Определение типов данных полей и аргументов
Когда у полей и аргументов вызываются методы (как и все, виртуальные), надо понять, в какой VMT их искать, а для этого надо понять, каких вообще они типов. Если не запускать отладчик, помогают это сделать динамические обёртки. На вход они получают аргументы формальных типов (Dynamic, Dynamic, Dynamic, … ) и, чтобы сделать вызов, они сначала приводят Dynamic к реальному ожидаемому методом типу. Во время этого преобразования как раз можно и узнать эти самые типы.
Например, если в теле обёртки видим:
.text:010B3884 LDR R1, =(off_23DE1D4 - 0x10B388E)
.text:010B3886 MOVS R3, #0
.text:010B3888 LDR R2, =(off_23E04A0 - 0x10B3890)
.text:010B388A ADD R1, PC ; off_23DE1D4
.text:010B388C ADD R2, PC ; off_23E04A0
.text:010B388E LDR R1, [R1] ; hx_Object_ci
.text:010B3890 LDR R2, [R2] ; off_22D9DE0
.text:010B3892 BLX.W __dynamic_cast
…то видно, что делается приведение из hx.Object во что-то другое. Если hx_Obejct_ci у вас ещё не идентифицирован, то оба класса будут неизвестные, но это решаемо. Смотрим, в чей RTTI ведут указатели (в данном примере off_22D9DE0), проставляем имена, делаем выводы.
Аналогично для полей пригождается __SetField, который вынужден приводить тип от Dynamic к реальному типу поля, тем самым давая подсказку.
Статические поля и методы
Если у класса есть статические элементы, у него переопределяются статические методы __GetStatic и/или __SetStatic. В VMT их по понятным причинам не видно, но если в классе есть одновременно и статические, и обычные элементы, то в транслированном коде идут по порядку __Field, __GetStatic, __SetField, __SetStatic, так что, зная, где __Field и __SetField, можно вычислить __GetStatic и __SetStatic рядом с ними. Там так же в начале switch по длине строки, и затем операции сравнения.
Скринкаст
00:00 Находим __Field и __SetField
03:00 Первый этап: ищем динамические обёртки
21:30 Второй этап: самые простые случаи
30:48 Третий этап: когда между обёртками две подпрограммы
33:15 Четвёртый этап: закрываем большие пробелы в VMT
49:00 Методы hx.Object
Автор: Иван Левашев