Проблема восстановления исходного кода из скомпилированных бинарников возникает сравнительно часто и протекает остро, с воем и рыданиями. Здесь нам, до некоторой степени, помогут замечательные
программы-декомпиляторы, и в этом посте автор собрал свои скромные попытки выдрать исходники (или хотя бы намёки на них) из скомпилированных из
С-шного кода бинарников.
Задача для декомпилятора бинарников, собранных из С кода:
Классический случай: один деятель на факультете написал на правильном ANSI C (и используя библиотеки BLAS и LAPACK) нужные и хорошие алгоритмы, и скомпилировал их в виде MEX-файлов для использования (это С-шный код, который можно вызывать из МАТЛАБ).
Но потом он повздорил с народом, разозлился и свалил в частную контору, унеся все исходники с собой. Документации нет. Копий исходников нет. Есть обрывки личной переписки и намёки в сопровождающих файлах на тип алгоритма. Вариант физического воздействия на автора тупыми тяжёлыми предметами не рассматривается.
Нужно восстановить исходники если и не до компилируемого состояния, то во всяком случае выудить оттуда алгоритмы и ключевые методы, использованные при реализации.
Кратко: суть и сложность проблемы
Декомпилятор (Decompiler) пытается перевести скомпилированный бинарный файл обратно в некое подобие исходного кода. Качество выхлопа зависит от особенностей языка исходника:
- Для C# или Java есть много декомпиляторов - байткод на java содержит много информации. Это помогает восстанавливать декомпилятору исходник до состояния, пригодного к повторной компиляции.
- Совершенно другая история с двоичными файлами, в которых, как правило, отладочной информации нет. Тем не менее, динамически связанные библиотеки функций, как правило, вызываются по имени. Часто, типы параметров библиотечных функций известны, и это может помочь до известных пределов.
Декомпиляторы пытаются восстановить информацию, которая частично утрачена при компиляции в бинарный файл - в этом и заключается основная сложность.
Если вы думаете, что с помощью декомпилятора вы получите обратно красивый код на С -
вы будете сильно разочарованы. В лучшем случае на выходе будет общая (и довольно грубая) структура программы, в худшем - только имена функций и
немного мусора.
Так что ответ на вопрос заголовка поста: "Обхватить голову руками и закричать #$@@@@@!" :-)
Программы для декомпилирования (decompilers) для C/C++
Декомпиляторов для C/C++ немного, и ниже список из наиболее работоспособных. Здесь нет разделения на опенсорс или Linux-only - для такого дела, как вскрытие исходников, можно (и нужно) поступиться своими светлыми идеалами и наступить на горло собственной песне.
Сразу замечу: скорее всего, ни один декомпилятор не выдаст вам сразу компилируемый код. Придётся потратить порядком времени и сил, чтобы это месиво превратить в код, который можно читать (желательно, не только компилятору).
Boomerang
Boomerang это C decompiler с открытыми исходами:
- поддерживаемые бинарные форматы: ELF PE-COFF Mac-OS
- платформы: Windows/Linux
- поддерживаемые архитектуры: IA32 MIPS PPC
- метод работы: поточный, есть жалкий графический интерфейс, лучше использовать CLI.
Весьма продвинутый набор алгоритмов анализа кода, что не удивительно - один из соавтором защитил на этом докторскую диссертацию [Michael James Van Emmerik, Static Single Assignment for Decompilation, Ph.D. thesis, the University of Queensland, Australia.
PDF,
mirror PDF].
Качество кода, выдаваемого декомпилятором:
- Структурирование: очень хорошее
- Переменные: хорошее
- Типы данных: очень хорошее
Выдаваемое качество кода сильно варьируется: некоторые функции почти идеально восстановлены, хорошо видна структура кода и есть указание типа переменных. В других случаях функции сильно запутаны и их почти невозможно прочитать.
Программа всё ещё в состоянии бета-версии и для больших проектов не подходит. Скачать можно
здесь.
RecStudio
Интерактивный декомпилятор
RecStudio для С и (отчасти) С++, закрытая разработка:
- поддерживаемые бинарные форматы: ELF PE-COFF AOUT RAW PS-X
- платформы: Windows/Linux/MacOS
- поддерживаемые архитектуры: x86 (ia32) x86_64 Mips PowerPC mc68k
- метод работы: поточный и интерактивный, есть графический интерфейс.
Использует продвинутый набор алгоритмов анализа (partial Single Static Assignment, SSA), хотя до сих пор (и это после 20 лет!) в стадии разработки. Качество кода, выдаваемого декомпилятором:
- Структурирование: хорошее
- Переменные: частично
- Типы данных: частично или никак
Выдаваемое качетсво кода, как правило, хуже, чем у Boomerang, хотя обновлённый
RecStudio более подробен.
Программа работает вполне стабильно, есть сборки под Linux. Скачать можно
здесь.
dcc - DOS to C decompiler
Поточный декомпилятор
Dcc, только ANSI C и для exe-файлов, с открытым исходным кодом под GPL:
- поддерживаемые бинарные форматы: EXE/COM
- платформы: Windows
- поддерживаемые архитектуры: x86
- метод работы: поточный.
Один из первых декомпиляторов вообще, и только под DOS. Сильная сторона - структурирование кода. Качество кода, выдаваемого декомпилятором:
- Структурирование: хорошее
- Переменные: частично
- Типы данных: частично или никак
Разработка Cristina Cifuentes, которая защитила PhD в
Queensland University of Technology на этом деле [можно полистать, если что - C. Cifuentes,
Reverse Compilation Techniques].
Скачать можно
отсюда (на сайте
университета).
Hex Rays - plugin для IDA Pro
На самом деле
Hex Rays не является отдельной программой - это плагин-декомпилятор для IDA Pro. Комбинация продвинутых возможностей IDA Pro (это дизассемблер) и Hex Rays в качестве декомпилятора очень впечатляет, как и аэрокосмическая цена.
По причине закрытости продукта (нет даже демо-версии) и нереальной цены в этом разделе про Hex Rays больше ничего написано не будет.
Ходовые испытания в реальных условиях
Для начала попробуем декомпилировать что-нибудь совсем простенькое и написанное на ANSI C и с использованем библиотеки BLAS для векторых и матричных операций. Бинарный файл можно скачать
здесь.
1. Простенький C-шный бинарник + BLAS
Собственно, код на C для перемножения матрицы и вектора (используется CBLAS). Исходник:
#include <stdio.h>
#include <cblas.h>
double m[] = {
3, 1, 3,
1, 5, 9,
2, 6, 5
};
double x[] = {
-1, -1, 1
};
double y[] = {
0, 0, 0
};
int main(void)
{
int i, j;
for (i=0; i<3; ++i) {
for (j=0; j<3; ++j) printf("%5.1f", m[i*3+j]);
putchar('\n');
}
cblas_dgemv(CblasRowMajor, CblasNoTrans, 3, 3, 1.0, m, 3, x, 1, 0.0, y, 1);
for (i=0; i<3; ++i) printf("%5.1f\n", y[i]);
return 0;
}
После перемножения выдаст результат на консоль.
Выхлоп Boomerang
У него много ключей и параметров, часть которых не знает даже официальная, скажем так, документация. Тем не менее, для ключа
-Td (Use data-flow-based type analysis) мы имеем выхлоп в стиле дзен:
double y;
double m = 3.;
// address: 0x80484e4
int main(int argc, char **argv, char **envp) {
void *local23; // r28
for(;;) {
proc1();
}
}
Скажем так, не слишком ободряюще. Ключ -Tc (Use old constraint-based type analysis) выдаёт больше информации к размышлению:
char y[24];
long long m[9] = { 0x4008000000000000LL, 0x3ff0000000000000LL, 0x4008000000000000LL, 0x3ff0000000000000LL, 0x4014000000000000LL, 0x4022000000000000LL, 0x4000000000000000LL, 0x4018000000000000LL, 0x4014000000000000LL };
// address: 0x80484e4
int main(int argc, char **argv, char **envp) {
int local10; // r24
int local13; // r28
int local14; // r29
int local15; // r32
int local2; // m[r28 + 72]{60}
int local3; // r28{181}
int local6; // m[r28 + 72]{119}
int local7; // m[r28 + 72]{149}
int local8; // m[r28 + 76]{9}
int local9; // m[r28 + 76]{49}
*(int*)(local13 - 4) = local14;
*(int*)(local13 - 12) = 0;
local3 = local13 - 84;
if (*(int*)(local3 + 72) > 2) {
*(int*)(local3 + 52) = 1;
*(int*)(local3 + 48) = 0x8049868;
*(long long*)(local3 + 40) = 0.;
*(int*)(local3 + 36) = 1;
*(int*)(local3 + 32) = 0x8049848;
*(int*)(local3 + 28) = 3;
*(int*)(local3 + 24) = 0x8049800;
*(long long*)(local3 + 16) = 1.;
*(int*)(local3 + 12) = 3;
*(int*)(local3 + 8) = 3;
*(int*)(local3 + 4) = 111;
*(int*)local3 = 101;
cblas_dgemv();
local6 = 0;
for(;;) {
*(long long*)(local3 + 4) = y[0];
*(int*)local3 = 0x80486b6;
proc1();
local7 = *(int*)(local3 + 72) + 1;
}
}
local8 = 0;
for(;;) {
local10 = *(int*)(local3 + 72) + *(int*)(local3 + 72) + *(int*)(local3 + 72);
local15 = m[local10];
*(long long*)(local3 + 4) = local15;
*(int*)local3 = 0x80486b0;
proc1();
local9 = *(int*)(local3 + 76) + 1;
}
}
Подробностей тут больше, и тут выловлен самый главный ключик -
cblas_dgemv();
Выхлоп RecStudio
Намного более обилен и представляет собой следующий поток сознания:
// Generated by Rec Studio 4 - build Oct 20 2012
_init()
{// addr = 0x08048398
_unknown_ __ebx; // r1
_unknown_ __ebp; // r6
_unknown_ _t2; // _t2
__esp = __esp - 4;
L1();
_pop(__ebx);
if( *((intOrPtr*)(_t2 + 0x1414)) != 0) {
__gmon_start__();
}
frame_dummy();
__do_global_ctors_aux();
_pop(__eax);
return;
}
L080483A4()
{
_unknown_ _t2; // _t2
_pop(__ebx);
if( *((intOrPtr*)(_t2 + 0x1414)) != 0) {
__gmon_start__();
}
frame_dummy();
__do_global_ctors_aux();
_pop(__eax);
_pop(__ebx);
__esp = __ebp;
_pop(__ebp);
return;
}
__gmon_start__()
{// addr = 0x080483D8
goto __imp____gmon_start__;
}
putchar()
{// addr = 0x080483E8
goto __imp__putchar;
}
__libc_start_main()
{// addr = 0x080483F8
goto __imp____libc_start_main;
}
cblas_dgemv()
{// addr = 0x08048408
goto __imp__cblas_dgemv;
}
printf()
{// addr = 0x08048418
goto __imp__printf;
}
_start(
signed int __eax, // r0
_unknown_ __edx // r3
)
{// addr = 0x08048430
_unknown_ __ebx; // r1
signed int _t5; // _t5
_unknown_ _t6; // _t6
_unknown_ _t10; // _t10
__edx = __edx;
_t4 = __eax;
_pop(__esi);
__ecx = __esp;
__esp = __esp & 240;
_push(__eax);
_push(__esp);
_push(__edx);
_push(__libc_csu_fini);
_push(__libc_csu_init);
_push(__ecx);
_push(_t10);
_push(main);
__libc_start_main();
asm("hlt ");
0;
0;
_push(0);
_push(_t6);
__esp = __esp - 4;
if(completed.5982 != 0) {
} else {
_t4 = dtor_idx.5984;
_t6 = ( &__DTOR_END__ - &__DTOR_LIST__ >> 2) - 1;
if(_t4 >= _t6) {
} else {
do {
_t5 = _t4 + 1;
dtor_idx.5984 = _t5;
*((intOrPtr*)(_t5 * 4 + &__DTOR_LIST__))();
_t4 = dtor_idx.5984;
} while(_t4 < _t6);
}
completed.5982 = 1;
}
__esp = __esp + 4;
_pop(__ebx);
_pop(__ebp);
return;
}
__do_global_dtors_aux(
_unknown_ __esi // r5
)
{// addr = 0x08048460
_unknown_ __ebx; // r1
_unknown_ __ebp; // r6
_unknown_ _t4; // _t4
signed int _t5; // _t5
signed int _t6; // _t6
_unknown_ _t10; // _t10
if(completed.5982 == 0) {
_t5 = dtor_idx.5984;
_t10 = ( &__DTOR_END__ - &__DTOR_LIST__ >> 2) - 1;
if(_t5 >= _t10) {
L4:
completed.5982 = 1;
return;
}
do {
_t6 = _t5 + 1;
dtor_idx.5984 = _t6;
*((intOrPtr*)(_t6 * 4 + &__DTOR_LIST__))();
_t5 = dtor_idx.5984;
} while(_t5 < _t10);
goto L4;
}
return;
}
frame_dummy()
{// addr = 0x080484C0
_unknown_ __ebp; // r6
__eax = __JCR_LIST__;
if(__JCR_LIST__ == 0) {
} else {
__eax = 0;
if(__eax != 0) {
*__esp = &__JCR_LIST__;
*__eax();
return;
}
}
return;
}
main(
_unknown_ __fp0 // r28
)
{// addr = 0x080484E4
signed int _v8; // _cfa_fffffff8
signed int _v12; // _cfa_fffffff4
intOrPtr _v32; // _cfa_ffffffe0
char* _v36; // _cfa_ffffffdc
intOrPtr _v48; // _cfa_ffffffd0
char* _v52; // _cfa_ffffffcc
intOrPtr _v56; // _cfa_ffffffc8
char* _v60; // _cfa_ffffffc4
intOrPtr _v72; // _cfa_ffffffb8
intOrPtr _v76; // _cfa_ffffffb4
intOrPtr _v80; // _cfa_ffffffb0
_unknown_ __ebp; // r6
__fp0 = __fp0;
__esp = __esp & 240;
__esp = __esp - 80;
_v12 = 0;
while(_v12 <= 2) {
_v8 = 0;
while(_v8 <= 2) {
__fp0 ?_? *((long long*)((_v12 + __edx + __edx + _v8) * 8 + &m));
asm("fstp qword [esp+0x4]");
*__esp = 134514352;
printf();
_v8 = _v8 + 1;
}
*__esp = 10;
putchar();
_v12 = _v12 + 1;
}
_v32 = 1;
_v36 = &y;
asm("fldz ");
asm("fstp qword [esp+0x28]");
_v48 = 1;
_v52 = &x;
_v56 = 3;
_v60 = &m;
asm("fld1 ");
asm("fstp qword [esp+0x10]");
_v72 = 3;
_v76 = 3;
_v80 = 111;
*__esp = 101;
cblas_dgemv();
_v12 = 0;
while(_v12 <= 2) {
__fp0 ?_? *((long long*)(_v12 * 8 + &y));
asm("fstp qword [esp+0x4]");
*__esp = "%5.1f\n";
printf();
_v12 = _v12 + 1;
}
return 0;
}
__libc_csu_fini()
{// addr = 0x080485F0
_unknown_ __ebp; // r6
return;
}
__libc_csu_init(
intOrPtr _a4, // _cfa_4
intOrPtr _a8, // _cfa_8
intOrPtr _a12 // _cfa_c
)
{// addr = 0x08048600
intOrPtr _v36; // _cfa_ffffffdc
intOrPtr _v40; // _cfa_ffffffd8
_unknown_ __ebx; // r1
_unknown_ __edi; // r4
signed int __esi; // r5
_unknown_ __ebp; // r6
_unknown_ _t14; // _t14
_unknown_ _t15; // _t15
signed int _t18; // _t18
__i686.get_pc_thunk.bx();
_t15 = _t14 + 4529;
__esp = __esp - 28;
_init();
_t18 = _t15 + -248 - _t15 + -248 >> 2;
if(_t18 == 0) {
} else {
__esi = 0;
do {
_v36 = _a12;
_v40 = _a8;
*__esp = _a4;
*((intOrPtr*)(_t15 + -248 + __esi * 4))();
__esi = __esi + 1;
} while(__esi < _t18);
}
__esp = __esp + 28;
return;
}
__i686.get_pc_thunk.bx()
{// addr = 0x0804865A
return;
}
__do_global_ctors_aux()
{// addr = 0x08048660
intOrPtr* __ebx; // r1
_unknown_ __ebp; // r6
__eax = __CTOR_LIST__;
if(__eax == 255) {
} else {
__ebx = &__CTOR_LIST__;
asm("o16 nop ");
do {
__ebx = __ebx - 4;
*__eax();
__eax = *__ebx;
} while(__eax != 255);
}
return;
}
_fini()
{// addr = 0x0804868C
_unknown_ __ebx; // r1
_unknown_ __ebp; // r6
_unknown_ _t1; // _t1
__esp = __esp - 4;
L1();
_pop(__ebx);
__do_global_dtors_aux(__esi);
_pop(__ecx);
return;
}
L08048698()
{
_unknown_ _t1; // _t1
_pop(__ebx);
__do_global_dtors_aux(__esi);
_pop(__ecx);
_pop(__ebx);
__esp = __ebp;
_pop(__ebp);
return;
}
L08048698()
{
_unknown_ _t1; // _t1
_pop(__ebx);
@rec __do_global_dtors_aux@__do_global_dtors_aux@(__esi);
_pop(__ecx);
_pop(__ebx);
__esp = __ebp;
_pop(__ebp);
return;
}
// Statistics:
// 74 Register nodes
// 35 Temporaries nodes
// 5 Casts
// 207 Statements
// 2 Labels
// 1 Gotos
// 17 Blocks
// 469 Nodes
// 10 Assembly nodes
// 27 Unknown Types
Total time: 0 seconds.
Структура программы (вначале) в общем несколько лучше, чем у Boomerang, и куда больше подробностей.
2. Бен, ай нид хелп: MEX-файл, написанный на C + BLAS, исходников которому нет.
Этот пример в посте приводить не стану, так как он длинный, но желающим попробовать своё декомпиляйшн-кунфу такая возможность предоставится:
Некоторые входные данные: это оптимизационный алгоритм для Quadratic Programming типа Branch-and-Bound (почитать тут и здесь). Алгоритм в целом прост и незатейлив, но самая сложная часть в нём - определить lower/upper bound через решение упрощённой оптимизационной задачи, и делать это быстро. Как такое сделать - хороший вопрос, и именно он меня интересует более всего.
Короче, важен не столько алгоритм, сколько его составные компоненты (стратегия и подпрограммы для lower bound estimation).
Автор этих строк, поковыряв выхлоп RecStudio, нашёл для себя подсказку на строчке 12319:
qps_mq_sub( .....
и особенно на строчке 12088:
getalp( ....
что позволило автору предположить, что для lower/upper bounds использутеся Liner Programming. Не слишком много, но по крайней мере понятно, в какую сторону копать.
Если у кого-то вдруг проявится желание потыкать в оный бинарник палочкой, IDA Pro и ещё чем - не стесняйтесь отписываться в комментариях.
Обновление: теперь можно сравнить выдачи Boomerang, RecStudio, и (спасибо, Григорий!) IDA Pro. В самом деле, выдача IDA Pro куда лучше того, что дают остальные, особенно boomerang. Можно выудить (до некоторой степени) структуру программы и даже сообщения об ошибках.
Ссылки
Интересующийся читатель может попробовать
полистать вебстраницы автора RecStudio с
полезной информацией, сходить на
wiki-ресурс по обратной разработке.
Помимо познавательных диссертаций Michael James Van Emmerik (
Boomerang) и Cristina Cifuentes [C. Cifuentes,
Reverse Compilation Techniques], есть хорошие книжки по теме:
- "Compilers - Principles, Techniques and Tools", Aho, Sethi, Ullman, 1986 Addison-Wesley Publishing Co. ISBN 0-201-10088-6.
- "Advanced Compiler Design & Implementation", Steven Muchnick, 1997 Morgan Kaufmann Publishers, ISBN 1-55860-320-4.
- "How debuggers work - Algorithms, Data Structures, and Architecture", Jonathan Rosemberg, 1996 John Wiley and Sons, ISBN 0-471-14966-7.
Дело это интересное, но весьма утомительное, хотя может помочь при раскопках очередного legacy-software и сэкономить вам полжизни.