Собираем бинарь в 3кб с GCC и разбираем из чего состоят .exe файлы

Привет, уважаемый читатель!

Для многих исполняемые файлы являются каким-то черным ящиком.
Для сисадминов - это "сундук с кодом, который че то делает", для программиста - "результат работы компилятора", а для обычного пользователя это вообще просто название.

Но тут я описал средненького админа, прикладного или веб программиста и недалекого пользователя. А что такое это все для системного программиста?



Разбираемся

  Начнем издалека. С картинок про котиков. Что такое картинка? Обычно это jpg/png файл. Он содержит в себе графическую информацию, иногда метаданные ( местоположение там и так далее ). Все это имеет в файле определенное положение. Но графические изображения - довольно простые файлы по сравнению с PE.

Стоп, что? PE? Да, Portable Executable ( PE ) - это общее название формата .exe, .dll, .sys, .com файлов.

Он также состоит из информации, которая имеет определенное положение. Как в графических - пиксели, метаданные. Только тут код и информация о том, как он хранится и что использует.

Давайте посмотрим. Простое приложение на Си, знакомое всем.

#include <stdio.h>

int main()
{
  printf("Hello, World, %d\n", 10);
  return 0;
} 

Что тут происходит? Приложение выводит Hello, World, 10. Но как? Давайте скомпилируем его при помощи gcc ( порт windows - mingw ).

Мы получили exe файл. При запуске на консоль выводит Hello, World, 10 и закрывается.

Из чего этот файл состоит? Итак, для начала. Когда мы запускаем exe файл загрузчик Windows проверяет его на валидность. То есть, является ли это экзешником?

Для этого он открывает файл для чтения и смотрит первые два байта: MZ - сигнатура PE файла. Попробуйте открыть наш исполняемый файл блокнотом и заменить там первые два байта на что-то другое. Запустим измененный файл. Я получаю такую ошибку:



Это основная сигнатура. Принадлежит она так называемому DOS заголовку. То есть, он перекочевал к нам из DOS. Там же находится смещение до PE заголовка, называемое e_lfanew.

Всего есть следующие заголовки: DOS, PE, опциональный, директория данных, таблица секций.

Рассмотрим по порядку.

В PE заголовке находится информация о типе нашего файла: EXE, DLL, ...
В опциональным то, как следует загружать нашу программу в память.

В директории данных смещение до таблицы импорта ( об этом подробнее далее )

В таблице секций находятся... внезапно, указатели на секции. Что такое вообще секции? Вспомните наш графический файл. Там всякая графическая информация, метаданные. Вот их можно разделить на две секции, например. То же самое и тут! Это секции кода, данных, ресурсов и прочей информации. Она вся структурирована.

Что такое таблица импорта

Вот и добрались мы до нее. Ради нее мы в основном и писали тот наш хеллоуворлд. Что такое вызов функции printf? Это стандартная функция библиотеке C, определена она в Windows в microsoft visual c runtime: msvcrt.dll.

Dll файл - не зря он называется библиотекой. Он ничем почти не отличается от обычной exe программы, кроме того, что все функции в ней можно вызвать из вне. То есть, это скомпилированный файл функционала разного, который делится на функции. Одна из таких функций - printf.

Когда линковщик собирает машинный код из объектных файлов, которые сделал компилятор, и видит вызов printf ( или другой функции ), то он добавляет ее в таблицу импорта. Эта таблица просто показывает, какие функции программа использует и откуда. Во время загрузки нашего файла в память загрузчиком Windows, он загружает также в память msvcrt.dll и пишет в таблицу адрес функции printf, чтобы наша программа могла его использовать.

Есть замечательный бесплатный сервис pedump.me. Он показывает основную информацию о загруженном файле, также сканирует его антивирусом и позволяет дизассемблировать точку входа ( об этом потом ).
Давайте заного скомпилируем наш файл и загрузим туда.

Последнее время сервис лагает, когда даешь прямую ссылку. Поэтому если будет недоступно - не обессудьте. http://pedump.me/731066ad98599c8cf134336d22541e27/#info

Перейдем во вкладку imports. И увидим... о Боже, огромное количество функций! Откуда они? Почему так?


Это все функции, которые использует наше маленькое на первый взгляд приложение. Почти в любой си программе есть так называемый CRT код. Именно с него начинается выполнение. Он инициализирует нашу программу для правильной работы с msvcrt библиотекой. Выделяет память, для работы malloc и так далее. 

Сам наш код после компиляции выглядит так
 ; int __cdecl main(int argc, const char **argv, const char **envp)  
 public _main  
 _main proc near  
 Format= dword ptr -10h  
 var_C= dword ptr -0Ch  
 argc= dword ptr 8  
 argv= dword ptr 0Ch  
 envp= dword ptr 10h  
 push  ebp  
 mov   ebp, esp  
 and   esp, 0FFFFFFF0h  
 sub   esp, 10h  
 call  ___main  
 mov   [esp+10h+var_C], 0Ah  
 mov   [esp+10h+Format], offset Format ; "Hello, World, %d\n"  
 call  _printf  
 mov   eax, 0  
 leave  
 retn  
 _main endp  

Этот листинг на языке ассемблера был получен при помощи интерактивного дизассемблера IDA Pro. Чтобы не вдаваться в подробности, скажу, что это мнемоническое представление команд процессора.

Это только наша функция, а там есть еще тонны кода, который добавляет в наш файл gcc.

И зачем нам это? Не надо нам это, продолжим.

Ну и где системное программирование? Хочу его!

  До этого я вам показал в общих чертах, как устроен исполняемый файл. А давайте сделаем тот же HelloWorld, только размером намного меньше!

Давайте скомпилируем при помощи gcc следующий файл.

>gcc --entry _mymain C:\c\test.c -o C:\c\out.exe -nostartfiles -node
faultlibs -mwindows -lkernel32 -luser32 -Os -s -Wl,--gc-sections

#include <windows.h> // описание WinAPI функций

int mymain()
{
  AllocConsole(); // создаем консоль
  HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // получаем идентификатор потока вывода
  
  char *pOut = (char*)VirtualAlloc(0, 256, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // выделяем память размером 256. Аналог сишного malloc(256);
  if (!pOut) 
    ExitProcess(-1);
  wsprintfA(pOut, "Hello, World, %d", 10); // собираем строку
  DWORD dwWritten;  
  WriteConsole(hOut, pOut, lstrlenA(pOut), &dwWritten, 0); // выводим в консоль  
  CloseHandle(hOut); // закрывает описатель
  VirtualFree(pOut, 0, MEM_RELEASE); // освобождает память
  ExitProcess(0); // выход из процесса
} 



Я прокоментировал. Чтобы получить подробное описание каждой из функции забиваете в гугл апи, например, WriteConsole и первая ссылка на Microsoft MSDN с описанием и прототипом функции.

Посмотрим на файл в PeDump

http://pedump.me/d9f2795493dea83b7d19d714e266299d/#info


Бинго! Ничего лишнего, никаких зависимостей. Библиотеки kernel32.dll и user32.dll есть в каждой Windows, начиная с Win98, заканчивая Win10 ( на момент статьи больше винд не было )

Правда такие трюки могут давать False Positive от антивирусов, так как их обычно используют вирусописатели для избавления своих программ от зависимостей языка.

И плюшечка от IDA Pro, в этот раз я предоставлю весь код, который только есть. Как видим, ничего лишнего!

 .text:00401000         public start  
 .text:00401000 start      proc near  
 .text:00401000  
 .text:00401000 nStdHandle   = dword ptr -38h  
 .text:00401000 dwSize     = dword ptr -34h  
 .text:00401000 flAllocationType= dword ptr -30h  
 .text:00401000 flProtect    = dword ptr -2Ch  
 .text:00401000 lpReserved   = dword ptr -28h  
 .text:00401000 NumberOfCharsWritten= dword ptr -0Ch  
 .text:00401000  
 .text:00401000         push  ebp  
 .text:00401001         mov   ebp, esp  
 .text:00401003         push  esi  
 .text:00401004         push  ebx  
 .text:00401005         sub   esp, 30h  
 .text:00401008         call  AllocConsole  
 .text:0040100D         mov   [esp+38h+nStdHandle], 0FFFFFFF5h ; nStdHandle  
 .text:00401014         call  GetStdHandle  
 .text:00401019         push  ebx  
 .text:0040101A         mov   esi, eax  
 .text:0040101C         mov   [esp+38h+flProtect], 4 ; flProtect  
 .text:00401024         mov   [esp+38h+flAllocationType], 3000h ; flAllocationType  
 .text:0040102C         mov   [esp+38h+dwSize], 100h ; dwSize  
 .text:00401034         mov   [esp+38h+nStdHandle], 0 ; lpAddress  
 .text:0040103B         call  VirtualAlloc  
 .text:00401040         sub   esp, 10h  
 .text:00401043         test  eax, eax  
 .text:00401045         mov   ebx, eax  
 .text:00401047         jnz   short loc_401052  
 .text:00401049         mov   [esp+38h+nStdHandle], 0FFFFFFFFh  
 .text:00401050         jmp   short loc_4010C0  
 .text:00401052 ; ---------------------------------------------------------------------------  
 .text:00401052  
 .text:00401052 loc_401052:               ; CODE XREF: start+47 j  
 .text:00401052         mov   [esp+38h+flAllocationType], 0Ah  
 .text:0040105A         mov   [esp+38h+dwSize], offset aHelloWorldD ; "Hello, World, %d"  
 .text:00401062         mov   [esp+38h+nStdHandle], eax ; LPSTR  
 .text:00401065         call  wsprintfA  
 .text:0040106A         mov   [esp+38h+nStdHandle], ebx ; lpString  
 .text:0040106D         call  lstrlenA  
 .text:00401072         push  edx  
 .text:00401073         lea   edx, [ebp+NumberOfCharsWritten]  
 .text:00401076         mov   [esp+38h+lpReserved], 0 ; lpReserved  
 .text:0040107E         mov   [esp+38h+flProtect], edx ; lpNumberOfCharsWritten  
 .text:00401082         mov   [esp+38h+flAllocationType], eax ; nNumberOfCharsToWrite  
 .text:00401086         mov   [esp+38h+dwSize], ebx ; lpBuffer  
 .text:0040108A         mov   [esp+38h+nStdHandle], esi ; hConsoleOutput  
 .text:0040108D         call  WriteConsoleA  
 .text:00401092         sub   esp, 14h  
 .text:00401095         mov   [esp+38h+nStdHandle], esi ; hObject  
 .text:00401098         call  CloseHandle  
 .text:0040109D         push  ecx  
 .text:0040109E         mov   [esp+38h+flAllocationType], 8000h ; dwFreeType  
 .text:004010A6         mov   [esp+38h+dwSize], 0 ; dwSize  
 .text:004010AE         mov   [esp+38h+nStdHandle], ebx ; lpAddress  
 .text:004010B1         call  VirtualFree  
 .text:004010B6         sub   esp, 0Ch  
 .text:004010B9         mov   [esp+38h+nStdHandle], 0 ; uExitCode  
 .text:004010C0  
 .text:004010C0 loc_4010C0:               ; CODE XREF: start+50 j  
 .text:004010C0         call  ExitProcess  
 .text:004010C0 start      endp  

Комментариев нет:

Отправить комментарий